From 8d87bb0d2a237aabe53a44ee6f75bf6df91369bf Mon Sep 17 00:00:00 2001 From: dwhitestratiform <52459927+dwhitestratiform@users.noreply.github.com> Date: Mon, 22 Jan 2024 15:46:29 -0500 Subject: [PATCH 01/41] Making QMR Dev Environments Not Publicly Accessible (#2036) Co-authored-by: dwhite_stratiform Co-authored-by: dwhite_stratiform Co-authored-by: Berry Davenport --- .github/build_vars.sh | 2 + .github/waf-controller.sh | 132 +++++++++++++++++++++++++ .github/workflows/cypress-workflow.yml | 2 +- .github/workflows/deploy.yml | 101 ++++++++++++++++++- services/ui/package.json | 3 + services/ui/serverless.yml | 102 ++++++++++++------- services/ui/yarn.lock | 5 + 7 files changed, 310 insertions(+), 37 deletions(-) create mode 100755 .github/waf-controller.sh diff --git a/.github/build_vars.sh b/.github/build_vars.sh index 1b5c603cba..380179a9cb 100755 --- a/.github/build_vars.sh +++ b/.github/build_vars.sh @@ -13,12 +13,14 @@ set_value() { if [ ! -z "${!varname}" ]; then echo "Setting $varname" echo "${varname}=${!varname}" >> $GITHUB_ENV + echo "${varname}=${!varname}" >> $GITHUB_OUTPUT fi } set_name() { varname=${1} echo "BRANCH_SPECIFIC_VARNAME_$varname=${branch_name//-/_}_$varname" >> $GITHUB_ENV + echo "BRANCH_SPECIFIC_VARNAME_$varname=${branch_name//-/_}_$varname" >> $GITHUB_OUTPUT } action=${1} diff --git a/.github/waf-controller.sh b/.github/waf-controller.sh new file mode 100755 index 0000000000..6f57206b0b --- /dev/null +++ b/.github/waf-controller.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash + +CIRCUIT_BREAKER=10 +AWS_RETRY_ERROR=254 +AWS_THROTTLING_EXCEPTION=252 +#0, 1, 2 are the levels of debug, with 0 being off +DEBUG=1 + +set -o pipefail -o nounset -u + +case ${1} in + append) + OP=append + ;; + set) + OP=set + ;; + *) + echo "Error: unkown operation" + echo "Usage: ${0} [append|set]" && exit 1 + ;; +esac + +shift +NAME="${1}" +ID="${2}" +shift; shift +RUNNER_CIDRS="${@}" + +[[ $DEBUG -ge 1 ]] && echo "Inputs: NAME ${NAME}, ID ${ID}, RUNNER_CIDRS ${RUNNER_CIDRS}" + +#Exponential backoff with jitter +jitter() { + #.5 seconds + SHORTEST=50 + #10 seconds + LONGEST=1000 + DIV=100 + EXP=$(perl -e "use bigint; print $SHORTEST**$1") + MIN=$(($EXP>$LONGEST ? $LONGEST : $EXP)) + RND=$(shuf -i$SHORTEST-$MIN -n1) + perl -e "print $RND/$DIV" +} + +#Attempt to avoid resource contention from the start +sleep $(jitter $(shuf -i1-10 -n1)) + +for ((i=1; i <= $CIRCUIT_BREAKER; i++)); do + #This loop is ONLY for retrying if the retries exceeded exception is thrown + for ((j=1; j <= $CIRCUIT_BREAKER; j++)); do + #Read WAF configuration from AWS + WAF_CONFIG=$(aws wafv2 get-ip-set --scope CLOUDFRONT --id ${ID} --name ${NAME} 2>&1) + CMD_CD=$? + [[ $DEBUG -ge 1 ]] && echo "AWS CLI Read Response Code: ${CMD_CD}" + [[ $DEBUG -ge 2 ]] && echo "AWS CLI Read Response: ${WAF_CONFIG}" + + #If the retries exceeded error code is returned, try again, otherwise exit the loop + [[ $CMD_CD -eq $AWS_RETRY_ERROR ]] || break + + SLEEP_FOR=$(jitter ${j}) + echo "CLI retries exceed. Waiting for ${SLEEP_FOR} seconds to execute read again...$({j})" + sleep ${SLEEP_FOR} + done + + #Unable to get the lock tocken and IP set so there isn't any point in attempting the write op + [[ $j -ge $CIRCUIT_BREAKER ]] && echo “Attempts to read WAF IPSet exceeded” && sleep $(jitter ${i}) && continue + + #The loop was short circuited with an error code other than 0, so something is wrong + [[ $CMD_CD -eq 0 ]] || ( echo "An unexpected read error occurred: ${CMD_CD}" && exit 2 ) + + echo "Read was successful." + + if [ ${OP} == "append" ]; then + ##If this is used to whitelist individual ips or cidrs, using an additive approach is what is required + #Parse out IP set addresses to array + IP_ADDRESSES=($(jq -r '.IPSet.Addresses | .[]' <<< ${WAF_CONFIG})) + + #If CIDR is already present in IP set, eject + grep -q $RUNNER_CIDRS <<< ${IP_ADDRESSES} + [[ $? -ne 0 ]] || ( echo "CIDR is present in IP Set." && exit 0 ) + + #Add runner CIDR to array + IP_ADDRESSES+=("$RUNNER_CIDRS") + else + ##If this is used to hard set the IP set, just clobber it + IP_ADDRESSES=("$RUNNER_CIDRS") + fi + + #Stringify IPs + STRINGIFIED=$(echo $(IFS=" " ; echo "${IP_ADDRESSES[*]}")) + [[ $DEBUG -ge 2 ]] && echo "Ip Addresses: ${STRINGIFIED}" + + #Parse out optimistic concurrency control token + OCC_TOKEN=$(jq -r '.LockToken' <<< ${WAF_CONFIG}) + [[ $DEBUG -ge 2 ]] && echo "LockToken: ${OCC_TOKEN}" + + #This loop is ONLY for retrying if the retries exceeded exception is thrown + for ((k=1; k <= $CIRCUIT_BREAKER; k++)); do + #Write updated WAF configuration to AWS + OUTPUT=$(aws wafv2 update-ip-set --scope CLOUDFRONT --id ${ID} --name ${NAME} --lock-token ${OCC_TOKEN} --addresses ${STRINGIFIED} 2>&1) + CMD_CD=$? + [[ $DEBUG -ge 1 ]] && echo "AWS CLI Write Response Code: ${CMD_CD}" + [[ $DEBUG -ge 2 ]] && echo "AWS CLI Write Response: ${OUTPUT}" + + #If the retries exceeded error code is returned, try again, otherwise exit the loop + [[ $CMD_CD -eq $AWS_RETRY_ERROR ]] || break + #If WAFOptimisticLockException error code is returned, exit the loop + [[ "$OUTPUT" =~ "WAFOptimisticLockException" ]] && break + + SLEEP_FOR=$(jitter ${k}) + echo "CLI retries exceed. Waiting for ${SLEEP_FOR} seconds to execute write again...(${k})" + sleep ${SLEEP_FOR} + done + + [[ $CMD_CD -ne 0 ]] || break + #Still not having success, so try again + + echo "Exit Code: ${CMD_CD}" + + SLEEP_FOR=$(jitter ${i}) + echo "Waiting for ${SLEEP_FOR} seconds to execute main loop again...(${i})" + sleep ${SLEEP_FOR} +done + +[[ $DEBUG -ge 1 ]] && echo "Attempts to update ip set: $i" + +[[ $i -gt $CIRCUIT_BREAKER ]] && echo “Attempts to update WAF IPSet exceeded, exiting.” && exit 2 + +echo "Applied the IP Set successfully." + +#Things should not have made it this far without being able to successfully write the IP Set +exit $CMD_CD diff --git a/.github/workflows/cypress-workflow.yml b/.github/workflows/cypress-workflow.yml index 6297aae216..b20bc320c0 100644 --- a/.github/workflows/cypress-workflow.yml +++ b/.github/workflows/cypress-workflow.yml @@ -46,7 +46,7 @@ jobs: runs-on: ubuntu-latest container: image: cypress/browsers:node16.16.0-chrome107-ff107 - options: --user 1001 + options: --user root needs: setup strategy: fail-fast: false diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c8415d296a..2cc84ab594 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -38,6 +38,16 @@ jobs: echo "branch_name=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV fi - uses: actions/checkout@v3 + - name: 'Setup jq' + uses: dcarbone/install-jq-action@v2.1.0 + with: + version: '${{ inputs.version }}' + force: '${{ inputs.force }}' + - name: 'Check jq' + # language=sh + run: | + which jq + jq --version - name: Validate branch name run: ./.github/branchNameValidation.sh $STAGE_PREFIX$branch_name - name: set branch specific variable names @@ -80,6 +90,8 @@ jobs: working-directory: services outputs: application_endpoint: ${{ steps.endpoint.outputs.application_endpoint}} + BRANCH_SPECIFIC_VARNAME_AWS_DEFAULT_REGION: ${{ steps.set_names.outputs.BRANCH_SPECIFIC_VARNAME_AWS_DEFAULT_REGION }} + BRANCH_SPECIFIC_VARNAME_AWS_OIDC_ROLE_TO_ASSUME: ${{ steps.set_names.outputs.BRANCH_SPECIFIC_VARNAME_AWS_OIDC_ROLE_TO_ASSUME }} # run e2e tests after deploy completes e2e-tests-init: name: Initialize End To End Tests @@ -96,14 +108,19 @@ jobs: - uses: actions/checkout@v3 - name: set branch_name run: | - if [[ "$GITHUB_REF" =~ ^refs/heads/dependabot/.* ]]; then # Dependabot builds very long branch names. This is a switch to make it shorter. - echo "branch_name=`echo ${GITHUB_REF#refs/heads/} | md5sum | head -c 10 | sed 's/^/x/'`" >> $GITHUB_ENV + echo "GITHUB_REF=${GITHUB_REF}" + if [[ "$GITHUB_REF" =~ ^refs/heads/dependabot/.* ]]; then + echo "branch_name=`echo ${GITHUB_REF##*/*-} | md5sum | head -c 10 | sed 's/^/x/'`" >> $GITHUB_ENV + elif [[ "$GITHUB_REF" =~ ^refs/.*/snyk-* ]]; then + echo "branch_name=`echo ${GITHUB_REF##*/*-} | head -c 10 | sed 's/^/s/'`" >> $GITHUB_ENV else echo "branch_name=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV fi - name: set branch specific variable names + id: set_names run: ./.github/build_vars.sh set_names - name: set variable values + id: set_values run: ./.github/build_vars.sh set_values env: AWS_DEFAULT_REGION: ${{ secrets[env.BRANCH_SPECIFIC_VARNAME_AWS_DEFAULT_REGION] || secrets.AWS_DEFAULT_REGION }} @@ -139,13 +156,63 @@ jobs: - name: set path run: | echo "PATH=$(pwd)/node_modules/.bin/:$PATH" >> $GITHUB_ENV + - name: Get Runner IP + id: get-ip + run: | + #!/bin/bash + # Get the IP address of the runner + IP_ADDRESS=$(curl https://api.ipify.org) + echo "Runner IP address: $IP_ADDRESS" + # Store the IP address as an output variable + echo "RUNNER_IP=$IP_ADDRESS/32" >> $GITHUB_OUTPUT + - name: Get Github Actions CIDR Blocks + id: get-gha-cidrs + shell: bash + run: | + #!/bin/bash + GHA_RESP=$(curl --header 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' https://api.github.com/meta) + echo "Response for GHA runner CIDR blocks: $GHA_RESP" + IPV4_CIDR_ARR=($(echo $GHA_RESP | jq -r '.actions | .[]' | grep -v ':')) + GHA_CIDRS_IPV4=$(echo $(IFS=" "; echo ${IPV4_CIDR_ARR[*]})) + echo "GHA_CIDRS_IPV4=$GHA_CIDRS_IPV4" >> $GITHUB_OUTPUT + - name: Generate IP Set Name + id: gen-ip-set-name + run: | + STAGE_GH_IPSET_NAME=$STAGE_PREFIX$branch_name-gh-ipset + echo "Github IP Set name: $STAGE_GH_IPSET_NAME" + echo "STAGE_GH_IPSET_NAME=$STAGE_GH_IPSET_NAME" >> $GITHUB_OUTPUT + - name: Fetch AWS IP set Metadata + id: fetch-ip-set-info + run: | + #!/bin/bash + # Fetch AWS IP set ARNs using AWS CLI and store them in a variable + AWS_IP_SET_INFO=$(aws wafv2 list-ip-sets --scope=CLOUDFRONT) + # Store the IP set ARNs in an output variable using GITHUB_OUTPUT + IPSET_NAME=${{ steps.gen-ip-set-name.outputs.STAGE_GH_IPSET_NAME }} + IPSET=$(jq '.IPSets | map(select(.Name == "'${IPSET_NAME}'")) | .[]' <<< ${AWS_IP_SET_INFO}) + [ -z "$IPSET" ] && echo "IP Set with name ${IPSET_NAME} was not located. Exiting..." && exit 1 + echo "IP Set metadata: ${IPSET}" + #Get Values from the IP SET + IPSET_ID=$(jq -r '.Id' <<< ${IPSET}) + echo "IPSET_ARN=$IPSET_ARN" >> $GITHUB_OUTPUT + echo "IPSET_NAME=$IPSET_NAME" >> $GITHUB_OUTPUT + echo "IPSET_ID=$IPSET_ID" >> $GITHUB_OUTPUT + - name: Update IP Set + id: update-ip-set + run: ./.github/waf-controller.sh set ${{ steps.fetch-ip-set-info.outputs.IPSET_NAME }} ${{ steps.fetch-ip-set-info.outputs.IPSET_ID }} ${{ steps.get-gha-cidrs.outputs.GHA_CIDRS_IPV4 }} + env: + AWS_RETRY_MODE: adaptive + AWS_MAX_ATTEMPTS: 10 outputs: application_endpoint: ${{ needs.deploy.outputs.application_endpoint }} + ipset_name: ${{ steps.fetch-ip-set-info.outputs.IPSET_NAME }} + ipset_id: ${{ steps.fetch-ip-set-info.outputs.IPSET_ID }} setup-tests: name: "Setup End To End Tests" uses: ./.github/workflows/cypress-workflow.yml - needs: e2e-tests-init + needs: + - e2e-tests-init with: test-path: "init" test-endpoint: "${{ needs.e2e-tests-init.outputs.application_endpoint }}" @@ -244,3 +311,31 @@ jobs: cypress-user3: ${{ secrets.CYPRESS_TEST_USER_3 }} cypress-user4: ${{ secrets.CYPRESS_TEST_USER_4 }} cypress-password: ${{ secrets.CYPRESS_TEST_PASSWORD_1 }} + + cleanup: + name: Deslist GHA Runner CIDR Blocks + if: ${{ github.ref != 'refs/heads/master' && github.ref != 'refs/heads/val' && github.ref != 'refs/heads/prod' }} + runs-on: ubuntu-latest + needs: + - e2e-tests-init + - e2e-feature-tests + - child-e2e-measure-tests + - adult-e2e-measure-tests + - health-home-e2e-measure-tests + - deploy + - a11y-tests + env: + SLS_DEPRECATION_DISABLE: "*" # Turn off deprecation warnings in the pipeline + steps: + - uses: actions/checkout@v3 + - name: Configure AWS credentials for GitHub Actions + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets[env.BRANCH_SPECIFIC_VARNAME_AWS_OIDC_ROLE_TO_ASSUME] || secrets.AWS_OIDC_ROLE_TO_ASSUME }} + aws-region: ${{ secrets[env.BRANCH_SPECIFIC_VARNAME_AWS_DEFAULT_REGION] || secrets.AWS_DEFAULT_REGION }} + - name: clean-up-iplist + id: reset-ip-set + run: ./.github/waf-controller.sh set ${{ needs.e2e-tests-init.outputs.ipset_name }} ${{ needs.e2e-tests-init.outputs.ipset_id }} '[]' + env: + AWS_RETRY_MODE: adaptive + AWS_MAX_ATTEMPTS: 10 diff --git a/services/ui/package.json b/services/ui/package.json index 90324d6171..dd977525f2 100644 --- a/services/ui/package.json +++ b/services/ui/package.json @@ -10,5 +10,8 @@ "license": "CC0-1.0", "devDependencies": { "serverless-s3-bucket-helper": "Enterprise-CMCS/serverless-s3-bucket-helper#0.1.1" + }, + "dependencies": { + "@enterprise-cmcs/serverless-waf-plugin": "^1.3.1" } } diff --git a/services/ui/serverless.yml b/services/ui/serverless.yml index 5831f7dcf9..ea40b9195e 100644 --- a/services/ui/serverless.yml +++ b/services/ui/serverless.yml @@ -17,6 +17,7 @@ plugins: - serverless-stack-termination-protection - serverless-iam-helper - serverless-s3-bucket-helper + - "@enterprise-cmcs/serverless-waf-plugin" custom: stage: ${opt:stage, self:provider.stage} @@ -30,18 +31,75 @@ custom: route53DomainName: ${ssm:/configuration/${self:custom.stage}/route53/domainName, ""} cloudfrontCertificateArn: ${ssm:/configuration/${self:custom.stage}/cloudfront/certificateArn, ssm:/configuration/default/cloudfront/certificateArn, ""} cloudfrontDomainName: ${ssm:/configuration/${self:custom.stage}/cloudfront/domainName, ""} - webAclName: ${self:service}-${self:custom.stage}-webacl + vpnIpSetArn: ${ssm:/configuration/${self:custom.stage}/vpnIpSetArn, ssm:/configuration/default/vpnIpSetArn, ""} + # + wafExcludeRules: + wafScope: CLOUDFRONT + wafPlugin: + name: ${self:service}-${self:custom.stage}-webacl + rules: + - enable: ${param:restrictToVpn} + rule: + Name: vpn-only + Priority: 0 + Action: + Allow: {} + VisibilityConfig: + SampledRequestsEnabled: true + CloudWatchMetricsEnabled: true + MetricName: ${self:service}-${self:custom.stage}-webacl-vpn-only + Statement: + IPSetReferenceStatement: + ARN: ${self:custom.vpnIpSetArn} + - enable: ${param:restrictToVpn} + rule: + Name: github-only + Priority: 1 + Action: + Allow: {} + VisibilityConfig: + SampledRequestsEnabled: true + CloudWatchMetricsEnabled: true + MetricName: ${self:service}-${self:custom.stage}-tmp-gh-runner + Statement: + IPSetReferenceStatement: + ARN: !GetAtt GitHubIPSet.Arn + - enable: ${param:restrictToVpn} + rule: + Name: block-all-other-traffic + Priority: 3 + Action: + Block: + CustomResponse: + ResponseCode: 403 + VisibilityConfig: + SampledRequestsEnabled: true + CloudWatchMetricsEnabled: true + MetricName: ${self:service}-${self:custom.stage}-block-traffic + Statement: + NotStatement: + Statement: + IPSetReferenceStatement: + ARN: ${self:custom.vpnIpSetArn} firehoseStreamName: aws-waf-logs-${self:service}-${self:custom.stage}-firehose scripts: hooks: # Associate the WAF ACL with the Firehose Delivery Stream deploy:finalize: | - wafAclArn=`aws wafv2 list-web-acls --scope CLOUDFRONT | jq -r '.WebACLs | .[] | select(.Name=="${self:custom.webAclName}") | .ARN'` + wafAclArn=`aws wafv2 list-web-acls --scope CLOUDFRONT | jq -r '.WebACLs | .[] | select(.Name=="${self:custom.wafPlugin.name}") | .ARN'` firehoseStreamArn=`aws firehose describe-delivery-stream --delivery-stream-name ${self:custom.firehoseStreamName} | jq -r '.DeliveryStreamDescription.DeliveryStreamARN'` aws wafv2 put-logging-configuration \ --logging-configuration ResourceArn=$wafAclArn,LogDestinationConfigs=$firehoseStreamArn \ --region ${self:provider.region} - +params: + prod: + restrictToVpn: false + val: + restrictToVpn: false + master: + restrictToVpn: true + default: + restrictToVpn: true resources: Conditions: CreateDnsRecord: @@ -65,6 +123,13 @@ resources: - "" - ${self:custom.cloudfrontDomainName} Resources: + GitHubIPSet: + Type: AWS::WAFv2::IPSet + Properties: + Name: ${self:custom.stage}-gh-ipset + Scope: CLOUDFRONT + IPAddressVersion: IPV4 + Addresses: [] S3Bucket: Type: AWS::S3::Bucket Properties: @@ -137,35 +202,6 @@ resources: Bool: aws:SecureTransport: false Bucket: !Ref CloudFrontLoggingBucket - CloudFrontWebAcl: - Type: AWS::WAFv2::WebACL - Properties: - Name: ${self:custom.webAclName} - DefaultAction: - Block: {} - Rules: - - Action: - Allow: {} - Name: ${self:custom.webAclName}-allow-usa-plus-territories - Priority: 0 - Statement: - GeoMatchStatement: - CountryCodes: - - GU # Guam - - PR # Puerto Rico - - US # USA - - UM # US Minor Outlying Islands - - VI # US Virgin Islands - - MP # Northern Mariana Islands - VisibilityConfig: - SampledRequestsEnabled: true - CloudWatchMetricsEnabled: true - MetricName: WafWebAcl - Scope: CLOUDFRONT - VisibilityConfig: - CloudWatchMetricsEnabled: true - SampledRequestsEnabled: true - MetricName: ${self:custom.stage}-webacl CloudFrontOriginAccessIdentity: Type: AWS::CloudFront::CloudFrontOriginAccessIdentity Properties: @@ -214,7 +250,7 @@ resources: - ErrorCode: 403 ResponseCode: 403 ResponsePagePath: /index.html - WebACLId: !GetAtt CloudFrontWebAcl.Arn + WebACLId: !GetAtt WafPluginAcl.Arn Logging: Bucket: !Sub "${CloudFrontLoggingBucket}.s3.amazonaws.com" Prefix: AWSLogs/CLOUDFRONT/${self:custom.stage}/ diff --git a/services/ui/yarn.lock b/services/ui/yarn.lock index 8886796bb4..a38b95ee47 100644 --- a/services/ui/yarn.lock +++ b/services/ui/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@enterprise-cmcs/serverless-waf-plugin@^1.3.1": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@enterprise-cmcs/serverless-waf-plugin/-/serverless-waf-plugin-1.3.2.tgz#66efd0b91326b7d1b045ab7ea7ba5826ed2e635d" + integrity sha512-577MWRddWK2uPEaeUMorOFQq6rhUhGwbdmz+tuKaU9+v77/bDQPqoc6cmhF2oYMswqpxvMgW0P07HAAcmKtquw== + serverless-s3-bucket-helper@Enterprise-CMCS/serverless-s3-bucket-helper#0.1.1: version "1.0.0" resolved "https://codeload.github.com/Enterprise-CMCS/serverless-s3-bucket-helper/tar.gz/f0f6d6a1ffe54e292f0afc93777764bce16a4037" From d55e34987687dbaaeb21d4b0f52e70e09877f2a5 Mon Sep 17 00:00:00 2001 From: dwhitestratiform <52459927+dwhitestratiform@users.noreply.github.com> Date: Tue, 23 Jan 2024 11:00:31 -0500 Subject: [PATCH 02/41] Rename existing WAF to fix existing environment build failure (#2037) Co-authored-by: dwhite_stratiform --- services/ui/serverless.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/ui/serverless.yml b/services/ui/serverless.yml index ea40b9195e..bf274b2192 100644 --- a/services/ui/serverless.yml +++ b/services/ui/serverless.yml @@ -36,7 +36,7 @@ custom: wafExcludeRules: wafScope: CLOUDFRONT wafPlugin: - name: ${self:service}-${self:custom.stage}-webacl + name: ${self:service}-${self:custom.stage}-webacl-waf rules: - enable: ${param:restrictToVpn} rule: From 690f948010402a0462fe4940dae43fe7aa3dd267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karla=20Valc=C3=A1rcel=20Mart=C3=ADnez?= <99458559+karla-vm@users.noreply.github.com> Date: Thu, 25 Jan 2024 13:16:19 -0500 Subject: [PATCH 03/41] CMDCT-3179: FFY2024 Setup (#2039) --- README.md | 14 +- .../handlers/dynamoUtils/measureList.ts | 338 ++ .../handlers/measures/tests/get.test.ts | 2 +- services/ui-src/src/config.ts | 2 +- services/ui-src/src/dataConstants.ts | 1 + .../ui-src/src/hooks/api/useGetCoreSets.tsx | 4 +- services/ui-src/src/libs/spaLib.ts | 124 + .../ui-src/src/measures/2024/AABAD/data.ts | 15 + .../src/measures/2024/AABAD/index.test.tsx | 244 ++ .../ui-src/src/measures/2024/AABAD/index.tsx | 80 + .../ui-src/src/measures/2024/AABAD/types.ts | 3 + .../src/measures/2024/AABAD/validation.ts | 77 + .../ui-src/src/measures/2024/AABCH/data.ts | 14 + .../src/measures/2024/AABCH/index.test.tsx | 239 ++ .../ui-src/src/measures/2024/AABCH/index.tsx | 85 + .../ui-src/src/measures/2024/AABCH/types.ts | 3 + .../src/measures/2024/AABCH/validation.ts | 73 + .../ui-src/src/measures/2024/ADDCH/data.ts | 58 + .../src/measures/2024/ADDCH/index.test.tsx | 251 ++ .../ui-src/src/measures/2024/ADDCH/index.tsx | 67 + .../ui-src/src/measures/2024/ADDCH/types.ts | 3 + .../src/measures/2024/ADDCH/validation.ts | 78 + .../ui-src/src/measures/2024/AIFHH/data.ts | 89 + .../src/measures/2024/AIFHH/index.test.tsx | 383 ++ .../ui-src/src/measures/2024/AIFHH/index.tsx | 85 + .../ui-src/src/measures/2024/AIFHH/types.ts | 3 + .../src/measures/2024/AIFHH/validation.ts | 124 + .../ui-src/src/measures/2024/AMBCH/data.ts | 12 + .../src/measures/2024/AMBCH/index.test.tsx | 274 ++ .../ui-src/src/measures/2024/AMBCH/index.tsx | 85 + .../ui-src/src/measures/2024/AMBCH/types.ts | 3 + .../src/measures/2024/AMBCH/validation.ts | 71 + .../ui-src/src/measures/2024/AMBHH/data.ts | 12 + .../src/measures/2024/AMBHH/index.test.tsx | 257 ++ .../ui-src/src/measures/2024/AMBHH/index.tsx | 84 + .../ui-src/src/measures/2024/AMBHH/types.ts | 3 + .../src/measures/2024/AMBHH/validation.ts | 85 + .../ui-src/src/measures/2024/AMMAD/data.ts | 48 + .../src/measures/2024/AMMAD/index.test.tsx | 260 ++ .../ui-src/src/measures/2024/AMMAD/index.tsx | 68 + .../ui-src/src/measures/2024/AMMAD/types.ts | 3 + .../src/measures/2024/AMMAD/validation.ts | 105 + .../ui-src/src/measures/2024/AMRAD/data.ts | 13 + .../src/measures/2024/AMRAD/index.test.tsx | 269 ++ .../ui-src/src/measures/2024/AMRAD/index.tsx | 115 + .../ui-src/src/measures/2024/AMRAD/types.ts | 106 + .../src/measures/2024/AMRAD/validation.ts | 73 + .../ui-src/src/measures/2024/AMRCH/data.ts | 12 + .../src/measures/2024/AMRCH/index.test.tsx | 260 ++ .../ui-src/src/measures/2024/AMRCH/index.tsx | 68 + .../ui-src/src/measures/2024/AMRCH/types.ts | 3 + .../src/measures/2024/AMRCH/validation.ts | 73 + .../ui-src/src/measures/2024/APMCH/data.ts | 50 + .../src/measures/2024/APMCH/index.test.tsx | 304 ++ .../ui-src/src/measures/2024/APMCH/index.tsx | 68 + .../ui-src/src/measures/2024/APMCH/types.ts | 3 + .../src/measures/2024/APMCH/validation.ts | 108 + .../ui-src/src/measures/2024/APPCH/data.ts | 12 + .../src/measures/2024/APPCH/index.test.tsx | 260 ++ .../ui-src/src/measures/2024/APPCH/index.tsx | 68 + .../ui-src/src/measures/2024/APPCH/types.ts | 3 + .../src/measures/2024/APPCH/validation.ts | 78 + .../ui-src/src/measures/2024/BCSAD/data.ts | 50 + .../src/measures/2024/BCSAD/index.test.tsx | 245 ++ .../ui-src/src/measures/2024/BCSAD/index.tsx | 68 + .../ui-src/src/measures/2024/BCSAD/types.ts | 3 + .../src/measures/2024/BCSAD/validation.ts | 81 + .../ui-src/src/measures/2024/CBPAD/data.ts | 74 + .../src/measures/2024/CBPAD/index.test.tsx | 245 ++ .../ui-src/src/measures/2024/CBPAD/index.tsx | 67 + .../ui-src/src/measures/2024/CBPAD/types.ts | 3 + .../src/measures/2024/CBPAD/validation.ts | 81 + .../ui-src/src/measures/2024/CBPHH/data.ts | 74 + .../src/measures/2024/CBPHH/index.test.tsx | 258 ++ .../ui-src/src/measures/2024/CBPHH/index.tsx | 69 + .../ui-src/src/measures/2024/CBPHH/types.ts | 3 + .../src/measures/2024/CBPHH/validation.ts | 83 + .../ui-src/src/measures/2024/CCPAD/data.ts | 16 + .../src/measures/2024/CCPAD/index.test.tsx | 262 ++ .../ui-src/src/measures/2024/CCPAD/index.tsx | 68 + .../ui-src/src/measures/2024/CCPAD/types.ts | 3 + .../src/measures/2024/CCPAD/validation.ts | 79 + .../ui-src/src/measures/2024/CCPCH/data.ts | 16 + .../src/measures/2024/CCPCH/index.test.tsx | 269 ++ .../ui-src/src/measures/2024/CCPCH/index.tsx | 69 + .../ui-src/src/measures/2024/CCPCH/types.ts | 3 + .../src/measures/2024/CCPCH/validation.ts | 78 + .../ui-src/src/measures/2024/CCSAD/data.ts | 79 + .../src/measures/2024/CCSAD/index.test.tsx | 239 ++ .../ui-src/src/measures/2024/CCSAD/index.tsx | 68 + .../ui-src/src/measures/2024/CCSAD/types.ts | 3 + .../src/measures/2024/CCSAD/validation.ts | 74 + .../ui-src/src/measures/2024/CCWAD/data.ts | 16 + .../src/measures/2024/CCWAD/index.test.tsx | 255 ++ .../ui-src/src/measures/2024/CCWAD/index.tsx | 68 + .../ui-src/src/measures/2024/CCWAD/types.ts | 11 + .../src/measures/2024/CCWAD/validation.ts | 76 + .../ui-src/src/measures/2024/CCWCH/data.ts | 16 + .../src/measures/2024/CCWCH/index.test.tsx | 268 ++ .../ui-src/src/measures/2024/CCWCH/index.tsx | 68 + .../ui-src/src/measures/2024/CCWCH/types.ts | 3 + .../src/measures/2024/CCWCH/validation.ts | 74 + .../ui-src/src/measures/2024/CDFAD/data.ts | 46 + .../src/measures/2024/CDFAD/index.test.tsx | 248 ++ .../ui-src/src/measures/2024/CDFAD/index.tsx | 67 + .../ui-src/src/measures/2024/CDFAD/types.ts | 3 + .../src/measures/2024/CDFAD/validation.ts | 79 + .../ui-src/src/measures/2024/CDFCH/data.ts | 46 + .../src/measures/2024/CDFCH/index.test.tsx | 248 ++ .../ui-src/src/measures/2024/CDFCH/index.tsx | 67 + .../ui-src/src/measures/2024/CDFCH/types.ts | 3 + .../src/measures/2024/CDFCH/validation.ts | 79 + .../ui-src/src/measures/2024/CDFHH/data.ts | 45 + .../src/measures/2024/CDFHH/index.test.tsx | 258 ++ .../ui-src/src/measures/2024/CDFHH/index.tsx | 75 + .../ui-src/src/measures/2024/CDFHH/types.ts | 3 + .../src/measures/2024/CDFHH/validation.ts | 89 + .../ui-src/src/measures/2024/CHLAD/data.ts | 46 + .../src/measures/2024/CHLAD/index.test.tsx | 240 ++ .../ui-src/src/measures/2024/CHLAD/index.tsx | 68 + .../ui-src/src/measures/2024/CHLAD/types.ts | 3 + .../src/measures/2024/CHLAD/validation.ts | 72 + .../ui-src/src/measures/2024/CHLCH/data.ts | 46 + .../src/measures/2024/CHLCH/index.test.tsx | 248 ++ .../ui-src/src/measures/2024/CHLCH/index.tsx | 68 + .../ui-src/src/measures/2024/CHLCH/types.ts | 3 + .../src/measures/2024/CHLCH/validation.ts | 72 + .../ui-src/src/measures/2024/CISCH/data.ts | 84 + .../src/measures/2024/CISCH/index.test.tsx | 287 ++ .../ui-src/src/measures/2024/CISCH/index.tsx | 67 + .../ui-src/src/measures/2024/CISCH/types.ts | 3 + .../src/measures/2024/CISCH/validation.ts | 75 + .../ui-src/src/measures/2024/COBAD/data.ts | 13 + .../src/measures/2024/COBAD/index.test.tsx | 250 ++ .../ui-src/src/measures/2024/COBAD/index.tsx | 67 + .../ui-src/src/measures/2024/COBAD/types.ts | 3 + .../src/measures/2024/COBAD/validation.ts | 82 + .../ui-src/src/measures/2024/COLAD/data.ts | 50 + .../src/measures/2024/COLAD/index.test.tsx | 248 ++ .../ui-src/src/measures/2024/COLAD/index.tsx | 67 + .../ui-src/src/measures/2024/COLAD/types.ts | 3 + .../src/measures/2024/COLAD/validation.ts | 84 + .../ui-src/src/measures/2024/COLHH/data.ts | 49 + .../src/measures/2024/COLHH/index.test.tsx | 258 ++ .../ui-src/src/measures/2024/COLHH/index.tsx | 68 + .../ui-src/src/measures/2024/COLHH/types.ts | 3 + .../src/measures/2024/COLHH/validation.ts | 84 + .../src/measures/2024/CPAAD/index.test.tsx | 158 + .../ui-src/src/measures/2024/CPAAD/index.tsx | 43 + .../2024/CPAAD/questions/DataSource.tsx | 95 + .../questions/DefinitionOfPopulation.tsx | 66 + .../2024/CPAAD/questions/HowDidYouReport.tsx | 38 + .../CPAAD/questions/PerformanceMeasure.tsx | 16 + .../2024/CPAAD/questions/Reporting.tsx | 41 + .../CPAAD/questions/WhyDidYouNotCollect.tsx | 192 + .../measures/2024/CPAAD/questions/index.tsx | 6 + .../ui-src/src/measures/2024/CPAAD/types.ts | 39 + .../src/measures/2024/CPAAD/validation.ts | 25 + .../src/measures/2024/CPCCH/index.test.tsx | 158 + .../ui-src/src/measures/2024/CPCCH/index.tsx | 44 + .../2024/CPCCH/questions/DataSource.tsx | 86 + .../questions/DefinitionOfPopulation.tsx | 49 + .../2024/CPCCH/questions/HowDidYouReport.tsx | 38 + .../CPCCH/questions/PerformanceMeasure.tsx | 22 + .../2024/CPCCH/questions/Reporting.tsx | 41 + .../CPCCH/questions/WhyDidYouNotCollect.tsx | 184 + .../measures/2024/CPCCH/questions/index.tsx | 6 + .../ui-src/src/measures/2024/CPCCH/types.ts | 39 + .../src/measures/2024/CPCCH/validation.ts | 25 + .../ui-src/src/measures/2024/CPUAD/data.ts | 45 + .../src/measures/2024/CPUAD/index.test.tsx | 253 ++ .../ui-src/src/measures/2024/CPUAD/index.tsx | 54 + .../ui-src/src/measures/2024/CPUAD/types.ts | 3 + .../src/measures/2024/CPUAD/validation.ts | 60 + .../ui-src/src/measures/2024/DEVCH/data.ts | 69 + .../src/measures/2024/DEVCH/index.test.tsx | 268 ++ .../ui-src/src/measures/2024/DEVCH/index.tsx | 68 + .../ui-src/src/measures/2024/DEVCH/types.ts | 3 + .../src/measures/2024/DEVCH/validation.ts | 75 + .../ui-src/src/measures/2024/FUAAD/data.ts | 16 + .../src/measures/2024/FUAAD/index.test.tsx | 268 ++ .../ui-src/src/measures/2024/FUAAD/index.tsx | 67 + .../ui-src/src/measures/2024/FUAAD/types.ts | 3 + .../src/measures/2024/FUAAD/validation.ts | 87 + .../ui-src/src/measures/2024/FUACH/data.ts | 16 + .../src/measures/2024/FUACH/index.test.tsx | 268 ++ .../ui-src/src/measures/2024/FUACH/index.tsx | 67 + .../ui-src/src/measures/2024/FUACH/types.ts | 3 + .../src/measures/2024/FUACH/validation.ts | 79 + .../ui-src/src/measures/2024/FUAHH/data.ts | 16 + .../src/measures/2024/FUAHH/index.test.tsx | 258 ++ .../ui-src/src/measures/2024/FUAHH/index.tsx | 69 + .../ui-src/src/measures/2024/FUAHH/types.ts | 3 + .../src/measures/2024/FUAHH/validation.ts | 97 + .../ui-src/src/measures/2024/FUHAD/data.ts | 16 + .../src/measures/2024/FUHAD/index.test.tsx | 259 ++ .../ui-src/src/measures/2024/FUHAD/index.tsx | 67 + .../ui-src/src/measures/2024/FUHAD/types.ts | 3 + .../src/measures/2024/FUHAD/validation.ts | 102 + .../ui-src/src/measures/2024/FUHCH/data.ts | 16 + .../src/measures/2024/FUHCH/index.test.tsx | 260 ++ .../ui-src/src/measures/2024/FUHCH/index.tsx | 67 + .../ui-src/src/measures/2024/FUHCH/types.ts | 3 + .../src/measures/2024/FUHCH/validation.ts | 95 + .../ui-src/src/measures/2024/FUHHH/data.ts | 16 + .../src/measures/2024/FUHHH/index.test.tsx | 258 ++ .../ui-src/src/measures/2024/FUHHH/index.tsx | 69 + .../ui-src/src/measures/2024/FUHHH/types.ts | 3 + .../src/measures/2024/FUHHH/validation.ts | 96 + .../ui-src/src/measures/2024/FUMAD/data.ts | 16 + .../src/measures/2024/FUMAD/index.test.tsx | 259 ++ .../ui-src/src/measures/2024/FUMAD/index.tsx | 66 + .../ui-src/src/measures/2024/FUMAD/types.ts | 3 + .../src/measures/2024/FUMAD/validation.ts | 93 + .../ui-src/src/measures/2024/FUMCH/data.ts | 16 + .../src/measures/2024/FUMCH/index.test.tsx | 260 ++ .../ui-src/src/measures/2024/FUMCH/index.tsx | 67 + .../ui-src/src/measures/2024/FUMCH/types.ts | 3 + .../src/measures/2024/FUMCH/validation.ts | 94 + .../ui-src/src/measures/2024/FUMHH/data.ts | 16 + .../src/measures/2024/FUMHH/index.test.tsx | 258 ++ .../ui-src/src/measures/2024/FUMHH/index.tsx | 72 + .../ui-src/src/measures/2024/FUMHH/types.ts | 3 + .../src/measures/2024/FUMHH/validation.ts | 99 + .../ui-src/src/measures/2024/FVAAD/data.ts | 13 + .../src/measures/2024/FVAAD/index.test.tsx | 246 ++ .../ui-src/src/measures/2024/FVAAD/index.tsx | 53 + .../ui-src/src/measures/2024/FVAAD/types.ts | 3 + .../src/measures/2024/FVAAD/validation.ts | 75 + .../ui-src/src/measures/2024/HBDAD/data.ts | 74 + .../src/measures/2024/HBDAD/index.test.tsx | 243 ++ .../ui-src/src/measures/2024/HBDAD/index.tsx | 67 + .../ui-src/src/measures/2024/HBDAD/types.ts | 3 + .../src/measures/2024/HBDAD/validation.ts | 83 + .../ui-src/src/measures/2024/HPCMIAD/data.ts | 71 + .../src/measures/2024/HPCMIAD/index.test.tsx | 245 ++ .../src/measures/2024/HPCMIAD/index.tsx | 67 + .../ui-src/src/measures/2024/HPCMIAD/types.ts | 3 + .../src/measures/2024/HPCMIAD/validation.ts | 83 + .../ui-src/src/measures/2024/HVLAD/data.ts | 46 + .../src/measures/2024/HVLAD/index.test.tsx | 259 ++ .../ui-src/src/measures/2024/HVLAD/index.tsx | 67 + .../ui-src/src/measures/2024/HVLAD/types.ts | 3 + .../src/measures/2024/HVLAD/validation.ts | 80 + .../ui-src/src/measures/2024/IETAD/data.ts | 49 + .../src/measures/2024/IETAD/index.test.tsx | 307 ++ .../ui-src/src/measures/2024/IETAD/index.tsx | 71 + .../ui-src/src/measures/2024/IETAD/types.ts | 3 + .../src/measures/2024/IETAD/validation.ts | 115 + .../ui-src/src/measures/2024/IETHH/data.ts | 49 + .../src/measures/2024/IETHH/index.test.tsx | 258 ++ .../ui-src/src/measures/2024/IETHH/index.tsx | 73 + .../ui-src/src/measures/2024/IETHH/types.ts | 3 + .../src/measures/2024/IETHH/validation.ts | 116 + .../ui-src/src/measures/2024/IMACH/data.ts | 80 + .../src/measures/2024/IMACH/index.test.tsx | 267 ++ .../ui-src/src/measures/2024/IMACH/index.tsx | 67 + .../ui-src/src/measures/2024/IMACH/types.ts | 3 + .../src/measures/2024/IMACH/validation.ts | 76 + .../ui-src/src/measures/2024/IUHH/data.ts | 70 + .../src/measures/2024/IUHH/index.test.tsx | 783 ++++ .../ui-src/src/measures/2024/IUHH/index.tsx | 85 + .../ui-src/src/measures/2024/IUHH/types.ts | 3 + .../src/measures/2024/IUHH/validation.ts | 130 + .../src/measures/2024/LBWCH/index.test.tsx | 90 + .../ui-src/src/measures/2024/LBWCH/index.tsx | 17 + .../src/measures/2024/LRCDCH/index.test.tsx | 90 + .../ui-src/src/measures/2024/LRCDCH/index.tsx | 20 + .../ui-src/src/measures/2024/LSCCH/data.ts | 70 + .../src/measures/2024/LSCCH/index.test.tsx | 271 ++ .../ui-src/src/measures/2024/LSCCH/index.tsx | 67 + .../ui-src/src/measures/2024/LSCCH/types.ts | 3 + .../src/measures/2024/LSCCH/validation.ts | 77 + .../ui-src/src/measures/2024/MSCAD/data.ts | 22 + .../src/measures/2024/MSCAD/index.test.tsx | 269 ++ .../ui-src/src/measures/2024/MSCAD/index.tsx | 57 + .../2024/MSCAD/questions/DataSource.tsx | 40 + .../measures/2024/MSCAD/questions/index.tsx | 1 + .../ui-src/src/measures/2024/MSCAD/types.ts | 19 + .../src/measures/2024/MSCAD/validation.ts | 80 + .../src/measures/2024/NCIDDSAD/index.test.tsx | 90 + .../src/measures/2024/NCIDDSAD/index.tsx | 24 + .../ui-src/src/measures/2024/OEVCH/data.ts | 12 + .../src/measures/2024/OEVCH/index.test.tsx | 286 ++ .../ui-src/src/measures/2024/OEVCH/index.tsx | 68 + .../ui-src/src/measures/2024/OEVCH/types.ts | 3 + .../src/measures/2024/OEVCH/validation.ts | 76 + .../ui-src/src/measures/2024/OHDAD/data.ts | 13 + .../src/measures/2024/OHDAD/index.test.tsx | 258 ++ .../ui-src/src/measures/2024/OHDAD/index.tsx | 67 + .../ui-src/src/measures/2024/OHDAD/types.ts | 3 + .../src/measures/2024/OHDAD/validation.ts | 78 + .../ui-src/src/measures/2024/OUDAD/data.ts | 20 + .../src/measures/2024/OUDAD/index.test.tsx | 264 ++ .../ui-src/src/measures/2024/OUDAD/index.tsx | 66 + .../ui-src/src/measures/2024/OUDAD/types.ts | 3 + .../src/measures/2024/OUDAD/validation.ts | 75 + .../ui-src/src/measures/2024/OUDHH/data.ts | 20 + .../src/measures/2024/OUDHH/index.test.tsx | 258 ++ .../ui-src/src/measures/2024/OUDHH/index.tsx | 66 + .../ui-src/src/measures/2024/OUDHH/types.ts | 3 + .../src/measures/2024/OUDHH/validation.ts | 74 + .../ui-src/src/measures/2024/PCRAD/data.ts | 17 + .../src/measures/2024/PCRAD/index.test.tsx | 284 ++ .../ui-src/src/measures/2024/PCRAD/index.tsx | 54 + .../PCRAD/questions/PerformanceMeasure.tsx | 196 + .../ui-src/src/measures/2024/PCRAD/types.ts | 3 + .../src/measures/2024/PCRAD/validation.ts | 109 + .../ui-src/src/measures/2024/PCRHH/data.ts | 17 + .../src/measures/2024/PCRHH/index.test.tsx | 276 ++ .../ui-src/src/measures/2024/PCRHH/index.tsx | 55 + .../PCRHH/questions/PerformanceMeasure.tsx | 196 + .../ui-src/src/measures/2024/PCRHH/types.ts | 3 + .../src/measures/2024/PCRHH/validation.ts | 109 + .../ui-src/src/measures/2024/PPCAD/data.ts | 71 + .../src/measures/2024/PPCAD/index.test.tsx | 258 ++ .../ui-src/src/measures/2024/PPCAD/index.tsx | 68 + .../ui-src/src/measures/2024/PPCAD/types.ts | 3 + .../src/measures/2024/PPCAD/validation.ts | 74 + .../ui-src/src/measures/2024/PPCCH/data.ts | 76 + .../src/measures/2024/PPCCH/index.test.tsx | 269 ++ .../ui-src/src/measures/2024/PPCCH/index.tsx | 68 + .../ui-src/src/measures/2024/PPCCH/types.ts | 3 + .../src/measures/2024/PPCCH/validation.ts | 74 + .../ui-src/src/measures/2024/PQI01AD/data.ts | 12 + .../src/measures/2024/PQI01AD/index.test.tsx | 258 ++ .../src/measures/2024/PQI01AD/index.tsx | 82 + .../ui-src/src/measures/2024/PQI01AD/types.ts | 3 + .../src/measures/2024/PQI01AD/validation.ts | 84 + .../ui-src/src/measures/2024/PQI05AD/data.ts | 12 + .../src/measures/2024/PQI05AD/index.test.tsx | 258 ++ .../src/measures/2024/PQI05AD/index.tsx | 82 + .../ui-src/src/measures/2024/PQI05AD/types.ts | 3 + .../src/measures/2024/PQI05AD/validation.ts | 89 + .../ui-src/src/measures/2024/PQI08AD/data.ts | 12 + .../src/measures/2024/PQI08AD/index.test.tsx | 258 ++ .../src/measures/2024/PQI08AD/index.tsx | 82 + .../ui-src/src/measures/2024/PQI08AD/types.ts | 3 + .../src/measures/2024/PQI08AD/validation.ts | 80 + .../ui-src/src/measures/2024/PQI15AD/data.ts | 12 + .../src/measures/2024/PQI15AD/index.test.tsx | 258 ++ .../src/measures/2024/PQI15AD/index.tsx | 81 + .../ui-src/src/measures/2024/PQI15AD/types.ts | 3 + .../src/measures/2024/PQI15AD/validation.ts | 64 + .../ui-src/src/measures/2024/PQI92HH/data.ts | 12 + .../src/measures/2024/PQI92HH/index.test.tsx | 258 ++ .../src/measures/2024/PQI92HH/index.tsx | 84 + .../ui-src/src/measures/2024/PQI92HH/types.ts | 3 + .../src/measures/2024/PQI92HH/validation.ts | 83 + .../ui-src/src/measures/2024/SAAAD/data.ts | 13 + .../src/measures/2024/SAAAD/index.test.tsx | 258 ++ .../ui-src/src/measures/2024/SAAAD/index.tsx | 67 + .../ui-src/src/measures/2024/SAAAD/types.ts | 3 + .../src/measures/2024/SAAAD/validation.ts | 75 + .../ui-src/src/measures/2024/SFMCH/data.ts | 13 + .../src/measures/2024/SFMCH/index.test.tsx | 268 ++ .../ui-src/src/measures/2024/SFMCH/index.tsx | 67 + .../ui-src/src/measures/2024/SFMCH/types.ts | 3 + .../src/measures/2024/SFMCH/validation.ts | 96 + .../ui-src/src/measures/2024/SSDAD/data.ts | 13 + .../src/measures/2024/SSDAD/index.test.tsx | 245 ++ .../ui-src/src/measures/2024/SSDAD/index.tsx | 67 + .../ui-src/src/measures/2024/SSDAD/types.ts | 3 + .../src/measures/2024/SSDAD/validation.ts | 74 + .../ui-src/src/measures/2024/SSHH/data.ts | 62 + .../src/measures/2024/SSHH/index.test.tsx | 168 + .../ui-src/src/measures/2024/SSHH/index.tsx | 37 + .../SSHH/questions/PerformanceMeasure.tsx | 153 + .../ui-src/src/measures/2024/SSHH/types.ts | 3 + .../src/measures/2024/SSHH/validation.ts | 136 + .../ui-src/src/measures/2024/TFLCH/data.ts | 13 + .../src/measures/2024/TFLCH/index.test.tsx | 349 ++ .../ui-src/src/measures/2024/TFLCH/index.tsx | 68 + .../ui-src/src/measures/2024/TFLCH/types.ts | 3 + .../src/measures/2024/TFLCH/validation.ts | 92 + .../ui-src/src/measures/2024/W30CH/data.ts | 20 + .../src/measures/2024/W30CH/index.test.tsx | 262 ++ .../ui-src/src/measures/2024/W30CH/index.tsx | 67 + .../ui-src/src/measures/2024/W30CH/types.ts | 3 + .../src/measures/2024/W30CH/validation.ts | 93 + .../ui-src/src/measures/2024/WCCCH/data.ts | 74 + .../src/measures/2024/WCCCH/index.test.tsx | 295 ++ .../ui-src/src/measures/2024/WCCCH/index.tsx | 68 + .../ui-src/src/measures/2024/WCCCH/types.ts | 3 + .../src/measures/2024/WCCCH/validation.ts | 108 + .../ui-src/src/measures/2024/WCVCH/data.ts | 13 + .../src/measures/2024/WCVCH/index.test.tsx | 270 ++ .../ui-src/src/measures/2024/WCVCH/index.tsx | 68 + .../ui-src/src/measures/2024/WCVCH/types.ts | 3 + .../src/measures/2024/WCVCH/validation.ts | 75 + services/ui-src/src/measures/2024/index.tsx | 318 ++ .../src/measures/2024/rateLabelText.test.ts | 40 + .../ui-src/src/measures/2024/rateLabelText.ts | 1820 +++++++++ .../AdditionalNotes/index.test.tsx | 59 + .../CommonQuestions/AdditionalNotes/index.tsx | 46 + .../CombinedRates/index.test.tsx | 178 + .../CommonQuestions/CombinedRates/index.tsx | 96 + .../__snapshots__/index.test.tsx.snap | 258 ++ .../shared/CommonQuestions/DataSource/data.ts | 43 + .../CommonQuestions/DataSource/index.test.tsx | 47 + .../CommonQuestions/DataSource/index.tsx | 148 + .../CommonQuestions/DataSourceCahps/data.ts | 26 + .../CommonQuestions/DataSourceCahps/index.tsx | 55 + .../CommonQuestions/DateRange/index.test.tsx | 104 + .../CommonQuestions/DateRange/index.tsx | 73 + .../__snapshots__/index.test.tsx.snap | 3452 +++++++++++++++++ .../DefinitionsOfPopulation/index.test.tsx | 37 + .../DefinitionsOfPopulation/index.tsx | 519 +++ .../index.tsx | 51 + .../MeasurementSpecification/index.test.tsx | 122 + .../MeasurementSpecification/index.tsx | 143 + .../NotCollectingOMS/index.test.tsx | 17 + .../NotCollectingOMS/index.tsx | 16 + .../additionalCategory.tsx | 67 + .../OptionalMeasureStrat/context.ts | 56 + .../OptionalMeasureStrat/data.ts | 139 + .../OptionalMeasureStrat/index.tsx | 243 ++ .../OptionalMeasureStrat/ndrSets.tsx | 629 +++ .../OptionalMeasureStrat/omsNodeBuilder.tsx | 148 + .../OptionalMeasureStrat/omsUtil.tsx | 382 ++ .../subCatClassification.tsx | 93 + .../OtherPerformanceMeasure/index.test.tsx | 76 + .../OtherPerformanceMeasure/index.tsx | 153 + .../PerformanceMeasure/data.ts | 81 + .../PerformanceMeasure/index.test.tsx | 206 + .../PerformanceMeasure/index.tsx | 326 ++ .../CommonQuestions/Reporting/index.test.tsx | 86 + .../CommonQuestions/Reporting/index.tsx | 55 + .../StatusOfData/index.test.tsx | 42 + .../CommonQuestions/StatusOfData/index.tsx | 40 + .../WhyAreYouNotReporting/index.test.tsx | 236 ++ .../WhyAreYouNotReporting/index.tsx | 220 ++ .../2024/shared/CommonQuestions/index.ts | 15 + .../2024/shared/CommonQuestions/index.tsx | 15 + .../2024/shared/CommonQuestions/types.ts | 317 ++ .../Common/administrativeQuestions.test.tsx | 84 + .../Common/administrativeQuestions.tsx | 134 + .../2024/shared/Qualifiers/Common/audit.tsx | 168 + .../Qualifiers/Common/costSavingsData.tsx | 39 + .../Qualifiers/Common/externalContractor.tsx | 70 + .../2024/shared/Qualifiers/Common/index.tsx | 5 + .../Qualifiers/Common/qualifierHeader.tsx | 18 + .../measures/2024/shared/Qualifiers/data.ts | 191 + .../shared/Qualifiers/deliverySystems.tsx | 186 + .../measures/2024/shared/Qualifiers/index.tsx | 69 + .../measures/2024/shared/Qualifiers/types.tsx | 44 + .../shared/Qualifiers/validationFunctions.ts | 9 + .../shared/Qualifiers/validations/adult.ts | 48 + .../Qualifiers/validations/childCHIP.ts | 33 + .../Qualifiers/validations/childCombined.ts | 51 + .../Qualifiers/validations/childMedicaid.ts | 33 + .../Qualifiers/validations/healthHome.ts | 79 + .../shared/Qualifiers/validations/index.ts | 5 + .../ComplexAtLeastOneRateComplete/index.tsx | 66 + .../ComplexNoNonZeroNumOrDenom/index.tsx | 105 + .../index.tsx | 60 + .../ComplexValidateNDRTotals/index.tsx | 140 + .../ComplexValueSameCrossCategory/index.tsx | 110 + .../PCRatLeastOneRateComplete/index.tsx | 52 + .../PCRnoNonZeroNumOrDenom/index.tsx | 71 + .../globalValidations/dataDrivenTools.test.ts | 82 + .../globalValidations/dataDrivenTools.ts | 135 + .../2024/shared/globalValidations/index.ts | 49 + .../omsValidator/index.test.ts | 129 + .../globalValidations/omsValidator/index.ts | 353 ++ .../globalValidations/testHelpers/_helper.ts | 39 + .../testHelpers/_testFormData.ts | 346 ++ .../2024/shared/globalValidations/types.ts | 41 + .../index.test.ts | 51 + .../validateAtLeastOneDataSource/index.ts | 20 + .../index.test.ts | 63 + .../validateAtLeastOneDataSourceType/index.ts | 26 + .../index.test.ts | 47 + .../index.ts | 21 + .../index.test.ts | 47 + .../validateAtLeastOneDeliverySystem/index.ts | 20 + .../index.test.ts | 61 + .../index.ts | 23 + .../index.test.ts | 94 + .../validateAtLeastOneRateComplete/index.ts | 53 + .../validateBothDatesInRange/index.test.ts | 121 + .../validateBothDatesInRange/index.ts | 32 + .../index.test.ts | 42 + .../index.ts | 16 + .../validateDualPopInformation/index.test.ts | 137 + .../validateDualPopInformation/index.ts | 62 + .../index.test.ts | 184 + .../index.ts | 80 + .../index.test.ts | 182 + .../index.ts | 85 + .../index.test.ts | 49 + .../validateFfsRadioButtonCompletion/index.ts | 37 + .../validateHedisYear/index.test.ts | 41 + .../validateHedisYear/index.ts | 27 + .../validateNoNonZeroNumOrDenom/index.ts | 137 + .../index.test.ts | 175 + .../index.ts | 94 + .../validateOPMRates/index.test.ts | 30 + .../validateOPMRates/index.ts | 34 + .../index.test.ts | 231 ++ .../index.ts | 191 + .../index.test.ts | 194 + .../index.ts | 119 + .../index.test.ts | 215 + .../index.ts | 129 + .../index.test.ts | 262 ++ .../validatePartialRateCompletion/index.ts | 194 + .../validateRateNotZero/index.test.ts | 167 + .../validateRateNotZero/index.ts | 79 + .../validateRateZero/index.test.ts | 198 + .../validateRateZero/index.ts | 105 + .../index.test.ts | 44 + .../validateReasonForNotReporting/index.ts | 27 + .../index.test.ts | 47 + .../index.ts | 22 + .../validateSameDenominatorSets/index.test.ts | 56 + .../validateSameDenominatorSets/index.ts | 44 + .../validateTotals/index.test.tsx | 357 ++ .../globalValidations/validateTotals/index.ts | 179 + .../validateYearFormat/index.test.ts | 87 + .../validateYearFormat/index.ts | 30 + .../2024/shared/util/validationsMock.tsx | 178 + services/ui-src/src/measures/index.tsx | 3 + .../src/measures/measureDescriptions.ts | 103 +- .../testUtils/2024/validationHelper.test.ts | 29 + .../utils/testUtils/2024/validationHelpers.ts | 313 ++ services/ui-src/src/views/AdminHome/index.tsx | 5 +- services/ui-src/src/views/Home/index.tsx | 5 +- services/ui-src/src/views/StateHome/index.tsx | 14 +- 529 files changed, 55393 insertions(+), 12 deletions(-) create mode 100644 services/ui-src/src/measures/2024/AABAD/data.ts create mode 100644 services/ui-src/src/measures/2024/AABAD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/AABAD/index.tsx create mode 100644 services/ui-src/src/measures/2024/AABAD/types.ts create mode 100644 services/ui-src/src/measures/2024/AABAD/validation.ts create mode 100644 services/ui-src/src/measures/2024/AABCH/data.ts create mode 100644 services/ui-src/src/measures/2024/AABCH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/AABCH/index.tsx create mode 100644 services/ui-src/src/measures/2024/AABCH/types.ts create mode 100644 services/ui-src/src/measures/2024/AABCH/validation.ts create mode 100644 services/ui-src/src/measures/2024/ADDCH/data.ts create mode 100644 services/ui-src/src/measures/2024/ADDCH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/ADDCH/index.tsx create mode 100644 services/ui-src/src/measures/2024/ADDCH/types.ts create mode 100644 services/ui-src/src/measures/2024/ADDCH/validation.ts create mode 100644 services/ui-src/src/measures/2024/AIFHH/data.ts create mode 100644 services/ui-src/src/measures/2024/AIFHH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/AIFHH/index.tsx create mode 100644 services/ui-src/src/measures/2024/AIFHH/types.ts create mode 100644 services/ui-src/src/measures/2024/AIFHH/validation.ts create mode 100644 services/ui-src/src/measures/2024/AMBCH/data.ts create mode 100644 services/ui-src/src/measures/2024/AMBCH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/AMBCH/index.tsx create mode 100644 services/ui-src/src/measures/2024/AMBCH/types.ts create mode 100644 services/ui-src/src/measures/2024/AMBCH/validation.ts create mode 100644 services/ui-src/src/measures/2024/AMBHH/data.ts create mode 100644 services/ui-src/src/measures/2024/AMBHH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/AMBHH/index.tsx create mode 100644 services/ui-src/src/measures/2024/AMBHH/types.ts create mode 100644 services/ui-src/src/measures/2024/AMBHH/validation.ts create mode 100644 services/ui-src/src/measures/2024/AMMAD/data.ts create mode 100644 services/ui-src/src/measures/2024/AMMAD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/AMMAD/index.tsx create mode 100644 services/ui-src/src/measures/2024/AMMAD/types.ts create mode 100644 services/ui-src/src/measures/2024/AMMAD/validation.ts create mode 100644 services/ui-src/src/measures/2024/AMRAD/data.ts create mode 100644 services/ui-src/src/measures/2024/AMRAD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/AMRAD/index.tsx create mode 100644 services/ui-src/src/measures/2024/AMRAD/types.ts create mode 100644 services/ui-src/src/measures/2024/AMRAD/validation.ts create mode 100644 services/ui-src/src/measures/2024/AMRCH/data.ts create mode 100644 services/ui-src/src/measures/2024/AMRCH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/AMRCH/index.tsx create mode 100644 services/ui-src/src/measures/2024/AMRCH/types.ts create mode 100644 services/ui-src/src/measures/2024/AMRCH/validation.ts create mode 100644 services/ui-src/src/measures/2024/APMCH/data.ts create mode 100644 services/ui-src/src/measures/2024/APMCH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/APMCH/index.tsx create mode 100644 services/ui-src/src/measures/2024/APMCH/types.ts create mode 100644 services/ui-src/src/measures/2024/APMCH/validation.ts create mode 100644 services/ui-src/src/measures/2024/APPCH/data.ts create mode 100644 services/ui-src/src/measures/2024/APPCH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/APPCH/index.tsx create mode 100644 services/ui-src/src/measures/2024/APPCH/types.ts create mode 100644 services/ui-src/src/measures/2024/APPCH/validation.ts create mode 100644 services/ui-src/src/measures/2024/BCSAD/data.ts create mode 100644 services/ui-src/src/measures/2024/BCSAD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/BCSAD/index.tsx create mode 100644 services/ui-src/src/measures/2024/BCSAD/types.ts create mode 100644 services/ui-src/src/measures/2024/BCSAD/validation.ts create mode 100644 services/ui-src/src/measures/2024/CBPAD/data.ts create mode 100644 services/ui-src/src/measures/2024/CBPAD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/CBPAD/index.tsx create mode 100644 services/ui-src/src/measures/2024/CBPAD/types.ts create mode 100644 services/ui-src/src/measures/2024/CBPAD/validation.ts create mode 100644 services/ui-src/src/measures/2024/CBPHH/data.ts create mode 100644 services/ui-src/src/measures/2024/CBPHH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/CBPHH/index.tsx create mode 100644 services/ui-src/src/measures/2024/CBPHH/types.ts create mode 100644 services/ui-src/src/measures/2024/CBPHH/validation.ts create mode 100644 services/ui-src/src/measures/2024/CCPAD/data.ts create mode 100644 services/ui-src/src/measures/2024/CCPAD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/CCPAD/index.tsx create mode 100644 services/ui-src/src/measures/2024/CCPAD/types.ts create mode 100644 services/ui-src/src/measures/2024/CCPAD/validation.ts create mode 100644 services/ui-src/src/measures/2024/CCPCH/data.ts create mode 100644 services/ui-src/src/measures/2024/CCPCH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/CCPCH/index.tsx create mode 100644 services/ui-src/src/measures/2024/CCPCH/types.ts create mode 100644 services/ui-src/src/measures/2024/CCPCH/validation.ts create mode 100644 services/ui-src/src/measures/2024/CCSAD/data.ts create mode 100644 services/ui-src/src/measures/2024/CCSAD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/CCSAD/index.tsx create mode 100644 services/ui-src/src/measures/2024/CCSAD/types.ts create mode 100644 services/ui-src/src/measures/2024/CCSAD/validation.ts create mode 100644 services/ui-src/src/measures/2024/CCWAD/data.ts create mode 100644 services/ui-src/src/measures/2024/CCWAD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/CCWAD/index.tsx create mode 100644 services/ui-src/src/measures/2024/CCWAD/types.ts create mode 100644 services/ui-src/src/measures/2024/CCWAD/validation.ts create mode 100644 services/ui-src/src/measures/2024/CCWCH/data.ts create mode 100644 services/ui-src/src/measures/2024/CCWCH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/CCWCH/index.tsx create mode 100644 services/ui-src/src/measures/2024/CCWCH/types.ts create mode 100644 services/ui-src/src/measures/2024/CCWCH/validation.ts create mode 100644 services/ui-src/src/measures/2024/CDFAD/data.ts create mode 100644 services/ui-src/src/measures/2024/CDFAD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/CDFAD/index.tsx create mode 100644 services/ui-src/src/measures/2024/CDFAD/types.ts create mode 100644 services/ui-src/src/measures/2024/CDFAD/validation.ts create mode 100644 services/ui-src/src/measures/2024/CDFCH/data.ts create mode 100644 services/ui-src/src/measures/2024/CDFCH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/CDFCH/index.tsx create mode 100644 services/ui-src/src/measures/2024/CDFCH/types.ts create mode 100644 services/ui-src/src/measures/2024/CDFCH/validation.ts create mode 100644 services/ui-src/src/measures/2024/CDFHH/data.ts create mode 100644 services/ui-src/src/measures/2024/CDFHH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/CDFHH/index.tsx create mode 100644 services/ui-src/src/measures/2024/CDFHH/types.ts create mode 100644 services/ui-src/src/measures/2024/CDFHH/validation.ts create mode 100644 services/ui-src/src/measures/2024/CHLAD/data.ts create mode 100644 services/ui-src/src/measures/2024/CHLAD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/CHLAD/index.tsx create mode 100644 services/ui-src/src/measures/2024/CHLAD/types.ts create mode 100644 services/ui-src/src/measures/2024/CHLAD/validation.ts create mode 100644 services/ui-src/src/measures/2024/CHLCH/data.ts create mode 100644 services/ui-src/src/measures/2024/CHLCH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/CHLCH/index.tsx create mode 100644 services/ui-src/src/measures/2024/CHLCH/types.ts create mode 100644 services/ui-src/src/measures/2024/CHLCH/validation.ts create mode 100644 services/ui-src/src/measures/2024/CISCH/data.ts create mode 100644 services/ui-src/src/measures/2024/CISCH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/CISCH/index.tsx create mode 100644 services/ui-src/src/measures/2024/CISCH/types.ts create mode 100644 services/ui-src/src/measures/2024/CISCH/validation.ts create mode 100644 services/ui-src/src/measures/2024/COBAD/data.ts create mode 100644 services/ui-src/src/measures/2024/COBAD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/COBAD/index.tsx create mode 100644 services/ui-src/src/measures/2024/COBAD/types.ts create mode 100644 services/ui-src/src/measures/2024/COBAD/validation.ts create mode 100644 services/ui-src/src/measures/2024/COLAD/data.ts create mode 100644 services/ui-src/src/measures/2024/COLAD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/COLAD/index.tsx create mode 100644 services/ui-src/src/measures/2024/COLAD/types.ts create mode 100644 services/ui-src/src/measures/2024/COLAD/validation.ts create mode 100644 services/ui-src/src/measures/2024/COLHH/data.ts create mode 100644 services/ui-src/src/measures/2024/COLHH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/COLHH/index.tsx create mode 100644 services/ui-src/src/measures/2024/COLHH/types.ts create mode 100644 services/ui-src/src/measures/2024/COLHH/validation.ts create mode 100644 services/ui-src/src/measures/2024/CPAAD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/CPAAD/index.tsx create mode 100644 services/ui-src/src/measures/2024/CPAAD/questions/DataSource.tsx create mode 100644 services/ui-src/src/measures/2024/CPAAD/questions/DefinitionOfPopulation.tsx create mode 100644 services/ui-src/src/measures/2024/CPAAD/questions/HowDidYouReport.tsx create mode 100644 services/ui-src/src/measures/2024/CPAAD/questions/PerformanceMeasure.tsx create mode 100644 services/ui-src/src/measures/2024/CPAAD/questions/Reporting.tsx create mode 100644 services/ui-src/src/measures/2024/CPAAD/questions/WhyDidYouNotCollect.tsx create mode 100644 services/ui-src/src/measures/2024/CPAAD/questions/index.tsx create mode 100644 services/ui-src/src/measures/2024/CPAAD/types.ts create mode 100644 services/ui-src/src/measures/2024/CPAAD/validation.ts create mode 100644 services/ui-src/src/measures/2024/CPCCH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/CPCCH/index.tsx create mode 100644 services/ui-src/src/measures/2024/CPCCH/questions/DataSource.tsx create mode 100644 services/ui-src/src/measures/2024/CPCCH/questions/DefinitionOfPopulation.tsx create mode 100644 services/ui-src/src/measures/2024/CPCCH/questions/HowDidYouReport.tsx create mode 100644 services/ui-src/src/measures/2024/CPCCH/questions/PerformanceMeasure.tsx create mode 100644 services/ui-src/src/measures/2024/CPCCH/questions/Reporting.tsx create mode 100644 services/ui-src/src/measures/2024/CPCCH/questions/WhyDidYouNotCollect.tsx create mode 100644 services/ui-src/src/measures/2024/CPCCH/questions/index.tsx create mode 100644 services/ui-src/src/measures/2024/CPCCH/types.ts create mode 100644 services/ui-src/src/measures/2024/CPCCH/validation.ts create mode 100644 services/ui-src/src/measures/2024/CPUAD/data.ts create mode 100644 services/ui-src/src/measures/2024/CPUAD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/CPUAD/index.tsx create mode 100644 services/ui-src/src/measures/2024/CPUAD/types.ts create mode 100644 services/ui-src/src/measures/2024/CPUAD/validation.ts create mode 100644 services/ui-src/src/measures/2024/DEVCH/data.ts create mode 100644 services/ui-src/src/measures/2024/DEVCH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/DEVCH/index.tsx create mode 100644 services/ui-src/src/measures/2024/DEVCH/types.ts create mode 100644 services/ui-src/src/measures/2024/DEVCH/validation.ts create mode 100644 services/ui-src/src/measures/2024/FUAAD/data.ts create mode 100644 services/ui-src/src/measures/2024/FUAAD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/FUAAD/index.tsx create mode 100644 services/ui-src/src/measures/2024/FUAAD/types.ts create mode 100644 services/ui-src/src/measures/2024/FUAAD/validation.ts create mode 100644 services/ui-src/src/measures/2024/FUACH/data.ts create mode 100644 services/ui-src/src/measures/2024/FUACH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/FUACH/index.tsx create mode 100644 services/ui-src/src/measures/2024/FUACH/types.ts create mode 100644 services/ui-src/src/measures/2024/FUACH/validation.ts create mode 100644 services/ui-src/src/measures/2024/FUAHH/data.ts create mode 100644 services/ui-src/src/measures/2024/FUAHH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/FUAHH/index.tsx create mode 100644 services/ui-src/src/measures/2024/FUAHH/types.ts create mode 100644 services/ui-src/src/measures/2024/FUAHH/validation.ts create mode 100644 services/ui-src/src/measures/2024/FUHAD/data.ts create mode 100644 services/ui-src/src/measures/2024/FUHAD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/FUHAD/index.tsx create mode 100644 services/ui-src/src/measures/2024/FUHAD/types.ts create mode 100644 services/ui-src/src/measures/2024/FUHAD/validation.ts create mode 100644 services/ui-src/src/measures/2024/FUHCH/data.ts create mode 100644 services/ui-src/src/measures/2024/FUHCH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/FUHCH/index.tsx create mode 100644 services/ui-src/src/measures/2024/FUHCH/types.ts create mode 100644 services/ui-src/src/measures/2024/FUHCH/validation.ts create mode 100644 services/ui-src/src/measures/2024/FUHHH/data.ts create mode 100644 services/ui-src/src/measures/2024/FUHHH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/FUHHH/index.tsx create mode 100644 services/ui-src/src/measures/2024/FUHHH/types.ts create mode 100644 services/ui-src/src/measures/2024/FUHHH/validation.ts create mode 100644 services/ui-src/src/measures/2024/FUMAD/data.ts create mode 100644 services/ui-src/src/measures/2024/FUMAD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/FUMAD/index.tsx create mode 100644 services/ui-src/src/measures/2024/FUMAD/types.ts create mode 100644 services/ui-src/src/measures/2024/FUMAD/validation.ts create mode 100644 services/ui-src/src/measures/2024/FUMCH/data.ts create mode 100644 services/ui-src/src/measures/2024/FUMCH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/FUMCH/index.tsx create mode 100644 services/ui-src/src/measures/2024/FUMCH/types.ts create mode 100644 services/ui-src/src/measures/2024/FUMCH/validation.ts create mode 100644 services/ui-src/src/measures/2024/FUMHH/data.ts create mode 100644 services/ui-src/src/measures/2024/FUMHH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/FUMHH/index.tsx create mode 100644 services/ui-src/src/measures/2024/FUMHH/types.ts create mode 100644 services/ui-src/src/measures/2024/FUMHH/validation.ts create mode 100644 services/ui-src/src/measures/2024/FVAAD/data.ts create mode 100644 services/ui-src/src/measures/2024/FVAAD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/FVAAD/index.tsx create mode 100644 services/ui-src/src/measures/2024/FVAAD/types.ts create mode 100644 services/ui-src/src/measures/2024/FVAAD/validation.ts create mode 100644 services/ui-src/src/measures/2024/HBDAD/data.ts create mode 100644 services/ui-src/src/measures/2024/HBDAD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/HBDAD/index.tsx create mode 100644 services/ui-src/src/measures/2024/HBDAD/types.ts create mode 100644 services/ui-src/src/measures/2024/HBDAD/validation.ts create mode 100644 services/ui-src/src/measures/2024/HPCMIAD/data.ts create mode 100644 services/ui-src/src/measures/2024/HPCMIAD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/HPCMIAD/index.tsx create mode 100644 services/ui-src/src/measures/2024/HPCMIAD/types.ts create mode 100644 services/ui-src/src/measures/2024/HPCMIAD/validation.ts create mode 100644 services/ui-src/src/measures/2024/HVLAD/data.ts create mode 100644 services/ui-src/src/measures/2024/HVLAD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/HVLAD/index.tsx create mode 100644 services/ui-src/src/measures/2024/HVLAD/types.ts create mode 100644 services/ui-src/src/measures/2024/HVLAD/validation.ts create mode 100644 services/ui-src/src/measures/2024/IETAD/data.ts create mode 100644 services/ui-src/src/measures/2024/IETAD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/IETAD/index.tsx create mode 100644 services/ui-src/src/measures/2024/IETAD/types.ts create mode 100644 services/ui-src/src/measures/2024/IETAD/validation.ts create mode 100644 services/ui-src/src/measures/2024/IETHH/data.ts create mode 100644 services/ui-src/src/measures/2024/IETHH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/IETHH/index.tsx create mode 100644 services/ui-src/src/measures/2024/IETHH/types.ts create mode 100644 services/ui-src/src/measures/2024/IETHH/validation.ts create mode 100644 services/ui-src/src/measures/2024/IMACH/data.ts create mode 100644 services/ui-src/src/measures/2024/IMACH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/IMACH/index.tsx create mode 100644 services/ui-src/src/measures/2024/IMACH/types.ts create mode 100644 services/ui-src/src/measures/2024/IMACH/validation.ts create mode 100644 services/ui-src/src/measures/2024/IUHH/data.ts create mode 100644 services/ui-src/src/measures/2024/IUHH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/IUHH/index.tsx create mode 100644 services/ui-src/src/measures/2024/IUHH/types.ts create mode 100644 services/ui-src/src/measures/2024/IUHH/validation.ts create mode 100644 services/ui-src/src/measures/2024/LBWCH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/LBWCH/index.tsx create mode 100644 services/ui-src/src/measures/2024/LRCDCH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/LRCDCH/index.tsx create mode 100644 services/ui-src/src/measures/2024/LSCCH/data.ts create mode 100644 services/ui-src/src/measures/2024/LSCCH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/LSCCH/index.tsx create mode 100644 services/ui-src/src/measures/2024/LSCCH/types.ts create mode 100644 services/ui-src/src/measures/2024/LSCCH/validation.ts create mode 100644 services/ui-src/src/measures/2024/MSCAD/data.ts create mode 100644 services/ui-src/src/measures/2024/MSCAD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/MSCAD/index.tsx create mode 100644 services/ui-src/src/measures/2024/MSCAD/questions/DataSource.tsx create mode 100644 services/ui-src/src/measures/2024/MSCAD/questions/index.tsx create mode 100644 services/ui-src/src/measures/2024/MSCAD/types.ts create mode 100644 services/ui-src/src/measures/2024/MSCAD/validation.ts create mode 100644 services/ui-src/src/measures/2024/NCIDDSAD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/NCIDDSAD/index.tsx create mode 100644 services/ui-src/src/measures/2024/OEVCH/data.ts create mode 100644 services/ui-src/src/measures/2024/OEVCH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/OEVCH/index.tsx create mode 100644 services/ui-src/src/measures/2024/OEVCH/types.ts create mode 100644 services/ui-src/src/measures/2024/OEVCH/validation.ts create mode 100644 services/ui-src/src/measures/2024/OHDAD/data.ts create mode 100644 services/ui-src/src/measures/2024/OHDAD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/OHDAD/index.tsx create mode 100644 services/ui-src/src/measures/2024/OHDAD/types.ts create mode 100644 services/ui-src/src/measures/2024/OHDAD/validation.ts create mode 100644 services/ui-src/src/measures/2024/OUDAD/data.ts create mode 100644 services/ui-src/src/measures/2024/OUDAD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/OUDAD/index.tsx create mode 100644 services/ui-src/src/measures/2024/OUDAD/types.ts create mode 100644 services/ui-src/src/measures/2024/OUDAD/validation.ts create mode 100644 services/ui-src/src/measures/2024/OUDHH/data.ts create mode 100644 services/ui-src/src/measures/2024/OUDHH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/OUDHH/index.tsx create mode 100644 services/ui-src/src/measures/2024/OUDHH/types.ts create mode 100644 services/ui-src/src/measures/2024/OUDHH/validation.ts create mode 100644 services/ui-src/src/measures/2024/PCRAD/data.ts create mode 100644 services/ui-src/src/measures/2024/PCRAD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/PCRAD/index.tsx create mode 100644 services/ui-src/src/measures/2024/PCRAD/questions/PerformanceMeasure.tsx create mode 100644 services/ui-src/src/measures/2024/PCRAD/types.ts create mode 100644 services/ui-src/src/measures/2024/PCRAD/validation.ts create mode 100644 services/ui-src/src/measures/2024/PCRHH/data.ts create mode 100644 services/ui-src/src/measures/2024/PCRHH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/PCRHH/index.tsx create mode 100644 services/ui-src/src/measures/2024/PCRHH/questions/PerformanceMeasure.tsx create mode 100644 services/ui-src/src/measures/2024/PCRHH/types.ts create mode 100644 services/ui-src/src/measures/2024/PCRHH/validation.ts create mode 100644 services/ui-src/src/measures/2024/PPCAD/data.ts create mode 100644 services/ui-src/src/measures/2024/PPCAD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/PPCAD/index.tsx create mode 100644 services/ui-src/src/measures/2024/PPCAD/types.ts create mode 100644 services/ui-src/src/measures/2024/PPCAD/validation.ts create mode 100644 services/ui-src/src/measures/2024/PPCCH/data.ts create mode 100644 services/ui-src/src/measures/2024/PPCCH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/PPCCH/index.tsx create mode 100644 services/ui-src/src/measures/2024/PPCCH/types.ts create mode 100644 services/ui-src/src/measures/2024/PPCCH/validation.ts create mode 100644 services/ui-src/src/measures/2024/PQI01AD/data.ts create mode 100644 services/ui-src/src/measures/2024/PQI01AD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/PQI01AD/index.tsx create mode 100644 services/ui-src/src/measures/2024/PQI01AD/types.ts create mode 100644 services/ui-src/src/measures/2024/PQI01AD/validation.ts create mode 100644 services/ui-src/src/measures/2024/PQI05AD/data.ts create mode 100644 services/ui-src/src/measures/2024/PQI05AD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/PQI05AD/index.tsx create mode 100644 services/ui-src/src/measures/2024/PQI05AD/types.ts create mode 100644 services/ui-src/src/measures/2024/PQI05AD/validation.ts create mode 100644 services/ui-src/src/measures/2024/PQI08AD/data.ts create mode 100644 services/ui-src/src/measures/2024/PQI08AD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/PQI08AD/index.tsx create mode 100644 services/ui-src/src/measures/2024/PQI08AD/types.ts create mode 100644 services/ui-src/src/measures/2024/PQI08AD/validation.ts create mode 100644 services/ui-src/src/measures/2024/PQI15AD/data.ts create mode 100644 services/ui-src/src/measures/2024/PQI15AD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/PQI15AD/index.tsx create mode 100644 services/ui-src/src/measures/2024/PQI15AD/types.ts create mode 100644 services/ui-src/src/measures/2024/PQI15AD/validation.ts create mode 100644 services/ui-src/src/measures/2024/PQI92HH/data.ts create mode 100644 services/ui-src/src/measures/2024/PQI92HH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/PQI92HH/index.tsx create mode 100644 services/ui-src/src/measures/2024/PQI92HH/types.ts create mode 100644 services/ui-src/src/measures/2024/PQI92HH/validation.ts create mode 100644 services/ui-src/src/measures/2024/SAAAD/data.ts create mode 100644 services/ui-src/src/measures/2024/SAAAD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/SAAAD/index.tsx create mode 100644 services/ui-src/src/measures/2024/SAAAD/types.ts create mode 100644 services/ui-src/src/measures/2024/SAAAD/validation.ts create mode 100644 services/ui-src/src/measures/2024/SFMCH/data.ts create mode 100644 services/ui-src/src/measures/2024/SFMCH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/SFMCH/index.tsx create mode 100644 services/ui-src/src/measures/2024/SFMCH/types.ts create mode 100644 services/ui-src/src/measures/2024/SFMCH/validation.ts create mode 100644 services/ui-src/src/measures/2024/SSDAD/data.ts create mode 100644 services/ui-src/src/measures/2024/SSDAD/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/SSDAD/index.tsx create mode 100644 services/ui-src/src/measures/2024/SSDAD/types.ts create mode 100644 services/ui-src/src/measures/2024/SSDAD/validation.ts create mode 100644 services/ui-src/src/measures/2024/SSHH/data.ts create mode 100644 services/ui-src/src/measures/2024/SSHH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/SSHH/index.tsx create mode 100644 services/ui-src/src/measures/2024/SSHH/questions/PerformanceMeasure.tsx create mode 100644 services/ui-src/src/measures/2024/SSHH/types.ts create mode 100644 services/ui-src/src/measures/2024/SSHH/validation.ts create mode 100644 services/ui-src/src/measures/2024/TFLCH/data.ts create mode 100644 services/ui-src/src/measures/2024/TFLCH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/TFLCH/index.tsx create mode 100644 services/ui-src/src/measures/2024/TFLCH/types.ts create mode 100644 services/ui-src/src/measures/2024/TFLCH/validation.ts create mode 100644 services/ui-src/src/measures/2024/W30CH/data.ts create mode 100644 services/ui-src/src/measures/2024/W30CH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/W30CH/index.tsx create mode 100644 services/ui-src/src/measures/2024/W30CH/types.ts create mode 100644 services/ui-src/src/measures/2024/W30CH/validation.ts create mode 100644 services/ui-src/src/measures/2024/WCCCH/data.ts create mode 100644 services/ui-src/src/measures/2024/WCCCH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/WCCCH/index.tsx create mode 100644 services/ui-src/src/measures/2024/WCCCH/types.ts create mode 100644 services/ui-src/src/measures/2024/WCCCH/validation.ts create mode 100644 services/ui-src/src/measures/2024/WCVCH/data.ts create mode 100644 services/ui-src/src/measures/2024/WCVCH/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/WCVCH/index.tsx create mode 100644 services/ui-src/src/measures/2024/WCVCH/types.ts create mode 100644 services/ui-src/src/measures/2024/WCVCH/validation.ts create mode 100644 services/ui-src/src/measures/2024/index.tsx create mode 100644 services/ui-src/src/measures/2024/rateLabelText.test.ts create mode 100644 services/ui-src/src/measures/2024/rateLabelText.ts create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/AdditionalNotes/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/AdditionalNotes/index.tsx create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/CombinedRates/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/CombinedRates/index.tsx create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/DataSource/__snapshots__/index.test.tsx.snap create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/DataSource/data.ts create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/DataSource/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/DataSource/index.tsx create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/DataSourceCahps/data.ts create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/DataSourceCahps/index.tsx create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/DateRange/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/DateRange/index.tsx create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/DefinitionsOfPopulation/__snapshots__/index.test.tsx.snap create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/DefinitionsOfPopulation/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/DefinitionsOfPopulation/index.tsx create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/DeviationFromMeasureSpecification/index.tsx create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/MeasurementSpecification/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/MeasurementSpecification/index.tsx create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/NotCollectingOMS/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/NotCollectingOMS/index.tsx create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/OptionalMeasureStrat/additionalCategory.tsx create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/OptionalMeasureStrat/context.ts create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data.ts create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/OptionalMeasureStrat/index.tsx create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/OptionalMeasureStrat/ndrSets.tsx create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/OptionalMeasureStrat/omsNodeBuilder.tsx create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/OptionalMeasureStrat/omsUtil.tsx create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/OptionalMeasureStrat/subCatClassification.tsx create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/OtherPerformanceMeasure/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/OtherPerformanceMeasure/index.tsx create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/PerformanceMeasure/data.ts create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/PerformanceMeasure/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/PerformanceMeasure/index.tsx create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/Reporting/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/Reporting/index.tsx create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/StatusOfData/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/StatusOfData/index.tsx create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/WhyAreYouNotReporting/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/WhyAreYouNotReporting/index.tsx create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/index.ts create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/index.tsx create mode 100644 services/ui-src/src/measures/2024/shared/CommonQuestions/types.ts create mode 100644 services/ui-src/src/measures/2024/shared/Qualifiers/Common/administrativeQuestions.test.tsx create mode 100644 services/ui-src/src/measures/2024/shared/Qualifiers/Common/administrativeQuestions.tsx create mode 100644 services/ui-src/src/measures/2024/shared/Qualifiers/Common/audit.tsx create mode 100644 services/ui-src/src/measures/2024/shared/Qualifiers/Common/costSavingsData.tsx create mode 100644 services/ui-src/src/measures/2024/shared/Qualifiers/Common/externalContractor.tsx create mode 100644 services/ui-src/src/measures/2024/shared/Qualifiers/Common/index.tsx create mode 100644 services/ui-src/src/measures/2024/shared/Qualifiers/Common/qualifierHeader.tsx create mode 100644 services/ui-src/src/measures/2024/shared/Qualifiers/data.ts create mode 100644 services/ui-src/src/measures/2024/shared/Qualifiers/deliverySystems.tsx create mode 100644 services/ui-src/src/measures/2024/shared/Qualifiers/index.tsx create mode 100644 services/ui-src/src/measures/2024/shared/Qualifiers/types.tsx create mode 100644 services/ui-src/src/measures/2024/shared/Qualifiers/validationFunctions.ts create mode 100644 services/ui-src/src/measures/2024/shared/Qualifiers/validations/adult.ts create mode 100644 services/ui-src/src/measures/2024/shared/Qualifiers/validations/childCHIP.ts create mode 100644 services/ui-src/src/measures/2024/shared/Qualifiers/validations/childCombined.ts create mode 100644 services/ui-src/src/measures/2024/shared/Qualifiers/validations/childMedicaid.ts create mode 100644 services/ui-src/src/measures/2024/shared/Qualifiers/validations/healthHome.ts create mode 100644 services/ui-src/src/measures/2024/shared/Qualifiers/validations/index.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/ComplexValidations/ComplexAtLeastOneRateComplete/index.tsx create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/ComplexValidations/ComplexNoNonZeroNumOrDenom/index.tsx create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/ComplexValidations/ComplexValidateDualPopInformation/index.tsx create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/ComplexValidations/ComplexValidateNDRTotals/index.tsx create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/ComplexValidations/ComplexValueSameCrossCategory/index.tsx create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/PCRValidations/PCRatLeastOneRateComplete/index.tsx create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/PCRValidations/PCRnoNonZeroNumOrDenom/index.tsx create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/dataDrivenTools.test.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/dataDrivenTools.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/index.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/omsValidator/index.test.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/omsValidator/index.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/testHelpers/_helper.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/testHelpers/_testFormData.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/types.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDataSource/index.test.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDataSource/index.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDataSourceType/index.test.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDataSourceType/index.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDefinitionOfPopulation/index.test.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDefinitionOfPopulation/index.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDeliverySystem/index.test.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDeliverySystem/index.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDeviationFieldFilled/index.test.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneDeviationFieldFilled/index.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneRateComplete/index.test.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateAtLeastOneRateComplete/index.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateBothDatesInRange/index.test.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateBothDatesInRange/index.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateDateRangeRadioButtonCompletion/index.test.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateDateRangeRadioButtonCompletion/index.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateDualPopInformation/index.test.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateDualPopInformation/index.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateEqualCategoryDenominators/index.test.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateEqualCategoryDenominators/index.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateEqualQualifierDenominators/index.test.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateEqualQualifierDenominators/index.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateFfsRadioButtonCompletion/index.test.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateFfsRadioButtonCompletion/index.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateHedisYear/index.test.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateHedisYear/index.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateNoNonZeroNumOrDenom/index.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateNumeratorsLessThanDenominators/index.test.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateNumeratorsLessThanDenominators/index.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateOPMRates/index.test.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateOPMRates/index.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateOneCatRateHigherThanOtherCat/index.test.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateOneCatRateHigherThanOtherCat/index.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateOneQualDenomHigherThanOtherDenomOMS/index.test.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateOneQualDenomHigherThanOtherDenomOMS/index.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateOneQualRateHigherThanOtherQual/index.test.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateOneQualRateHigherThanOtherQual/index.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validatePartialRateCompletion/index.test.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validatePartialRateCompletion/index.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateRateNotZero/index.test.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateRateNotZero/index.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateRateZero/index.test.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateRateZero/index.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateReasonForNotReporting/index.test.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateReasonForNotReporting/index.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateRequiredRadioButtonForCombinedRates/index.test.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateRequiredRadioButtonForCombinedRates/index.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateSameDenominatorSets/index.test.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateSameDenominatorSets/index.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateTotals/index.test.tsx create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateTotals/index.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateYearFormat/index.test.ts create mode 100644 services/ui-src/src/measures/2024/shared/globalValidations/validateYearFormat/index.ts create mode 100644 services/ui-src/src/measures/2024/shared/util/validationsMock.tsx create mode 100644 services/ui-src/src/utils/testUtils/2024/validationHelper.test.ts create mode 100644 services/ui-src/src/utils/testUtils/2024/validationHelpers.ts diff --git a/README.md b/README.md index 27c332c8cf..320ade22cf 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ QMR is the CMCS MDCT application for collecting state data for related to measur # Table of Contents +- [MDCT QMR (Quality Measure Reporting)](#mdct-qmr-quality-measure-reporting) +- [Table of Contents](#table-of-contents) - [Getting Started](#getting-started) - [Local Development Setup](#local-development-setup) - [Prettier](#prettier) @@ -539,9 +541,13 @@ to make in order to get that year working. ![After](./.images/afterImportUpdate.png?raw=true) -5. Copy over the `/globalValidations`, `/CommonQuestions`, and `/Qualifiers` directories to the latest year +5. Go to the `/services/ui-src/src/utils/testUtils`, create a new directory for the latest year (e.g. 2024), and copy over the `validationHelpers.ts` and `validationHelpers.test.ts` files -6. Similar to Step 4, update import names from the previous year to the most recent year + **NOTE:** Remember to update the imports in these files to reflect the latest year. + +6. Copy over the `/globalValidations`, `/CommonQuestions`, and `/Qualifiers` directories to the latest year + +7. Similar to Step 4, update import names from the previous year to the most recent year Before @@ -551,9 +557,9 @@ to make in order to get that year working. ![After](./.images/afterCommonComponentUpdate.png?raw=true) -7. In `services/ui-src/src/libs/spaLib.ts`, copy over the prior year's entry into the array. +8. In `services/ui-src/src/libs/spaLib.ts`, copy over the prior year's entry into the array. -8. In `services/ui-src/src/measures/measureDescriptions.ts` , copy over the prior year's entry into the array. +9. In `services/ui-src/src/measures/measureDescriptions.ts` , copy over the prior year's entry into the array. ## Things to Look Out For (Gotchas) diff --git a/services/app-api/handlers/dynamoUtils/measureList.ts b/services/app-api/handlers/dynamoUtils/measureList.ts index e1c6a0e22c..ffd81ee8c4 100644 --- a/services/app-api/handlers/dynamoUtils/measureList.ts +++ b/services/app-api/handlers/dynamoUtils/measureList.ts @@ -1034,4 +1034,342 @@ export const measures: Measure = { placeholder: true, }, ], + 2024: [ + { + type: "A", + measure: "CSQ", + }, + { + type: "C", + measure: "CSQ", + }, + { + type: "H", + measure: "CSQ", + }, + { + type: "A", + measure: "AAB-AD", + }, + { + type: "A", + measure: "AMM-AD", + }, + { + type: "A", + measure: "AMR-AD", + }, + { + type: "A", + measure: "BCS-AD", + }, + { + type: "A", + measure: "CBP-AD", + }, + { + type: "A", + measure: "CCP-AD", + }, + { + type: "A", + measure: "CCS-AD", + }, + { + type: "A", + measure: "CCW-AD", + }, + { + type: "A", + measure: "CDF-AD", + }, + { + type: "A", + measure: "CHL-AD", + }, + { + type: "A", + measure: "COB-AD", + }, + { + type: "A", + measure: "COL-AD", + }, + { + type: "A", + measure: "CPA-AD", + }, + { + type: "A", + measure: "CPU-AD", + }, + { + type: "A", + measure: "FUA-AD", + }, + { + type: "A", + measure: "FUH-AD", + }, + { + type: "A", + measure: "FUM-AD", + }, + { + type: "A", + measure: "FVA-AD", + }, + { + type: "A", + measure: "HBD-AD", + }, + { + type: "A", + measure: "HPCMI-AD", + }, + { + type: "A", + measure: "HVL-AD", + }, + { + type: "A", + measure: "IET-AD", + }, + { + type: "A", + measure: "MSC-AD", + }, + { + type: "A", + measure: "NCIDDS-AD", + autocompleteOnCreation: true, + }, + { + type: "A", + measure: "OHD-AD", + }, + { + type: "A", + measure: "OUD-AD", + }, + { + type: "A", + measure: "PCR-AD", + }, + { + type: "A", + measure: "PPC-AD", + }, + { + type: "A", + measure: "PQI01-AD", + }, + { + type: "A", + measure: "PQI05-AD", + }, + { + type: "A", + measure: "PQI08-AD", + }, + { + type: "A", + measure: "PQI15-AD", + }, + { + type: "A", + measure: "SAA-AD", + }, + { + type: "A", + measure: "SSD-AD", + }, + { + type: "C", + measure: "AAB-CH", + }, + { + type: "C", + measure: "ADD-CH", + }, + { + type: "C", + measure: "AMB-CH", + }, + { + type: "C", + measure: "AMR-CH", + }, + { + type: "C", + measure: "APM-CH", + }, + { + type: "C", + measure: "APP-CH", + }, + { + type: "C", + measure: "CCP-CH", + }, + { + type: "C", + measure: "CCW-CH", + }, + { + type: "C", + measure: "CDF-CH", + }, + { + type: "C", + measure: "CHL-CH", + }, + { + type: "C", + measure: "CIS-CH", + }, + { + type: "C", + measure: "CPC-CH", + }, + { + type: "C", + measure: "DEV-CH", + }, + { + type: "C", + measure: "FUA-CH", + }, + { + type: "C", + measure: "FUH-CH", + }, + { + type: "C", + measure: "FUM-CH", + }, + { + type: "C", + measure: "IMA-CH", + }, + { + type: "C", + measure: "LSC-CH", + }, + { + type: "C", + measure: "LBW-CH", + autocompleteOnCreation: true, + }, + { + type: "C", + measure: "LRCD-CH", + autocompleteOnCreation: true, + }, + { + type: "C", + measure: "OEV-CH", + }, + { + type: "C", + measure: "PPC-CH", + }, + { + type: "C", + measure: "SFM-CH", + }, + { + type: "C", + measure: "TFL-CH", + }, + { + type: "C", + measure: "W30-CH", + }, + { + type: "C", + measure: "WCC-CH", + }, + { + type: "C", + measure: "WCV-CH", + }, + { + type: "H", + measure: "AIF-HH", + }, + { + type: "H", + measure: "AMB-HH", + }, + { + type: "H", + measure: "CBP-HH", + }, + { + type: "H", + measure: "CDF-HH", + }, + { + type: "H", + measure: "COL-HH", + }, + { + type: "H", + measure: "FUA-HH", + }, + { + type: "H", + measure: "FUH-HH", + }, + { + type: "H", + measure: "FUM-HH", + }, + { + type: "H", + measure: "IET-HH", + }, + { + type: "H", + measure: "IU-HH", + }, + { + type: "H", + measure: "OUD-HH", + }, + { + type: "H", + measure: "PCR-HH", + }, + { + type: "H", + measure: "PQI92-HH", + }, + { + type: "H", + measure: "SS-1-HH", + placeholder: true, + }, + { + type: "H", + measure: "SS-2-HH", + placeholder: true, + }, + { + type: "H", + measure: "SS-3-HH", + placeholder: true, + }, + { + type: "H", + measure: "SS-4-HH", + placeholder: true, + }, + { + type: "H", + measure: "SS-5-HH", + placeholder: true, + }, + ], }; diff --git a/services/app-api/handlers/measures/tests/get.test.ts b/services/app-api/handlers/measures/tests/get.test.ts index 3312c39019..882864d599 100644 --- a/services/app-api/handlers/measures/tests/get.test.ts +++ b/services/app-api/handlers/measures/tests/get.test.ts @@ -150,7 +150,7 @@ describe("Test Get Measure Handlers", () => { }; const res = await getReportingYears(event, null); expect(res.statusCode).toBe(StatusCodes.SUCCESS); - expect(res.body).toBe('["2021","2022","2023"]'); + expect(res.body).toBe('["2021","2022","2023","2024"]'); }); test("Test getMeasureListInfo works when called with an empty object", async () => { diff --git a/services/ui-src/src/config.ts b/services/ui-src/src/config.ts index 5d429e925c..3497e65808 100644 --- a/services/ui-src/src/config.ts +++ b/services/ui-src/src/config.ts @@ -32,7 +32,7 @@ const configToExport = { REDIRECT_SIGNOUT: window._env_.COGNITO_REDIRECT_SIGNOUT, }, POST_SIGNOUT_REDIRECT: window._env_.POST_SIGNOUT_REDIRECT, - currentReportingYear: "2023", + currentReportingYear: "2024", REACT_APP_LD_SDK_CLIENT: window._env_.REACT_APP_LD_SDK_CLIENT, }; diff --git a/services/ui-src/src/dataConstants.ts b/services/ui-src/src/dataConstants.ts index 1db44642f0..877b269d48 100644 --- a/services/ui-src/src/dataConstants.ts +++ b/services/ui-src/src/dataConstants.ts @@ -113,6 +113,7 @@ export const HEDIS_2020 = "HEDIS 2020"; export const HEDIS_MY_2020 = "HEDIS MY 2020"; export const HEDIS_MY_2021 = "HEDIS MY 2021"; export const HEDIS_MY_2022 = "HEDIS MY 2022"; +export const HEDIS_MY_2023 = "HEDIS MY 2023"; export const HRSA = "HRSA"; export const HYBRID_ADMINSTRATIVE_AND_MEDICAL_RECORDS_DATA = "HybridAdministrativeandMedicalRecordsData"; diff --git a/services/ui-src/src/hooks/api/useGetCoreSets.tsx b/services/ui-src/src/hooks/api/useGetCoreSets.tsx index 8a792ed5f7..af408e19d4 100644 --- a/services/ui-src/src/hooks/api/useGetCoreSets.tsx +++ b/services/ui-src/src/hooks/api/useGetCoreSets.tsx @@ -14,9 +14,9 @@ const getCoreSets = async ({ state, year }: GetCoreSets) => { }); }; -export const useGetCoreSets = (releasedTwentyTwentyThree: boolean) => { +export const useGetCoreSets = (releasedTwentyTwentyFour: boolean) => { const { state, year } = useParams(); - if (state && year && (releasedTwentyTwentyThree || year !== "2023")) { + if (state && year && (releasedTwentyTwentyFour || year !== "2024")) { return useQuery(["coreSets", state, year], () => getCoreSets({ state, year }) ); diff --git a/services/ui-src/src/libs/spaLib.ts b/services/ui-src/src/libs/spaLib.ts index 5f6dc5cdc8..487761e80d 100644 --- a/services/ui-src/src/libs/spaLib.ts +++ b/services/ui-src/src/libs/spaLib.ts @@ -376,4 +376,128 @@ export const SPA: { [year: string]: SPAi[] } = { { id: "12-008", state: "WI", name: "Individuals with HIV/AIDS" }, { id: "21-0012", state: "WI", name: "Substance Use Disorder Health Home" }, ], + "2024": [ + { + id: "19-0037", + state: "CA", + name: "California Health Home Program (HHP)", + }, + { id: "20-0002", state: "CA", name: "California Behavioral Health Home" }, + { + id: "15-014", + state: "CT", + name: "Serious and Persistent Mental Illness", + }, + { + id: "18-006", + state: "DE", + name: "Delaware Assertive Community Integration Support Team (ACIST) Health Home", + }, + { + id: "18-0006", + state: "DC", + name: "Chronic Care Management for Individuals with Serious and Persistent Mental Health Conditions", + }, + { id: "18-0007", state: "DC", name: "Chronic Conditions" }, + { id: "22-0003", state: "IA", name: "Iowa Health Home Services" }, + { + id: "20-0011", + state: "IA", + name: "Iowa Severe and Persistent Mental Illness Health Home ", + }, + { + id: "20-0004", + state: "KS", + name: "Health Home Serving Serious Mental Illness (SMI)", + }, + { + id: "20-0005", + state: "KS", + name: "Health Home Chronic Conditions - Asthma", + }, + { + id: "13-012", + state: "ME", + name: "Maine Stage A Health Home Targeting Individuals with Chronic Conditions", + }, + { id: "18-0002", state: "ME", name: "Maine Behavioral Health Home" }, + { + id: "22-0018", + state: "ME", + name: "Maine Health Home for Beneficiaries Receiving Medication Assisted Treatment for Opioid Addiction", + }, + { id: "21-0005", state: "MD", name: "Maryland Health Home Services" }, + { + id: "20-1500", + state: "MI", + name: "Chronic Care Model for Individuals with Serious and Persistent Mental Health Conditions", + }, + { id: "15-2000", state: "MI", name: "Michigan Care Team" }, + { id: "22-1501", state: "MI", name: "Opioid Health Home" }, + { id: "19-0015", state: "MN", name: "Minnesota Behavioral Health Homes" }, + { + id: "19-0020", + state: "MO", + name: "Community Mental Health Center Health Home", + }, + { id: "19-0003", state: "MO", name: "Primary Care Clinic Health Home" }, + { + id: "16-0001", + state: "NJ", + name: "New Jersey Behavioral Health Home (Adults)", + }, + { + id: "16-0002", + state: "NJ", + name: "New Jersey Behavioral Health Home (Children)", + }, + { id: "18-0002", state: "NM", name: "CareLink New Mexico" }, + { + id: "21-0072", + state: "NY", + name: "Health Home for High-Cost, High-Needs Enrollees ", + }, + { id: "22-0073", state: "NY", name: "New York I/DD Health Home Services" }, + { id: "14-0012", state: "OK", name: "Oklahoma Health Home (Adults)" }, + { id: "14-0011", state: "OK", name: "Oklahoma Health Home (Children)" }, + { + id: "18-009", + state: "RI", + name: "Comprehensive Evaluation, Diagnosis, Assessment, Referral, & Reevaluation (CEDARR) Family Centers Health Home", + }, + { + id: "18-006", + state: "RI", + name: "Community Mental Health Organizations Health Home", + }, + { + id: "21-0025", + state: "RI", + name: "Rhode Island Opioid Treatment Program Health Home Services", + }, + { id: "20-0008", state: "SD", name: "South Dakota Health Home" }, + { + id: "16-004", + state: "TN", + name: "Tennessee Health Link Health Home Program ", + }, + { + id: "14-007", + state: "VT", + name: "Vermont Health Home for Beneficiaries Receiving Medication Assisted Treatment for Opioid Addiction ", + }, + { id: "20-0031", state: "WA", name: "Washington Health Home Services" }, + { + id: "16-0007", + state: "WV", + name: "West Virginia Health Home for Individuals with Bipolar Disorder at Risk for Hepatitis Type B and C", + }, + { + id: "16-0008", + state: "WV", + name: "West Virginia Health Home for Individuals with Chronic Conditions", + }, + { id: "12-008", state: "WI", name: "Individuals with HIV/AIDS" }, + { id: "21-0012", state: "WI", name: "Substance Use Disorder Health Home" }, + ], }; diff --git a/services/ui-src/src/measures/2024/AABAD/data.ts b/services/ui-src/src/measures/2024/AABAD/data.ts new file mode 100644 index 0000000000..25b8ebee47 --- /dev/null +++ b/services/ui-src/src/measures/2024/AABAD/data.ts @@ -0,0 +1,15 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("AAB-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + customPrompt: + "Enter a number for the numerator and the denominator. The measure is reported as an inverted rate. The formula for the Rate = (1 - (Numerator/Denominator)) x 100", + questionText: [ + "The percentage of episodes for beneficiaries age 18 and older with a diagnosis of acute bronchitis/bronchiolitis that did not result in an antibiotic dispensing event.", + ], + questionListItems: [], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/AABAD/index.test.tsx b/services/ui-src/src/measures/2024/AABAD/index.test.tsx new file mode 100644 index 0000000000..3dbd6c11bd --- /dev/null +++ b/services/ui-src/src/measures/2024/AABAD/index.test.tsx @@ -0,0 +1,244 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "AAB-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 18 to 64", + rate: "0.0", + numerator: "55", + denominator: "55", + }, + { + label: "Age 65 and Older", + rate: "0.0", + numerator: "44", + denominator: "44", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/AABAD/index.tsx b/services/ui-src/src/measures/2024/AABAD/index.tsx new file mode 100644 index 0000000000..be280b5701 --- /dev/null +++ b/services/ui-src/src/measures/2024/AABAD/index.tsx @@ -0,0 +1,80 @@ +import * as PMD from "./data"; +import * as QMR from "components"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; + +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; + +import { FormData } from "./types"; +import { validationFunctions } from "./validation"; +import { AABRateCalculation } from "utils/rateFormulas"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; + +export const AABAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && ( + + )} + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/AABAD/types.ts b/services/ui-src/src/measures/2024/AABAD/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/AABAD/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/AABAD/validation.ts b/services/ui-src/src/measures/2024/AABAD/validation.ts new file mode 100644 index 0000000000..d494d709b8 --- /dev/null +++ b/services/ui-src/src/measures/2024/AABAD/validation.ts @@ -0,0 +1,77 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const AABADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 1; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + let errorArray: any[] = []; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const dateRange = data[DC.DATE_RANGE]; + const deviationReason = data[DC.DEVIATION_REASON]; + + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateOPMRates(OPM), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [GV.validateNumeratorLessThanDenominatorOMS()], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [AABADValidation]; diff --git a/services/ui-src/src/measures/2024/AABCH/data.ts b/services/ui-src/src/measures/2024/AABCH/data.ts new file mode 100644 index 0000000000..73afe4b456 --- /dev/null +++ b/services/ui-src/src/measures/2024/AABCH/data.ts @@ -0,0 +1,14 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("AAB-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + customPrompt: + "Enter a number for the numerator and the denominator. The measure is reported as an inverted rate. The formula for the Rate = (1 - (Numerator/Denominator)) x 100", + questionText: [ + "The percentage of episodes for beneficiaries ages 3 months to 17 years with a diagnosis of acute bronchitis/bronchiolitis that did not result in an antibiotic dispensing event.", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/AABCH/index.test.tsx b/services/ui-src/src/measures/2024/AABCH/index.test.tsx new file mode 100644 index 0000000000..3cd25850fa --- /dev/null +++ b/services/ui-src/src/measures/2024/AABCH/index.test.tsx @@ -0,0 +1,239 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "AAB-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 3 months to 17", + rate: "0.0", + numerator: "55", + denominator: "55", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/AABCH/index.tsx b/services/ui-src/src/measures/2024/AABCH/index.tsx new file mode 100644 index 0000000000..e75a54ce03 --- /dev/null +++ b/services/ui-src/src/measures/2024/AABCH/index.tsx @@ -0,0 +1,85 @@ +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { FormData } from "./types"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { positiveNumbersWithMaxDecimalPlaces } from "utils"; +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import { validationFunctions } from "./validation"; +import { AABRateCalculation } from "utils/rateFormulas"; + +export const AABCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + let mask: RegExp = positiveNumbersWithMaxDecimalPlaces(1); + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + const rateScale = 100; + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && ( + + )} + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/AABCH/types.ts b/services/ui-src/src/measures/2024/AABCH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/AABCH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/AABCH/validation.ts b/services/ui-src/src/measures/2024/AABCH/validation.ts new file mode 100644 index 0000000000..d127c01387 --- /dev/null +++ b/services/ui-src/src/measures/2024/AABCH/validation.ts @@ -0,0 +1,73 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const AABCHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const deviationReason = data[DC.DEVIATION_REASON]; + + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + // Performance Measure Validations + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateTotalNDR(performanceMeasureArray), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + PMD.qualifiers + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateOPMRates(OPM), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateOMSTotalNDR(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [AABCHValidation]; diff --git a/services/ui-src/src/measures/2024/ADDCH/data.ts b/services/ui-src/src/measures/2024/ADDCH/data.ts new file mode 100644 index 0000000000..e7aec883bb --- /dev/null +++ b/services/ui-src/src/measures/2024/ADDCH/data.ts @@ -0,0 +1,58 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("ADD-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of children newly prescribed attention-deficit/hyperactivity disorder (ADHD) medication who had at least three follow-up care visits within a 10-month period, one of which was within 30 days of when the first ADHD medication was dispensed. Two rates are reported.", + ], + questionSubtextTitles: [ + "Initiation Phase", + "Continuation and Maintenance (C&M) Phase", + ], + questionSubtext: [ + "Percentage of children ages 6 to 12 with a prescription dispensed for ADHD medication, who had one follow-up visit with a practitioner with prescribing authority during the 30-day Initiation Phase.", + "Percentage of children ages 6 to 12 with a prescription dispensed for ADHD medication who remained on the medication for at least 210 days and who, in addition to the visit in the Initiation Phase, had at least two follow-up visits with a practitioner within 270 days (9 months) after the Initiation Phase ended.", + ], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_CLINIC_DATA_SYSTEMS, + description: true, + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/ADDCH/index.test.tsx b/services/ui-src/src/measures/2024/ADDCH/index.test.tsx new file mode 100644 index 0000000000..64b29589db --- /dev/null +++ b/services/ui-src/src/measures/2024/ADDCH/index.test.tsx @@ -0,0 +1,251 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "ADD-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect( + V.validateOneQualDenomHigherThanOtherDenomOMS + ).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateOneQualDenomHigherThanOtherDenomOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Initiation Phase", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Continuation and Maintenance (C&M) Phase", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/ADDCH/index.tsx b/services/ui-src/src/measures/2024/ADDCH/index.tsx new file mode 100644 index 0000000000..96edb598f7 --- /dev/null +++ b/services/ui-src/src/measures/2024/ADDCH/index.tsx @@ -0,0 +1,67 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +import { FormData } from "./types"; + +export const ADDCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/ADDCH/types.ts b/services/ui-src/src/measures/2024/ADDCH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/ADDCH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/ADDCH/validation.ts b/services/ui-src/src/measures/2024/ADDCH/validation.ts new file mode 100644 index 0000000000..c9e56a1b82 --- /dev/null +++ b/services/ui-src/src/measures/2024/ADDCH/validation.ts @@ -0,0 +1,78 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { getPerfMeasureRateArray } from "../shared/globalValidations"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const ADDCHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + let errorArray: any[] = []; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const dateRange = data[DC.DATE_RANGE]; + const deviationReason = data[DC.DEVIATION_REASON]; + + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + + errorArray = [ + ...errorArray, + ...GV.validateOneQualDenomHigherThanOtherDenomPM(data, PMD), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateOneQualDenomHigherThanOtherDenomOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [ADDCHValidation]; diff --git a/services/ui-src/src/measures/2024/AIFHH/data.ts b/services/ui-src/src/measures/2024/AIFHH/data.ts new file mode 100644 index 0000000000..74c176b04c --- /dev/null +++ b/services/ui-src/src/measures/2024/AIFHH/data.ts @@ -0,0 +1,89 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("AIF-HH"); + +const measureName = "AIFHH"; + +const inputFieldNames = [ + { + label: "Number of Enrollee Months", + text: "Number of Enrollee Months", + id: "QFSYON", + }, + { + label: "Number of Short-Term Admissions", + text: "Number of Short-Term Admissions", + id: "tMeLfq", + }, + { + label: "Short-Term Admissions per 1,000 Enrollee Months", + text: "Short-Term Admissions per 1,000 Enrollee Months", + id: "bxkVCC", + }, + { + label: "Number of Medium-Term Admissions", + text: "Number of Medium-Term Admissions", + id: "KBOnkQ", + }, + { + label: "Medium-Term Admissions per 1,000 Enrollee Months", + text: "Medium-Term Admissions per 1,000 Enrollee Months", + id: "5RO62J", + }, + { + label: "Number of Long-Term Admissions", + text: "Number of Long-Term Admissions", + id: "m3HvMS", + }, + { + label: "Long-Term Admissions per 1,000 Enrollee Months", + text: "Long-Term Admissions per 1,000 Enrollee Months", + id: "dFMGFi", + }, +]; + +// Rate structure by index in row +const ndrFormulas = [ + // Short-Term Admissions per 1,000 Enrollee Months + { + num: 1, + denom: 0, + rate: 2, + mult: 1000, + }, + // Medium-Term Admissions per 1,000 Enrollee Months + { + num: 3, + denom: 0, + rate: 4, + mult: 1000, + }, + // Long-Term Admissions per 1,000 Enrollee Months + { + num: 5, + denom: 0, + rate: 6, + mult: 1000, + }, +]; + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "The number of admissions to a facility among enrollees age 18 and older residing in the community for at least one month. The number of short-term, medium-term, or long-term admissions is reported per 1,000 enrollee months. Enrollee months reflect the total number of months each enrollee is enrolled in the program and residing in the community for at least one day of the month.", + ], + questionListItems: [ + " The rate of admissions resulting in a short-term stay (1 to 20 days) per 1,000 enrollee months.", + " The rate of admissions resulting in a medium-term stay (21 to 100 days) per 1,000 enrollee months.", + " The rate of admissions resulting in a long-term stay (greater than or equal to 101 days) per 1,000 enrollee months.", + ], + questionListTitles: ["Short-Term Stay", "Medium-Term Stay", "Long-Term Stay"], + questionSubtext: [ + "The following three performance rates are reported across four age groups (ages 18 to 64, ages 65 to 74, ages 75 to 84, and age 85 and older):", + ], + measureName, + inputFieldNames, + ndrFormulas, + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/AIFHH/index.test.tsx b/services/ui-src/src/measures/2024/AIFHH/index.test.tsx new file mode 100644 index 0000000000..631ac571a2 --- /dev/null +++ b/services/ui-src/src/measures/2024/AIFHH/index.test.tsx @@ -0,0 +1,383 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "AIF-HH"; +const coreSet = "HHCS"; +const state = "DC"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + afterEach(() => { + screen.debug(); + }); + it.skip("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it.skip("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it.skip("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it.skip("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it.skip("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it.skip("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it.skip("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.ComplexValidateDualPopInformation).not.toHaveBeenCalled(); + expect(V.ComplexNoNonZeroNumOrDenom).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.ComplexValidateNDRTotals).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it.skip("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.ComplexAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.ComplexNoNonZeroNumOrDenom).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.ComplexValidateNDRTotals).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it.skip("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + fields: [ + { + value: "1", + label: "Number of Enrollee Months", + }, + { + value: "1", + label: "Number of Short-Term Admissions", + }, + { + value: "1000.0", + label: "Short-Term Admissions per 1,000 Enrollee Months", + }, + { + value: "1", + label: "Number of Medium-Term Admissions", + }, + { + value: "1000.0", + label: "Medium-Term Admissions per 1,000 Enrollee Months", + }, + { + value: "1", + label: "Number of Long-Term Admissions", + }, + { + value: "1000.0", + label: "Long-Term Admissions per 1,000 Enrollee Months", + }, + ], + label: "Ages 18 to 64", + }, + { + fields: [ + { + value: "1", + label: "Number of Enrollee Months", + }, + { + value: "1", + label: "Number of Short-Term Admissions", + }, + { + value: "1000.0", + label: "Short-Term Admissions per 1,000 Enrollee Months", + }, + { + value: "1", + label: "Number of Medium-Term Admissions", + }, + { + value: "1000.0", + label: "Medium-Term Admissions per 1,000 Enrollee Months", + }, + { + value: "1", + label: "Number of Long-Term Admissions", + }, + { + value: "1000.0", + label: "Long-Term Admissions per 1,000 Enrollee Months", + }, + ], + label: "Ages 65 to 74", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Number of Short-Term Admissions", + }, + { + label: "Short-Term Admissions per 1,000 Enrollee Months", + }, + { + label: "Number of Medium-Term Admissions", + }, + { + label: "Medium-Term Admissions per 1,000 Enrollee Months", + }, + { + label: "Number of Long-Term Admissions", + }, + { + label: "Long-Term Admissions per 1,000 Enrollee Months", + }, + ], + label: "Ages 75 to 84", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Number of Short-Term Admissions", + }, + { + label: "Short-Term Admissions per 1,000 Enrollee Months", + }, + { + label: "Number of Medium-Term Admissions", + }, + { + label: "Medium-Term Admissions per 1,000 Enrollee Months", + }, + { + label: "Number of Long-Term Admissions", + }, + { + label: "Long-Term Admissions per 1,000 Enrollee Months", + }, + ], + label: "Age 85 and older", + }, + { + fields: [ + { + value: "2", + label: "Number of Enrollee Months", + }, + { + value: 2, + label: "Number of Short-Term Admissions", + }, + { + value: "1000.0", + label: "Short-Term Admissions per 1,000 Enrollee Months", + }, + { + value: 2, + label: "Number of Medium-Term Admissions", + }, + { + value: "1000.0", + label: "Medium-Term Admissions per 1,000 Enrollee Months", + }, + { + value: 2, + label: "Number of Long-Term Admissions", + }, + { + value: "1000.0", + label: "Long-Term Admissions per 1,000 Enrollee Months", + }, + ], + isTotal: true, + label: "Total (Age 18 and older)", + }, + ], + }, + }, + MeasurementSpecification: "CMS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/AIFHH/index.tsx b/services/ui-src/src/measures/2024/AIFHH/index.tsx new file mode 100644 index 0000000000..e2949af271 --- /dev/null +++ b/services/ui-src/src/measures/2024/AIFHH/index.tsx @@ -0,0 +1,85 @@ +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { FormData } from "./types"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import { validationFunctions } from "./validation"; +import { xNumbersYDecimals } from "utils"; + +export const AIFHH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && ( + + )} + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/AIFHH/types.ts b/services/ui-src/src/measures/2024/AIFHH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/AIFHH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/AIFHH/validation.ts b/services/ui-src/src/measures/2024/AIFHH/validation.ts new file mode 100644 index 0000000000..ccf5474bf5 --- /dev/null +++ b/services/ui-src/src/measures/2024/AIFHH/validation.ts @@ -0,0 +1,124 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +// Rate structure by index in row +const ndrFormulas = [ + // Short-Term Admissions per 1,000 Enrollee Months + { + numerator: 1, + denominator: 0, + rateIndex: 2, + mult: 1000, + }, + // Medium-Term Admissions per 1,000 Enrollee Months + { + numerator: 3, + denominator: 0, + rateIndex: 4, + mult: 1000, + }, + // Long-Term Admissions per 1,000 Enrollee Months + { + numerator: 5, + denominator: 0, + rateIndex: 6, + mult: 1000, + }, +]; + +let OPM: any; + +const AIFHHValidation = (data: FormData) => { + let errorArray: any[] = []; + const dateRange = data[DC.DATE_RANGE]; + const definitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + OPM = data[DC.OPM_RATES]; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + // Quick reference list of all rate indices + errorArray = [ + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + + ...GV.ComplexValidateDualPopInformation( + performanceMeasureArray, + OPM, + definitionOfDenominator + ), + + // Performance Measure Validations + ...GV.ComplexAtLeastOneRateComplete(performanceMeasureArray, OPM), + ...GV.ComplexNoNonZeroNumOrDenom(performanceMeasureArray, OPM, ndrFormulas), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.ComplexValidateNDRTotals( + performanceMeasureArray, + PMD.categories, + ndrFormulas + ), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [OMSValidations], + }), + ]; + return errorArray; +}; + +const OMSValidations: GV.Types.OmsValidationCallback = ({ + rateData, + locationDictionary, + label, +}) => { + const rates = Object.keys(rateData?.rates ?? {}).map((x) => { + return { rate: [rateData?.rates?.[x]?.OPM?.[0]] }; + }); + return OPM === undefined + ? [ + ...GV.ComplexNoNonZeroNumOrDenomOMS( + rateData?.["aifhh-rate"]?.rates ?? {}, + rates ?? [], + ndrFormulas, + `Optional Measure Stratification: ${locationDictionary(label)}` + ), + ] + : [ + ...GV.ComplexNoNonZeroNumOrDenomOMS( + rateData?.rates, + rates ?? [], + ndrFormulas, + `Optional Measure Stratification: ${locationDictionary(label)}` + ), + ]; +}; + +export const validationFunctions = [AIFHHValidation]; diff --git a/services/ui-src/src/measures/2024/AMBCH/data.ts b/services/ui-src/src/measures/2024/AMBCH/data.ts new file mode 100644 index 0000000000..2551bf4525 --- /dev/null +++ b/services/ui-src/src/measures/2024/AMBCH/data.ts @@ -0,0 +1,12 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("AMB-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Rate of emergency department (ED) visits per 1,000 beneficiary months among children up to age 19.", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/AMBCH/index.test.tsx b/services/ui-src/src/measures/2024/AMBCH/index.test.tsx new file mode 100644 index 0000000000..1ab8051d79 --- /dev/null +++ b/services/ui-src/src/measures/2024/AMBCH/index.test.tsx @@ -0,0 +1,274 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "AMB-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "< Age 1", + rate: "1000.0", + numerator: "1", + denominator: "1", + }, + { + label: "Ages 1 to 9", + rate: "1000.0", + numerator: "1", + denominator: "1", + }, + { + label: "Ages 10 to 19", + rate: "1000.0", + numerator: "1", + denominator: "1", + }, + { + label: "Ages unknown", + rate: "1000.0", + numerator: "1", + denominator: "1", + }, + { + label: "Total", + isTotal: true, + rate: "1000.0", + numerator: "4", + denominator: "4", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/AMBCH/index.tsx b/services/ui-src/src/measures/2024/AMBCH/index.tsx new file mode 100644 index 0000000000..dea4fc0050 --- /dev/null +++ b/services/ui-src/src/measures/2024/AMBCH/index.tsx @@ -0,0 +1,85 @@ +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { FormData } from "./types"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { positiveNumbersWithMaxDecimalPlaces } from "utils"; +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import { validationFunctions } from "./validation"; + +export const AMBCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + let mask: RegExp = positiveNumbersWithMaxDecimalPlaces(1); + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + const rateScale = 1000; + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && ( + + )} + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/AMBCH/types.ts b/services/ui-src/src/measures/2024/AMBCH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/AMBCH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/AMBCH/validation.ts b/services/ui-src/src/measures/2024/AMBCH/validation.ts new file mode 100644 index 0000000000..3aaacbfecf --- /dev/null +++ b/services/ui-src/src/measures/2024/AMBCH/validation.ts @@ -0,0 +1,71 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const AMBCHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateTotalNDR(performanceMeasureArray, undefined, undefined), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateEqualQualifierDenominatorsOMS(), + GV.validateOneCatRateHigherThanOtherCatOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateOMSTotalNDR(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [AMBCHValidation]; diff --git a/services/ui-src/src/measures/2024/AMBHH/data.ts b/services/ui-src/src/measures/2024/AMBHH/data.ts new file mode 100644 index 0000000000..5e8aea9eec --- /dev/null +++ b/services/ui-src/src/measures/2024/AMBHH/data.ts @@ -0,0 +1,12 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("AMB-HH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Rate of emergency department (ED) visits per 1,000 enrollee months among health home enrollees.", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/AMBHH/index.test.tsx b/services/ui-src/src/measures/2024/AMBHH/index.test.tsx new file mode 100644 index 0000000000..f172d2a291 --- /dev/null +++ b/services/ui-src/src/measures/2024/AMBHH/index.test.tsx @@ -0,0 +1,257 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "AMB-HH"; +const coreSet = "HHCS"; +const state = "DC"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/AMBHH/index.tsx b/services/ui-src/src/measures/2024/AMBHH/index.tsx new file mode 100644 index 0000000000..9326fcd785 --- /dev/null +++ b/services/ui-src/src/measures/2024/AMBHH/index.tsx @@ -0,0 +1,84 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { FormData } from "./types"; +import { positiveNumbersWithMaxDecimalPlaces } from "utils"; + +export const AMBHH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && ( + + )} + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/AMBHH/types.ts b/services/ui-src/src/measures/2024/AMBHH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/AMBHH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/AMBHH/validation.ts b/services/ui-src/src/measures/2024/AMBHH/validation.ts new file mode 100644 index 0000000000..a8d15a7e57 --- /dev/null +++ b/services/ui-src/src/measures/2024/AMBHH/validation.ts @@ -0,0 +1,85 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const AMBHHValidation = (data: FormData) => { + const definitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const OPM = data[DC.OPM_RATES]; + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const dateRange = data[DC.DATE_RANGE]; + + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + let errorArray: any[] = []; + + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + const validateDualPopInformationArray = [ + performanceMeasureArray?.[0].filter((pm) => { + return pm?.label === "Age 65 and older"; + }), + ]; + + const age65PlusIndex = 0; + + errorArray = [ + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateHedisYear(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + // Performance Measure Validations + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateDualPopInformationPM( + validateDualPopInformationArray, + OPM, + age65PlusIndex, + definitionOfDenominator + ), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateTotalNDR(performanceMeasureArray), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateOMSTotalNDR(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [AMBHHValidation]; diff --git a/services/ui-src/src/measures/2024/AMMAD/data.ts b/services/ui-src/src/measures/2024/AMMAD/data.ts new file mode 100644 index 0000000000..f09b161fde --- /dev/null +++ b/services/ui-src/src/measures/2024/AMMAD/data.ts @@ -0,0 +1,48 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("AMM-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of beneficiaries age 18 and older who were treated with antidepressant medication, had a diagnosis of major depression, and who remained on an antidepressant medication treatment. Two rates are reported:", + ], + questionListItems: [ + "Effective Acute Phase Treatment: Percentage of beneficiaries who remained on an antidepressant medication for at least 84 days (12 weeks).", + "Effective Continuation Phase Treatment: Percentage of beneficiaries who remained on an antidepressant medication for at least 180 days (6 months).", + ], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: "Administrative Data", + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: "Medicaid Management Information System (MMIS)", + }, + { + value: "Administrative Data Other", + description: true, + }, + ], + }, + ], + }, + { + value: "Electronic Health Records", + description: true, + }, + { + value: "Other Data Source", + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/AMMAD/index.test.tsx b/services/ui-src/src/measures/2024/AMMAD/index.test.tsx new file mode 100644 index 0000000000..d609c23ff4 --- /dev/null +++ b/services/ui-src/src/measures/2024/AMMAD/index.test.tsx @@ -0,0 +1,260 @@ +import { fireEvent, screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "AMM-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsPM).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + it("should not allow non state users to edit forms by disabling buttons", async () => { + useApiMock(apiData); + renderWithHookForm(component); + + expect(screen.getByTestId("measure-wrapper-form")).toBeInTheDocument(); + const completeButton = screen.getByText("Complete Measure"); + fireEvent.click(completeButton); + expect(completeButton).toHaveAttribute("disabled"); + }); + + jest.setTimeout(33000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + EffectiveAcutePhaseTreatment: [ + { + label: "Ages 18 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Age 65 and older", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/AMMAD/index.tsx b/services/ui-src/src/measures/2024/AMMAD/index.tsx new file mode 100644 index 0000000000..9ead254113 --- /dev/null +++ b/services/ui-src/src/measures/2024/AMMAD/index.tsx @@ -0,0 +1,68 @@ +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { FormData } from "./types"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import { validationFunctions } from "./validation"; + +export const AMMAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + // Conditional check to let rate be readonly when administrative data is the only option or no option is selected + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/AMMAD/types.ts b/services/ui-src/src/measures/2024/AMMAD/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/AMMAD/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/AMMAD/validation.ts b/services/ui-src/src/measures/2024/AMMAD/validation.ts new file mode 100644 index 0000000000..5f58b90884 --- /dev/null +++ b/services/ui-src/src/measures/2024/AMMAD/validation.ts @@ -0,0 +1,105 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const AMMADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 1; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const deviationReason = data[DC.DEVIATION_REASON]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + let unfilteredSameDenominatorErrors: any[] = []; + for (let i = 0; i < performanceMeasureArray.length; i += 2) { + unfilteredSameDenominatorErrors = [ + ...unfilteredSameDenominatorErrors, + ...GV.validateEqualQualifierDenominatorsPM( + [performanceMeasureArray[i], performanceMeasureArray[i + 1]], + ageGroups + ), + ]; + } + + let filteredSameDenominatorErrors: any = []; + let errorList: string[] = []; + unfilteredSameDenominatorErrors.forEach((error) => { + if (!(errorList.indexOf(error.errorMessage) > -1)) { + errorList.push(error.errorMessage); + filteredSameDenominatorErrors.push(error); + } + }); + + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...filteredSameDenominatorErrors, + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateOneCatRateHigherThanOtherCatOMS(), + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateSameDenominatorSets(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [AMMADValidation]; diff --git a/services/ui-src/src/measures/2024/AMRAD/data.ts b/services/ui-src/src/measures/2024/AMRAD/data.ts new file mode 100644 index 0000000000..fb14331192 --- /dev/null +++ b/services/ui-src/src/measures/2024/AMRAD/data.ts @@ -0,0 +1,13 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("AMR-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "The percentage of beneficiaries ages 19 to 64 who were identified as having persistent asthma and had a ratio of controller medications to total asthma medications of 0.50 or greater during the measurement year.", + ], + questionListItems: [], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/AMRAD/index.test.tsx b/services/ui-src/src/measures/2024/AMRAD/index.test.tsx new file mode 100644 index 0000000000..59e3e67574 --- /dev/null +++ b/services/ui-src/src/measures/2024/AMRAD/index.test.tsx @@ -0,0 +1,269 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +import * as DC from "dataConstants"; + +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "AMR-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")).toBeInTheDocument(); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { + MeasurementSpecification: "Other", + DidReport: "yes", + [DC.OPM_RATES]: [ + { + rate: [{ denominator: "", numerator: "", rate: "" }], + description: "", + }, + ], +}; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/AMRAD/index.tsx b/services/ui-src/src/measures/2024/AMRAD/index.tsx new file mode 100644 index 0000000000..898d7cd049 --- /dev/null +++ b/services/ui-src/src/measures/2024/AMRAD/index.tsx @@ -0,0 +1,115 @@ +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as QMR from "components"; +import * as PMD from "./data"; +import { useFormContext, useWatch } from "react-hook-form"; +import { FormData, Measure } from "./types"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { useEffect } from "react"; +import { validationFunctions } from "./validation"; + +export const AMRAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + const { getValues } = useFormContext(); + + // Watch Values of Form Questions + const watchOtherPerformanceMeasureRates = useWatch({ + name: "OtherPerformanceMeasure-Rates", + }); + + // Age Conditionals for Deviations from Measure Specifications/Optional Measure Stratification + const showOtherPerformanceMeasureRates = !!watchOtherPerformanceMeasureRates; + + const watchPersistentAsthma = useWatch({ + name: `PerformanceMeasure.rates.singleCategory`, + }); + + const showPersistentAsthma19To50 = !!watchPersistentAsthma?.[0]?.rate; + const showPersistentAsthma51To64 = !!watchPersistentAsthma?.[1]?.rate; + const showPersistentAsthmaTotal = !!watchPersistentAsthma?.[2]?.rate; + + // Logic to conditionally show age groups in Deviations from Measure Specifications/Optional Measure Stratification + const ageGroups = []; + + if (showPersistentAsthma19To50) { + ageGroups[0] = { label: "Ages 19 to 50", id: 0, isTotal: false }; + } + + if (showPersistentAsthma51To64) { + ageGroups[1] = { label: "Ages 51 to 64", id: 1, isTotal: false }; + } + + // Total field should show in OMS section if any of the NDRs in Performance Measures have been filled out. + if ( + showPersistentAsthma19To50 || + showPersistentAsthma51To64 || + showPersistentAsthmaTotal + ) { + ageGroups[2] = { label: "Total", id: 2, isTotal: true }; + } + + if (showOtherPerformanceMeasureRates) { + let otherRates = getValues("OtherPerformanceMeasure-Rates"); + otherRates.forEach((rate) => { + if (rate.description) { + ageGroups.push({ label: rate.description, id: ageGroups.length }); + } + }); + } + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {/* Show Performance Measure when HEDIS is selected from DataSource */} + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/AMRAD/types.ts b/services/ui-src/src/measures/2024/AMRAD/types.ts new file mode 100644 index 0000000000..22a8fec8f6 --- /dev/null +++ b/services/ui-src/src/measures/2024/AMRAD/types.ts @@ -0,0 +1,106 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; +export namespace Measure { + export interface Props { + name: string; + year: string; + measureId: string; + handleSubmit?: any; + handleValidation?: any; + setValidationFunctions?: React.Dispatch>; + } + + interface RateFields { + numerator: string; + denominator: string; + rate: string; + } + + interface AggregateRate { + subRate: RateFields[]; + total: RateFields[]; + } + + export interface Form + extends Types.MeasurementSpecification, + Types.AdditionalNotes, + Types.CombinedRates, + Types.DateRange, + Types.DefinitionOfPopulation, + Types.StatusOfData, + Types.DidReport, + Types.WhyAreYouNotReporting, + Types.DataSource, + Types.PerformanceMeasure, + Types.DeviationFromMeasureSpecification, + Types.OtherPerformanceMeasure { + "PerformanceMeasure-Explanation": string; + "PerformanceMeasure-AgeRates-Persistent-Asthma": { + denominator: string; + numerator: string; + other: string; + id: string; + label: string; + rate: string; + }[]; + + AddtnlEthnicity: string[]; + AddtnlEthnicityRates: AggregateRate[]; + + AddtnlNonHispanicRace: string[]; + AddtnlNonHispanicRaceRates: AggregateRate[]; + AddtnlNonHispanicRaceSubCatTitle: { titles: string[] }[]; + AddtnlNonHispanicRaceSubCatOptions: string[][]; + AddtnlNonHispanicRaceSubCatRates: { rates: AggregateRate[] }[]; + + AddtnlNonHispanicSubCat: string[]; + AddtnlNonHispanicSubCatRates: AggregateRate[]; + + NonHispanicRacialCategories: string[]; + "NHRC-WhiteRates": AggregateRate; + "NHRC-BlackOrAfricanAmericanRates": AggregateRate; + "NHRC-AmericanIndianOrAlaskaNativeRates": AggregateRate; + "NHRC-AggregateAsianRates": AggregateRate; + "NHRC-IndependentAsianRates": AggregateRate[]; + "NHRC-AggregateHawaiianOrPacificIslanderRates": AggregateRate; + "NHRC-IndependentHawaiianOrPacificIslanderRates": AggregateRate[]; + + EthnicityCategories: string[]; + EthnicitySubCategories: string[]; + NonHispanicEthnicityRates: AggregateRate; + HispanicIndependentReporting: string; + HispanicEthnicityAggregateRate: AggregateRate; + IndependentHispanicOptions: string[]; + IndependentHispanicRates: AggregateRate[]; + + AsianIndependentReporting: string; + IndependentAsianOptions: string[]; + NativeHawaiianIndependentReporting: string; + IndependentNativeHawaiianOptions: string[]; + + SexOptions: string[]; + MaleSexRates: AggregateRate; + FemaleSexRates: AggregateRate; + + PrimaryLanguageOptions: string[]; + AddtnlPrimaryLanguage: string[]; + AddtnlPrimaryLanguageRates: AggregateRate[]; + EnglishLanguageRate: AggregateRate; + SpanishLanguageRate: AggregateRate; + + DisabilityStatusOptions: string[]; + DisabilitySSIRate: AggregateRate; + DisabilityNonSSIRate: AggregateRate; + AddtnlDisabilityStatusDesc: string; + AddtnlDisabilityRate: AggregateRate; + + GeographyOptions: string[]; + UrbanGeographyRate: AggregateRate; + RuralGeographyRate: AggregateRate; + AddtnlGeographyDesc: string; + AddtnlGeographyRate: AggregateRate; + + ACAGroupRate: AggregateRate; + } +} + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/AMRAD/validation.ts b/services/ui-src/src/measures/2024/AMRAD/validation.ts new file mode 100644 index 0000000000..65c9d3365f --- /dev/null +++ b/services/ui-src/src/measures/2024/AMRAD/validation.ts @@ -0,0 +1,73 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const AMRADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateTotalNDR(performanceMeasureArray), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateOMSTotalNDR(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [AMRADValidation]; diff --git a/services/ui-src/src/measures/2024/AMRCH/data.ts b/services/ui-src/src/measures/2024/AMRCH/data.ts new file mode 100644 index 0000000000..1010929e66 --- /dev/null +++ b/services/ui-src/src/measures/2024/AMRCH/data.ts @@ -0,0 +1,12 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("AMR-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "The percentage of children and adolescents ages 5 to 18 who were identified as having persistent asthma and had a ratio of controller medications to total asthma medications of 0.50 or greater during the measurement year.", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/AMRCH/index.test.tsx b/services/ui-src/src/measures/2024/AMRCH/index.test.tsx new file mode 100644 index 0000000000..73410c0e37 --- /dev/null +++ b/services/ui-src/src/measures/2024/AMRCH/index.test.tsx @@ -0,0 +1,260 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "AMR-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 5 to 11", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + { + label: "Ages 12 to 18", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "2", + denominator: "2", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/AMRCH/index.tsx b/services/ui-src/src/measures/2024/AMRCH/index.tsx new file mode 100644 index 0000000000..10476d4887 --- /dev/null +++ b/services/ui-src/src/measures/2024/AMRCH/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +import { FormData } from "./types"; + +export const AMRCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/AMRCH/types.ts b/services/ui-src/src/measures/2024/AMRCH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/AMRCH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/AMRCH/validation.ts b/services/ui-src/src/measures/2024/AMRCH/validation.ts new file mode 100644 index 0000000000..d90fc74495 --- /dev/null +++ b/services/ui-src/src/measures/2024/AMRCH/validation.ts @@ -0,0 +1,73 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const AMRCHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateTotalNDR(performanceMeasureArray), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateOMSTotalNDR(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [AMRCHValidation]; diff --git a/services/ui-src/src/measures/2024/APMCH/data.ts b/services/ui-src/src/measures/2024/APMCH/data.ts new file mode 100644 index 0000000000..71b68e39ab --- /dev/null +++ b/services/ui-src/src/measures/2024/APMCH/data.ts @@ -0,0 +1,50 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("APM-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of children and adolescents ages 1 to 17 who had two or more antipsychotic prescriptions and had metabolic testing. Three rates are reported:", + ], + questionListItems: [ + "Percentage of children and adolescents on antipsychotics who received blood glucose testing", + "Percentage of children and adolescents on antipsychotics who received cholesterol testing", + "Percentage of children and adolescents on antipsychotics who received blood glucose and cholesterol testing", + ], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_CLINIC_DATA_SYSTEMS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/APMCH/index.test.tsx b/services/ui-src/src/measures/2024/APMCH/index.test.tsx new file mode 100644 index 0000000000..3e17ee0a9e --- /dev/null +++ b/services/ui-src/src/measures/2024/APMCH/index.test.tsx @@ -0,0 +1,304 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "APM-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(33000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + Cholesterol: [ + { + label: "Ages 1 to 11", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + { + label: "Ages 12 to 17", + rate: "100.0", + denominator: "1", + numerator: "1", + }, + { + label: "Total (Ages 1 to 17)", + isTotal: true, + rate: "100.0", + numerator: "2", + denominator: "2", + }, + ], + BloodGlucoseandCholesterol: [ + { + label: "Ages 1 to 11", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + { + label: "Ages 12 to 17", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + { + label: "Total (Ages 1 to 17)", + isTotal: true, + rate: "100.0", + numerator: "2", + denominator: "2", + }, + ], + BloodGlucose: [ + { + label: "Ages 1 to 11", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + { + label: "Ages 12 to 17", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + { + label: "Total (Ages 1 to 17)", + isTotal: true, + rate: "100.0", + numerator: "2", + denominator: "2", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/APMCH/index.tsx b/services/ui-src/src/measures/2024/APMCH/index.tsx new file mode 100644 index 0000000000..607df15597 --- /dev/null +++ b/services/ui-src/src/measures/2024/APMCH/index.tsx @@ -0,0 +1,68 @@ +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { FormData } from "./types"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import { validationFunctions } from "./validation"; + +export const APMCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/APMCH/types.ts b/services/ui-src/src/measures/2024/APMCH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/APMCH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/APMCH/validation.ts b/services/ui-src/src/measures/2024/APMCH/validation.ts new file mode 100644 index 0000000000..bb4a6b9af6 --- /dev/null +++ b/services/ui-src/src/measures/2024/APMCH/validation.ts @@ -0,0 +1,108 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const APMCHValidation = (data: FormData) => { + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + const validateEqualQualifierDenominatorsErrorMessage = ( + qualifier: string + ) => { + const isTotal = qualifier.split(" ")[0] === "Total"; + return `${ + isTotal ? "" : "The " + }${qualifier} denominator must be the same for each indicator.`; + }; + + const validateTotalNDRErrorMessage = ( + qualifier: string, + fieldType: string + ) => { + return `${fieldType} for the ${qualifier} Total rate is not equal to the sum of the ${qualifier} age-specific ${fieldType.toLowerCase()}s.`; + }; + + errorArray = [ + // Performance Measure and OPM Validations + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + PMD.qualifiers, + PMD.categories + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + PMD.qualifiers + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, PMD.qualifiers), + ...GV.validateRateZeroPM( + performanceMeasureArray, + OPM, + PMD.qualifiers, + data + ), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateTotalNDR( + performanceMeasureArray, + undefined, + PMD.categories, + validateTotalNDRErrorMessage + ), + ...GV.validateEqualQualifierDenominatorsPM( + performanceMeasureArray, + PMD.qualifiers, + undefined, + validateEqualQualifierDenominatorsErrorMessage + ), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateEqualQualifierDenominatorsOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateOMSTotalNDR(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [APMCHValidation]; diff --git a/services/ui-src/src/measures/2024/APPCH/data.ts b/services/ui-src/src/measures/2024/APPCH/data.ts new file mode 100644 index 0000000000..9cd3663f08 --- /dev/null +++ b/services/ui-src/src/measures/2024/APPCH/data.ts @@ -0,0 +1,12 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("APP-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of children and adolescents ages 1 to 17 who had a new prescription for an antipsychotic medication and had documentation of psychosocial care as first-line treatment.", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/APPCH/index.test.tsx b/services/ui-src/src/measures/2024/APPCH/index.test.tsx new file mode 100644 index 0000000000..d7b5023f5c --- /dev/null +++ b/services/ui-src/src/measures/2024/APPCH/index.test.tsx @@ -0,0 +1,260 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "APP-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 1 to 11", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + { + label: "Ages 12 to 17", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "2", + denominator: "2", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/APPCH/index.tsx b/services/ui-src/src/measures/2024/APPCH/index.tsx new file mode 100644 index 0000000000..c4ae6858a4 --- /dev/null +++ b/services/ui-src/src/measures/2024/APPCH/index.tsx @@ -0,0 +1,68 @@ +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { FormData } from "./types"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import { validationFunctions } from "./validation"; + +export const APPCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/APPCH/types.ts b/services/ui-src/src/measures/2024/APPCH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/APPCH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/APPCH/validation.ts b/services/ui-src/src/measures/2024/APPCH/validation.ts new file mode 100644 index 0000000000..8f38e08e4a --- /dev/null +++ b/services/ui-src/src/measures/2024/APPCH/validation.ts @@ -0,0 +1,78 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const APPCHValidation = (data: FormData) => { + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const dateRange = data[DC.DATE_RANGE]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + PMD.qualifiers, + PMD.categories + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + PMD.qualifiers + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, PMD.qualifiers), + ...GV.validateRateZeroPM( + performanceMeasureArray, + OPM, + PMD.qualifiers, + data + ), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateTotalNDR(performanceMeasureArray), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateOMSTotalNDR(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [APPCHValidation]; diff --git a/services/ui-src/src/measures/2024/BCSAD/data.ts b/services/ui-src/src/measures/2024/BCSAD/data.ts new file mode 100644 index 0000000000..e07f42f647 --- /dev/null +++ b/services/ui-src/src/measures/2024/BCSAD/data.ts @@ -0,0 +1,50 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; +import * as DC from "dataConstants"; + +export const { categories, qualifiers } = getCatQualLabels("BCS-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of women ages 50 to 74 who had a mammogram to screen for breast cancer.", + ], + questionListItems: [], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_CLINIC_DATA_SYSTEMS, + description: true, + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/BCSAD/index.test.tsx b/services/ui-src/src/measures/2024/BCSAD/index.test.tsx new file mode 100644 index 0000000000..51f66c364f --- /dev/null +++ b/services/ui-src/src/measures/2024/BCSAD/index.test.tsx @@ -0,0 +1,245 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "BCS-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 50 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 65 to 74", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/BCSAD/index.tsx b/services/ui-src/src/measures/2024/BCSAD/index.tsx new file mode 100644 index 0000000000..b54ce809b0 --- /dev/null +++ b/services/ui-src/src/measures/2024/BCSAD/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +import { FormData } from "./types"; + +export const BCSAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/BCSAD/types.ts b/services/ui-src/src/measures/2024/BCSAD/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/BCSAD/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/BCSAD/validation.ts b/services/ui-src/src/measures/2024/BCSAD/validation.ts new file mode 100644 index 0000000000..21d9d1e684 --- /dev/null +++ b/services/ui-src/src/measures/2024/BCSAD/validation.ts @@ -0,0 +1,81 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const BCSValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 1; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [BCSValidation]; diff --git a/services/ui-src/src/measures/2024/CBPAD/data.ts b/services/ui-src/src/measures/2024/CBPAD/data.ts new file mode 100644 index 0000000000..05edfd1841 --- /dev/null +++ b/services/ui-src/src/measures/2024/CBPAD/data.ts @@ -0,0 +1,74 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("CBP-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of beneficiaries ages 18 to 85 who had a diagnosis of hypertension and whose blood pressure (BP) was adequately controlled (< 140/90 mm Hg) during the measurement year.", + ], + questionListItems: [], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.HYBRID_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: "Medicaid Management Information System (MMIS)", + }, + { + value: "Other", + description: true, + }, + ], + }, + { + label: "What is the Medical Records Data Source?", + options: [ + { + value: DC.EHR_DATA, + }, + { + value: DC.PAPER, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/CBPAD/index.test.tsx b/services/ui-src/src/measures/2024/CBPAD/index.test.tsx new file mode 100644 index 0000000000..549fef742d --- /dev/null +++ b/services/ui-src/src/measures/2024/CBPAD/index.test.tsx @@ -0,0 +1,245 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "CBP-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 18 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 65 to 85", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/CBPAD/index.tsx b/services/ui-src/src/measures/2024/CBPAD/index.tsx new file mode 100644 index 0000000000..b1d1b2a9c4 --- /dev/null +++ b/services/ui-src/src/measures/2024/CBPAD/index.tsx @@ -0,0 +1,67 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as QMR from "components"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { FormData } from "./types"; + +export const CBPAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/CBPAD/types.ts b/services/ui-src/src/measures/2024/CBPAD/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/CBPAD/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/CBPAD/validation.ts b/services/ui-src/src/measures/2024/CBPAD/validation.ts new file mode 100644 index 0000000000..44711e825a --- /dev/null +++ b/services/ui-src/src/measures/2024/CBPAD/validation.ts @@ -0,0 +1,81 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const CBPValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 1; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator, + PMD.qualifiers[1].label + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + dataSource: data[DC.DATA_SOURCE], + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + ], + }), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [CBPValidation]; diff --git a/services/ui-src/src/measures/2024/CBPHH/data.ts b/services/ui-src/src/measures/2024/CBPHH/data.ts new file mode 100644 index 0000000000..87ee74b1c1 --- /dev/null +++ b/services/ui-src/src/measures/2024/CBPHH/data.ts @@ -0,0 +1,74 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("CBP-HH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of health home enrollees ages 18 to 85 who had a diagnosis of hypertension and whose blood pressure (BP) was adequately controlled (< 140/90 mm Hg) during the measurement year.", + ], + questionListItems: [], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.HYBRID_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: "Other", + description: true, + }, + ], + }, + { + label: "What is the Medical Records Data Source?", + options: [ + { + value: DC.EHR_DATA, + }, + { + value: DC.PAPER, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/CBPHH/index.test.tsx b/services/ui-src/src/measures/2024/CBPHH/index.test.tsx new file mode 100644 index 0000000000..5747d87aa2 --- /dev/null +++ b/services/ui-src/src/measures/2024/CBPHH/index.test.tsx @@ -0,0 +1,258 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "CBP-HH"; +const coreSet = "HHCS"; +const state = "DC"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/CBPHH/index.tsx b/services/ui-src/src/measures/2024/CBPHH/index.tsx new file mode 100644 index 0000000000..94b82d6cc2 --- /dev/null +++ b/services/ui-src/src/measures/2024/CBPHH/index.tsx @@ -0,0 +1,69 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as QMR from "components"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { FormData } from "./types"; + +export const CBPHH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/CBPHH/types.ts b/services/ui-src/src/measures/2024/CBPHH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/CBPHH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/CBPHH/validation.ts b/services/ui-src/src/measures/2024/CBPHH/validation.ts new file mode 100644 index 0000000000..43c084cb64 --- /dev/null +++ b/services/ui-src/src/measures/2024/CBPHH/validation.ts @@ -0,0 +1,83 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const CBPValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 1; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + + errorArray = [ + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator, + PMD.qualifiers[1].label + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateTotalNDR(performanceMeasureArray), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + dataSource: data[DC.DATA_SOURCE], + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + GV.validateOMSTotalNDR(), + ], + }), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [CBPValidation]; diff --git a/services/ui-src/src/measures/2024/CCPAD/data.ts b/services/ui-src/src/measures/2024/CCPAD/data.ts new file mode 100644 index 0000000000..3aa27c9d30 --- /dev/null +++ b/services/ui-src/src/measures/2024/CCPAD/data.ts @@ -0,0 +1,16 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("CCP-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Among women ages 21 to 44 who had a live birth, the percentage that:", + ], + questionListItems: [ + "Were provided a most effective or moderately effective method of contraception within 3 and 90 days of delivery", + "Were provided a long-acting reversible method of contraception (LARC) within 3 and 90 days of delivery", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/CCPAD/index.test.tsx b/services/ui-src/src/measures/2024/CCPAD/index.test.tsx new file mode 100644 index 0000000000..692883c69b --- /dev/null +++ b/services/ui-src/src/measures/2024/CCPAD/index.test.tsx @@ -0,0 +1,262 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "CCP-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + LongactingreversiblemethodofcontraceptionLARC: [ + { + label: "Three Days Postpartum Rate", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Sixty Days Postpartum Rate", + }, + ], + Mosteffectiveormoderatelyeffectivemethodofcontraception: [ + { + label: "Three Days Postpartum Rate", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ninety Days Postpartum Rate", + }, + ], + }, + }, + MeasurementSpecification: "OPA", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/CCPAD/index.tsx b/services/ui-src/src/measures/2024/CCPAD/index.tsx new file mode 100644 index 0000000000..88e3e037ff --- /dev/null +++ b/services/ui-src/src/measures/2024/CCPAD/index.tsx @@ -0,0 +1,68 @@ +import * as CMQ from "../shared/CommonQuestions"; +import * as QMR from "components"; +import { FormData } from "./types"; +import { useEffect } from "react"; +import { validationFunctions } from "./validation"; +import * as PMD from "./data"; +import { useFormContext } from "react-hook-form"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; + +export const CCPAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/CCPAD/types.ts b/services/ui-src/src/measures/2024/CCPAD/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/CCPAD/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/CCPAD/validation.ts b/services/ui-src/src/measures/2024/CCPAD/validation.ts new file mode 100644 index 0000000000..4f4cacb2b3 --- /dev/null +++ b/services/ui-src/src/measures/2024/CCPAD/validation.ts @@ -0,0 +1,79 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const CCPADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + // Performance Measure and OPM Validations + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateOneQualRateHigherThanOtherQualPM(data, PMD.data, 1, 0), + ...GV.validateEqualCategoryDenominatorsPM(data, PMD.categories), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD.data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + + // OMS Specific Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateEqualCategoryDenominatorsOMS(), + GV.validateOneQualRateHigherThanOtherQualOMS(1, 0), + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateOneCatRateHigherThanOtherCatOMS(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [CCPADValidation]; diff --git a/services/ui-src/src/measures/2024/CCPCH/data.ts b/services/ui-src/src/measures/2024/CCPCH/data.ts new file mode 100644 index 0000000000..09e194f746 --- /dev/null +++ b/services/ui-src/src/measures/2024/CCPCH/data.ts @@ -0,0 +1,16 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("CCP-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Among women ages 15 to 20 who had a live birth, the percentage that:", + ], + questionListItems: [ + "Were provided a most effective or moderately effective method of contraception within 3 and 90 days of delivery", + "Were provided a long-acting reversible method of contraception (LARC) within 3 and 90 days of delivery", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/CCPCH/index.test.tsx b/services/ui-src/src/measures/2024/CCPCH/index.test.tsx new file mode 100644 index 0000000000..f637c34fd5 --- /dev/null +++ b/services/ui-src/src/measures/2024/CCPCH/index.test.tsx @@ -0,0 +1,269 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "CCP-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + LongactingreversiblemethodofcontraceptionLARC: [ + { + label: "Three Days Postpartum Rate", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + { + label: "Sixty Days Postpartum Rate", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + ], + Mosteffectiveormoderatelyeffectivemethodofcontraception: [ + { + label: "Three Days Postpartum Rate", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + { + label: "Sixty Days Postpartum Rate", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + ], + }, + }, + MeasurementSpecification: "OPA", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/CCPCH/index.tsx b/services/ui-src/src/measures/2024/CCPCH/index.tsx new file mode 100644 index 0000000000..6ca15248b2 --- /dev/null +++ b/services/ui-src/src/measures/2024/CCPCH/index.tsx @@ -0,0 +1,69 @@ +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; + +import { FormData } from "./types"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import { validationFunctions } from "./validation"; + +export const CCPCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/CCPCH/types.ts b/services/ui-src/src/measures/2024/CCPCH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/CCPCH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/CCPCH/validation.ts b/services/ui-src/src/measures/2024/CCPCH/validation.ts new file mode 100644 index 0000000000..de36a55b8b --- /dev/null +++ b/services/ui-src/src/measures/2024/CCPCH/validation.ts @@ -0,0 +1,78 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const CCPCHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + // Performance Measure and OPM Validations + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateEqualCategoryDenominatorsPM(data, PMD.categories), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD.data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateOneQualRateHigherThanOtherQualPM(data, PMD.data, 1, 0), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + + // OMS Specific Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateEqualCategoryDenominatorsOMS(), + GV.validateOneQualRateHigherThanOtherQualOMS(1, 0), + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateOneCatRateHigherThanOtherCatOMS(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + ], + }), + ]; + return errorArray; +}; + +export const validationFunctions = [CCPCHValidation]; diff --git a/services/ui-src/src/measures/2024/CCSAD/data.ts b/services/ui-src/src/measures/2024/CCSAD/data.ts new file mode 100644 index 0000000000..031f219d16 --- /dev/null +++ b/services/ui-src/src/measures/2024/CCSAD/data.ts @@ -0,0 +1,79 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("CCS-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of women ages 21 to 64 who were screened for cervical cancer using either of the following criteria:", + ], + questionListItems: [ + "Women ages 21 to 64 who had cervical cytology performed within the last 3 years", + "Women ages 30 to 64 who had cervical high-risk human papillomavirus (hrHPV) testing performed within the last 5 years", + "Women ages 30 to 64 who had cervical cytology/high-risk human papillomavirus (hrHPV) cotesting within the last 5 years", + ], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.HYBRID_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + + { + value: DC.OTHER, + description: true, + }, + ], + }, + { + label: "What is the Medical Records Data Source?", + options: [ + { + value: DC.EHR_DATA, + }, + { + value: DC.PAPER, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/CCSAD/index.test.tsx b/services/ui-src/src/measures/2024/CCSAD/index.test.tsx new file mode 100644 index 0000000000..06b2dfc636 --- /dev/null +++ b/services/ui-src/src/measures/2024/CCSAD/index.test.tsx @@ -0,0 +1,239 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "CCS-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Percentage of women ages 21 to 64 screened", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/CCSAD/index.tsx b/services/ui-src/src/measures/2024/CCSAD/index.tsx new file mode 100644 index 0000000000..412be77735 --- /dev/null +++ b/services/ui-src/src/measures/2024/CCSAD/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { FormData } from "./types"; + +export const CCSAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/CCSAD/types.ts b/services/ui-src/src/measures/2024/CCSAD/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/CCSAD/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/CCSAD/validation.ts b/services/ui-src/src/measures/2024/CCSAD/validation.ts new file mode 100644 index 0000000000..d1a8b00983 --- /dev/null +++ b/services/ui-src/src/measures/2024/CCSAD/validation.ts @@ -0,0 +1,74 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const CCSADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = + GV.getPerfMeasureRateArray(data, PMD.data) ?? []; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + dataSource: data[DC.DATA_SOURCE], + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + ], + }), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ]; + + return errorArray; +}; + +export const validationFunctions = [CCSADValidation]; diff --git a/services/ui-src/src/measures/2024/CCWAD/data.ts b/services/ui-src/src/measures/2024/CCWAD/data.ts new file mode 100644 index 0000000000..cc1dc4839a --- /dev/null +++ b/services/ui-src/src/measures/2024/CCWAD/data.ts @@ -0,0 +1,16 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("CCW-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Among women ages 21 to 44 at risk of unintended pregnancy, the percentage that:", + ], + questionListItems: [ + "Were provided a most effective or moderately effective method of contraception", + "Were provided a long-acting reversible method of contraception (LARC)", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/CCWAD/index.test.tsx b/services/ui-src/src/measures/2024/CCWAD/index.test.tsx new file mode 100644 index 0000000000..857d1d838c --- /dev/null +++ b/services/ui-src/src/measures/2024/CCWAD/index.test.tsx @@ -0,0 +1,255 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "CCW-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + LongactingreversiblemethodofcontraceptionLARC: [ + { + label: "All Women Ages 21 to 44", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + Mosteffectiveormoderatelyeffectivemethodofcontraception: [ + { + label: "All Women Ages 21 to 44", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + }, + }, + MeasurementSpecification: "OPA", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/CCWAD/index.tsx b/services/ui-src/src/measures/2024/CCWAD/index.tsx new file mode 100644 index 0000000000..dc2dd2416b --- /dev/null +++ b/services/ui-src/src/measures/2024/CCWAD/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { FormData } from "./types"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; + +export const CCWAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/CCWAD/types.ts b/services/ui-src/src/measures/2024/CCWAD/types.ts new file mode 100644 index 0000000000..9e4666ee93 --- /dev/null +++ b/services/ui-src/src/measures/2024/CCWAD/types.ts @@ -0,0 +1,11 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type DeviationCheckBoxOptions = + | "moderate-method-deviation-Numerator" + | "moderate-method-deviation-Denominator" + | "moderate-method-deviation-Other" + | "reversible-method-deviation-Numerator" + | "reversible-method-deviation-Denominator" + | "reversible-method-deviation-Other"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/CCWAD/validation.ts b/services/ui-src/src/measures/2024/CCWAD/validation.ts new file mode 100644 index 0000000000..ca61493857 --- /dev/null +++ b/services/ui-src/src/measures/2024/CCWAD/validation.ts @@ -0,0 +1,76 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const CCWADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD), + ...GV.validateEqualCategoryDenominatorsPM(data, PMD.categories, ageGroups), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateOneQualRateHigherThanOtherQualPM(data, PMD), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateEqualCategoryDenominatorsOMS(), + GV.validateOneCatRateHigherThanOtherCatOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [CCWADValidation]; diff --git a/services/ui-src/src/measures/2024/CCWCH/data.ts b/services/ui-src/src/measures/2024/CCWCH/data.ts new file mode 100644 index 0000000000..ff2553830c --- /dev/null +++ b/services/ui-src/src/measures/2024/CCWCH/data.ts @@ -0,0 +1,16 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("CCW-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Among women ages 15 to 20 at risk of unintended pregnancy, the percentage that:", + ], + questionListItems: [ + "Were provided a most effective or moderately effective method of contraception", + "Were provided a long-acting reversible method of contraception (LARC)", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/CCWCH/index.test.tsx b/services/ui-src/src/measures/2024/CCWCH/index.test.tsx new file mode 100644 index 0000000000..2eb411f577 --- /dev/null +++ b/services/ui-src/src/measures/2024/CCWCH/index.test.tsx @@ -0,0 +1,268 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "CCW-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateOne); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + LongactingreversiblemethodofcontraceptionLARC: [ + { + label: "Three Days Postpartum Rate", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + { + label: "Sixty Days Postpartum Rate", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + ], + Mosteffectiveormoderatelyeffectivemethodofcontraception: [ + { + label: "Three Days Postpartum Rate", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + { + label: "Sixty Days Postpartum Rate", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + ], + }, + }, + MeasurementSpecification: "OPA", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/CCWCH/index.tsx b/services/ui-src/src/measures/2024/CCWCH/index.tsx new file mode 100644 index 0000000000..a8130403fa --- /dev/null +++ b/services/ui-src/src/measures/2024/CCWCH/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +import { FormData } from "./types"; + +export const CCWCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/CCWCH/types.ts b/services/ui-src/src/measures/2024/CCWCH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/CCWCH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/CCWCH/validation.ts b/services/ui-src/src/measures/2024/CCWCH/validation.ts new file mode 100644 index 0000000000..d0de747193 --- /dev/null +++ b/services/ui-src/src/measures/2024/CCWCH/validation.ts @@ -0,0 +1,74 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const CCWCHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateOneQualRateHigherThanOtherQualPM(data, PMD), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateEqualCategoryDenominatorsOMS(), + GV.validateOneQualRateHigherThanOtherQualOMS(), + ], + }), + ...GV.validateEqualCategoryDenominatorsPM(data, PMD.categories, ageGroups), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [CCWCHValidation]; diff --git a/services/ui-src/src/measures/2024/CDFAD/data.ts b/services/ui-src/src/measures/2024/CDFAD/data.ts new file mode 100644 index 0000000000..f0eefd7bc3 --- /dev/null +++ b/services/ui-src/src/measures/2024/CDFAD/data.ts @@ -0,0 +1,46 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("CDF-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of beneficiaries age 18 and older screened for depression on the date of the encounter or 14 days prior to the date of the encounter using an age-appropriate standardized depression screening tool, and if positive, a follow-up plan is documented on the date of the eligible encounter.", + ], + questionListItems: [], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/CDFAD/index.test.tsx b/services/ui-src/src/measures/2024/CDFAD/index.test.tsx new file mode 100644 index 0000000000..0f63f6d763 --- /dev/null +++ b/services/ui-src/src/measures/2024/CDFAD/index.test.tsx @@ -0,0 +1,248 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "CDF-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 18 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Age 65 and older", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + }, + }, + MeasurementSpecification: "CMS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/CDFAD/index.tsx b/services/ui-src/src/measures/2024/CDFAD/index.tsx new file mode 100644 index 0000000000..c4895141b4 --- /dev/null +++ b/services/ui-src/src/measures/2024/CDFAD/index.tsx @@ -0,0 +1,67 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +import { FormData } from "./types"; + +export const CDFAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/CDFAD/types.ts b/services/ui-src/src/measures/2024/CDFAD/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/CDFAD/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/CDFAD/validation.ts b/services/ui-src/src/measures/2024/CDFAD/validation.ts new file mode 100644 index 0000000000..52ebd257c3 --- /dev/null +++ b/services/ui-src/src/measures/2024/CDFAD/validation.ts @@ -0,0 +1,79 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const CDFADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 1; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + + errorArray = [ + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [CDFADValidation]; diff --git a/services/ui-src/src/measures/2024/CDFCH/data.ts b/services/ui-src/src/measures/2024/CDFCH/data.ts new file mode 100644 index 0000000000..961d15e31c --- /dev/null +++ b/services/ui-src/src/measures/2024/CDFCH/data.ts @@ -0,0 +1,46 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("CDF-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of beneficiaries ages 12 to 17 screened for depression on the date of the encounter or 14 days prior to the date of the encounter using an age-appropriate standardized depression screening tool, and if positive, a follow-up plan is documented on the date of the eligible encounter.", + ], + questionListItems: [], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/CDFCH/index.test.tsx b/services/ui-src/src/measures/2024/CDFCH/index.test.tsx new file mode 100644 index 0000000000..28b32a92da --- /dev/null +++ b/services/ui-src/src/measures/2024/CDFCH/index.test.tsx @@ -0,0 +1,248 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "CDF-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateOne); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 12 to 17", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + }, + }, + MeasurementSpecification: "CMS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/CDFCH/index.tsx b/services/ui-src/src/measures/2024/CDFCH/index.tsx new file mode 100644 index 0000000000..5708d6388d --- /dev/null +++ b/services/ui-src/src/measures/2024/CDFCH/index.tsx @@ -0,0 +1,67 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +import { FormData } from "./types"; + +export const CDFCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/CDFCH/types.ts b/services/ui-src/src/measures/2024/CDFCH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/CDFCH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/CDFCH/validation.ts b/services/ui-src/src/measures/2024/CDFCH/validation.ts new file mode 100644 index 0000000000..b01b2a1833 --- /dev/null +++ b/services/ui-src/src/measures/2024/CDFCH/validation.ts @@ -0,0 +1,79 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const CDFCHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 1; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + + errorArray = [ + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [CDFCHValidation]; diff --git a/services/ui-src/src/measures/2024/CDFHH/data.ts b/services/ui-src/src/measures/2024/CDFHH/data.ts new file mode 100644 index 0000000000..463c5bd591 --- /dev/null +++ b/services/ui-src/src/measures/2024/CDFHH/data.ts @@ -0,0 +1,45 @@ +import * as DC from "dataConstants"; +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("CDF-HH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of health home enrollees age 12 and older screened for depression on the date of the encounter or 14 days prior to the date of the encounter using an age-appropriate standardized depression screening tool, and if positive, a follow-up plan is documented on the date of the eligible encounter.", + ], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/CDFHH/index.test.tsx b/services/ui-src/src/measures/2024/CDFHH/index.test.tsx new file mode 100644 index 0000000000..17efbfa03b --- /dev/null +++ b/services/ui-src/src/measures/2024/CDFHH/index.test.tsx @@ -0,0 +1,258 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "CDF-HH"; +const coreSet = "HHCS"; +const state = "DC"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/CDFHH/index.tsx b/services/ui-src/src/measures/2024/CDFHH/index.tsx new file mode 100644 index 0000000000..195ed35ce8 --- /dev/null +++ b/services/ui-src/src/measures/2024/CDFHH/index.tsx @@ -0,0 +1,75 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { FormData } from "./types"; + +export const CDFHH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && ( + + )} + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/CDFHH/types.ts b/services/ui-src/src/measures/2024/CDFHH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/CDFHH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/CDFHH/validation.ts b/services/ui-src/src/measures/2024/CDFHH/validation.ts new file mode 100644 index 0000000000..4174ade892 --- /dev/null +++ b/services/ui-src/src/measures/2024/CDFHH/validation.ts @@ -0,0 +1,89 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const CDFHHValidation = (data: FormData) => { + const definitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const OPM = data[DC.OPM_RATES]; + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const dateRange = data[DC.DATE_RANGE]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + let errorArray: any[] = []; + + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + const validateDualPopInformationArray = [ + performanceMeasureArray?.[0].filter((pm) => { + return pm?.label === "Age 65 and older"; + }), + ]; + + const age65PlusIndex = 0; + + errorArray = [ + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + + // Performance Measure Validations + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateDualPopInformationPM( + validateDualPopInformationArray, + OPM, + age65PlusIndex, + definitionOfDenominator + ), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateTotalNDR(performanceMeasureArray), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateOMSTotalNDR(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [CDFHHValidation]; diff --git a/services/ui-src/src/measures/2024/CHLAD/data.ts b/services/ui-src/src/measures/2024/CHLAD/data.ts new file mode 100644 index 0000000000..b190da3abc --- /dev/null +++ b/services/ui-src/src/measures/2024/CHLAD/data.ts @@ -0,0 +1,46 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("CHL-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of women ages 21 to 24 who were identified as sexually active and who had at least one test for chlamydia during the measurement year.", + ], + questionListItems: [], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/CHLAD/index.test.tsx b/services/ui-src/src/measures/2024/CHLAD/index.test.tsx new file mode 100644 index 0000000000..d7012d2650 --- /dev/null +++ b/services/ui-src/src/measures/2024/CHLAD/index.test.tsx @@ -0,0 +1,240 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "CHL-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 21 to 24", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + }, + }, + MeasurementSpecification: "CMS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/CHLAD/index.tsx b/services/ui-src/src/measures/2024/CHLAD/index.tsx new file mode 100644 index 0000000000..6f269408f9 --- /dev/null +++ b/services/ui-src/src/measures/2024/CHLAD/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +import { FormData } from "./types"; + +export const CHLAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/CHLAD/types.ts b/services/ui-src/src/measures/2024/CHLAD/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/CHLAD/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/CHLAD/validation.ts b/services/ui-src/src/measures/2024/CHLAD/validation.ts new file mode 100644 index 0000000000..95affdfb35 --- /dev/null +++ b/services/ui-src/src/measures/2024/CHLAD/validation.ts @@ -0,0 +1,72 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const CHLValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [CHLValidation]; diff --git a/services/ui-src/src/measures/2024/CHLCH/data.ts b/services/ui-src/src/measures/2024/CHLCH/data.ts new file mode 100644 index 0000000000..78e4d80c4f --- /dev/null +++ b/services/ui-src/src/measures/2024/CHLCH/data.ts @@ -0,0 +1,46 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("CHL-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of women ages 16 to 20 who were identified as sexually active and who had at least one test for chlamydia during the measurement year.", + ], + questionListItems: [], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/CHLCH/index.test.tsx b/services/ui-src/src/measures/2024/CHLCH/index.test.tsx new file mode 100644 index 0000000000..b23ab6cd45 --- /dev/null +++ b/services/ui-src/src/measures/2024/CHLCH/index.test.tsx @@ -0,0 +1,248 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "CHL-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateOne); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(33000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 16 to 20", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/CHLCH/index.tsx b/services/ui-src/src/measures/2024/CHLCH/index.tsx new file mode 100644 index 0000000000..abdbbb2090 --- /dev/null +++ b/services/ui-src/src/measures/2024/CHLCH/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +import { FormData } from "./types"; + +export const CHLCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/CHLCH/types.ts b/services/ui-src/src/measures/2024/CHLCH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/CHLCH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/CHLCH/validation.ts b/services/ui-src/src/measures/2024/CHLCH/validation.ts new file mode 100644 index 0000000000..d3ccfc8a25 --- /dev/null +++ b/services/ui-src/src/measures/2024/CHLCH/validation.ts @@ -0,0 +1,72 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const CHLValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateOPMRates(OPM), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [CHLValidation]; diff --git a/services/ui-src/src/measures/2024/CISCH/data.ts b/services/ui-src/src/measures/2024/CISCH/data.ts new file mode 100644 index 0000000000..740975dfaf --- /dev/null +++ b/services/ui-src/src/measures/2024/CISCH/data.ts @@ -0,0 +1,84 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("CIS-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of children age 2 who had four diphtheria, tetanus and acellular pertussis (DTaP); three polio (IPV); one measles, mumps and rubella (MMR); three haemophilus influenza type B (HiB); three hepatitis B (Hep B), one chicken pox (VZV); four pneumococcal conjugate (PCV); one hepatitis A (HepA); two or three rotavirus (RV); and two influenza (flu) vaccines by their second birthday. The measure calculates a rate for each vaccine and three separate combination rates.", + ], + questionListItems: [], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.IMMUNIZATION_REGISTRY_INFORMATION_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.HYBRID_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.IMMUNIZATION_REGISTRY_INFORMATION_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + { + label: "What is the Medical Records Data Source?", + options: [ + { + value: DC.EHR_DATA, + }, + { + value: DC.PAPER, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_CLINIC_DATA_SYSTEMS, + description: true, + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/CISCH/index.test.tsx b/services/ui-src/src/measures/2024/CISCH/index.test.tsx new file mode 100644 index 0000000000..907342c349 --- /dev/null +++ b/services/ui-src/src/measures/2024/CISCH/index.test.tsx @@ -0,0 +1,287 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "CIS-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(44000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "DTaP", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "IPV", + }, + { + label: "MMR", + }, + { + label: "HiB", + }, + { + label: "Hep B", + }, + { + label: "VZV", + }, + { + label: "PCV", + }, + { + label: "Hep A", + }, + { + label: "RV", + }, + { + label: "Flu", + }, + { + label: "Combo 3", + }, + { + label: "Combo 7", + }, + { + label: "Combo 10", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/CISCH/index.tsx b/services/ui-src/src/measures/2024/CISCH/index.tsx new file mode 100644 index 0000000000..5dbc39f0ed --- /dev/null +++ b/services/ui-src/src/measures/2024/CISCH/index.tsx @@ -0,0 +1,67 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { FormData } from "./types"; + +export const CISCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/CISCH/types.ts b/services/ui-src/src/measures/2024/CISCH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/CISCH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/CISCH/validation.ts b/services/ui-src/src/measures/2024/CISCH/validation.ts new file mode 100644 index 0000000000..f59c33df6e --- /dev/null +++ b/services/ui-src/src/measures/2024/CISCH/validation.ts @@ -0,0 +1,75 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const CISCHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = + GV.getPerfMeasureRateArray(data, PMD.data) ?? []; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + dataSource: data[DC.DATA_SOURCE], + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + ], + }), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateEqualCategoryDenominatorsPM(data, PMD.categories, ageGroups), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ]; + + return errorArray; +}; + +export const validationFunctions = [CISCHValidation]; diff --git a/services/ui-src/src/measures/2024/COBAD/data.ts b/services/ui-src/src/measures/2024/COBAD/data.ts new file mode 100644 index 0000000000..84a6399741 --- /dev/null +++ b/services/ui-src/src/measures/2024/COBAD/data.ts @@ -0,0 +1,13 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("COB-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of beneficiaries age 18 and older with concurrent use of prescription opioids and benzodiazepines. Beneficiaries with a cancer diagnosis, sickle cell disease diagnosis, or in hospice or palliative care are excluded.", + ], + questionListItems: [], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/COBAD/index.test.tsx b/services/ui-src/src/measures/2024/COBAD/index.test.tsx new file mode 100644 index 0000000000..ad01119e1b --- /dev/null +++ b/services/ui-src/src/measures/2024/COBAD/index.test.tsx @@ -0,0 +1,250 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "COB-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 18 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Age 65 and older", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + }, + }, + MeasurementSpecification: "PQA", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/COBAD/index.tsx b/services/ui-src/src/measures/2024/COBAD/index.tsx new file mode 100644 index 0000000000..d84bb6e790 --- /dev/null +++ b/services/ui-src/src/measures/2024/COBAD/index.tsx @@ -0,0 +1,67 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +import { FormData } from "./types"; + +export const COBAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/COBAD/types.ts b/services/ui-src/src/measures/2024/COBAD/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/COBAD/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/COBAD/validation.ts b/services/ui-src/src/measures/2024/COBAD/validation.ts new file mode 100644 index 0000000000..2fa9a374e4 --- /dev/null +++ b/services/ui-src/src/measures/2024/COBAD/validation.ts @@ -0,0 +1,82 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const COBADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 1; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator, + "Ages 65 to 85" + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + dataSource: data[DC.DATA_SOURCE], + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + GV.validateEqualQualifierDenominatorsOMS(), + ], + }), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [COBADValidation]; diff --git a/services/ui-src/src/measures/2024/COLAD/data.ts b/services/ui-src/src/measures/2024/COLAD/data.ts new file mode 100644 index 0000000000..23abb7471d --- /dev/null +++ b/services/ui-src/src/measures/2024/COLAD/data.ts @@ -0,0 +1,50 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("COL-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of beneficiaries ages 45 to 75 who had appropriate screening for colorectal cancer.", + ], + questionListItems: [], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_CLINIC_DATA_SYSTEMS, + description: true, + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/COLAD/index.test.tsx b/services/ui-src/src/measures/2024/COLAD/index.test.tsx new file mode 100644 index 0000000000..06203e5d9b --- /dev/null +++ b/services/ui-src/src/measures/2024/COLAD/index.test.tsx @@ -0,0 +1,248 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "COL-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 50 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 65 to 75", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/COLAD/index.tsx b/services/ui-src/src/measures/2024/COLAD/index.tsx new file mode 100644 index 0000000000..71ef21076d --- /dev/null +++ b/services/ui-src/src/measures/2024/COLAD/index.tsx @@ -0,0 +1,67 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +import { FormData } from "./types"; + +export const COLAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/COLAD/types.ts b/services/ui-src/src/measures/2024/COLAD/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/COLAD/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/COLAD/validation.ts b/services/ui-src/src/measures/2024/COLAD/validation.ts new file mode 100644 index 0000000000..fad29a3c74 --- /dev/null +++ b/services/ui-src/src/measures/2024/COLAD/validation.ts @@ -0,0 +1,84 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const COLADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 1; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const errorReplacementText = "Ages 65 to 75"; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator, + errorReplacementText + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + dataSource: data[DC.DATA_SOURCE], + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + GV.validateEqualQualifierDenominatorsOMS(), + ], + }), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [COLADValidation]; diff --git a/services/ui-src/src/measures/2024/COLHH/data.ts b/services/ui-src/src/measures/2024/COLHH/data.ts new file mode 100644 index 0000000000..ca3c402387 --- /dev/null +++ b/services/ui-src/src/measures/2024/COLHH/data.ts @@ -0,0 +1,49 @@ +import * as DC from "dataConstants"; +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("COL-HH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of health home enrollees ages 46 to 75 who had appropriate screening for colorectal cancer.", + ], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_CLINIC_DATA_SYSTEMS, + description: true, + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/COLHH/index.test.tsx b/services/ui-src/src/measures/2024/COLHH/index.test.tsx new file mode 100644 index 0000000000..e0a5438f0f --- /dev/null +++ b/services/ui-src/src/measures/2024/COLHH/index.test.tsx @@ -0,0 +1,258 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "COL-HH"; +const coreSet = "HHCS"; +const state = "DC"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/COLHH/index.tsx b/services/ui-src/src/measures/2024/COLHH/index.tsx new file mode 100644 index 0000000000..c39615c81b --- /dev/null +++ b/services/ui-src/src/measures/2024/COLHH/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +import { FormData } from "./types"; + +export const COLHH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/COLHH/types.ts b/services/ui-src/src/measures/2024/COLHH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/COLHH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/COLHH/validation.ts b/services/ui-src/src/measures/2024/COLHH/validation.ts new file mode 100644 index 0000000000..304a499a12 --- /dev/null +++ b/services/ui-src/src/measures/2024/COLHH/validation.ts @@ -0,0 +1,84 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const COLHHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 1; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const errorReplacementText = "Ages 65 to 75"; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator, + errorReplacementText + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateHedisYear(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateOPMRates(OPM), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + dataSource: data[DC.DATA_SOURCE], + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + GV.validateEqualQualifierDenominatorsOMS(), + ], + }), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [COLHHValidation]; diff --git a/services/ui-src/src/measures/2024/CPAAD/index.test.tsx b/services/ui-src/src/measures/2024/CPAAD/index.test.tsx new file mode 100644 index 0000000000..c74eae0357 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPAAD/index.test.tsx @@ -0,0 +1,158 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { clearMocks } from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "CPA-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.getByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Did you collect question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.getByText("Did you collect this measure?")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + expect( + screen.getByText( + "Which Supplemental Item Sets were included in the Survey" + ) + ).toBeInTheDocument(); + expect( + screen.getByText( + "Which administrative protocol was used to administer the survey?" + ) + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + expect( + screen.queryByText( + "Which Supplemental Item Sets were included in the Survey" + ) + ).not.toBeInTheDocument(); + expect( + screen.queryByText( + "Which administrative protocol was used to administer the survey?" + ) + ).not.toBeInTheDocument(); + expect( + screen.getByText("Why did you not collect this measure") + ).toBeInTheDocument(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidCollect: "no", +}; + +const completedMeasureData = { + MeasurementSpecification: "AHRQ-NCQA", + DidCollect: "yes", +}; diff --git a/services/ui-src/src/measures/2024/CPAAD/index.tsx b/services/ui-src/src/measures/2024/CPAAD/index.tsx new file mode 100644 index 0000000000..33dd613db9 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPAAD/index.tsx @@ -0,0 +1,43 @@ +import * as Q from "./questions"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import { useParams } from "react-router-dom"; +import * as QMR from "components"; +import { useFormContext } from "react-hook-form"; +import { FormData } from "./types"; +import { validationFunctions } from "./validation"; +import { useEffect } from "react"; + +export const CPAAD = ({ + name, + year, + setValidationFunctions, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const { coreSetId } = useParams(); + const data = watch(); + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + return ( + <> + + {data["DidCollect"] !== "no" && ( + <> + + + + + + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/CPAAD/questions/DataSource.tsx b/services/ui-src/src/measures/2024/CPAAD/questions/DataSource.tsx new file mode 100644 index 0000000000..b9ae0d7be2 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPAAD/questions/DataSource.tsx @@ -0,0 +1,95 @@ +import * as QMR from "components"; +import * as CUI from "@chakra-ui/react"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import { FormData } from "../types"; + +export const DataSource = () => { + const register = useCustomRegister(); + + return ( + + + Describe the data source ( + + text in this field is included in publicly-reported + state-specific comments + + ): + + } + {...register("DataSource-CAHPS-Version-Other")} + />, + ], + }, + ]} + /> + + Which Supplemental Item Sets were included in the Survey + + , + ], + }, + ]} + label="Select all that apply:" + /> + , + ], + }, + ]} + /> + + ); +}; diff --git a/services/ui-src/src/measures/2024/CPAAD/questions/DefinitionOfPopulation.tsx b/services/ui-src/src/measures/2024/CPAAD/questions/DefinitionOfPopulation.tsx new file mode 100644 index 0000000000..dbe7d5f963 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPAAD/questions/DefinitionOfPopulation.tsx @@ -0,0 +1,66 @@ +import * as QMR from "components"; +import * as CUI from "@chakra-ui/react"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import { FormData } from "../types"; + +export const DefinitionOfPopulation = () => { + const register = useCustomRegister(); + + return ( + + + Definition of population included in the survey sample + + + Please select all populations that are included. For example, if your + data include both non-dual Medicaid beneficiaries and Medicare and + Medicaid Dual Eligibles, select both: + + + Survey sample includes Medicaid population + + Survey sample includes Medicare and Medicaid Dually-Eligible + population + + + , + ], + }, + ]} + /> + + + ); +}; diff --git a/services/ui-src/src/measures/2024/CPAAD/questions/HowDidYouReport.tsx b/services/ui-src/src/measures/2024/CPAAD/questions/HowDidYouReport.tsx new file mode 100644 index 0000000000..afeb83a2c4 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPAAD/questions/HowDidYouReport.tsx @@ -0,0 +1,38 @@ +import * as QMR from "components"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import { FormData } from "../types"; + +export const HowDidYouReport = () => { + const register = useCustomRegister(); + return ( + + , + ], + }, + ]} + formLabelProps={{ fontWeight: "bold" }} + /> + + ); +}; diff --git a/services/ui-src/src/measures/2024/CPAAD/questions/PerformanceMeasure.tsx b/services/ui-src/src/measures/2024/CPAAD/questions/PerformanceMeasure.tsx new file mode 100644 index 0000000000..10408e9884 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPAAD/questions/PerformanceMeasure.tsx @@ -0,0 +1,16 @@ +import * as QMR from "components"; + +export const PerformanceMeasure = () => { + return ( + + This measure provides information on the experiences of beneficiaries with + their health care and gives a general indication of how well the health + care meets the beneficiaries’ expectations. Results summarize + beneficiaries’ experiences through ratings, composites, and question + summary rates. + + ); +}; diff --git a/services/ui-src/src/measures/2024/CPAAD/questions/Reporting.tsx b/services/ui-src/src/measures/2024/CPAAD/questions/Reporting.tsx new file mode 100644 index 0000000000..6fa4b9a0b8 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPAAD/questions/Reporting.tsx @@ -0,0 +1,41 @@ +import * as QMR from "components"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import { useFormContext } from "react-hook-form"; +import { WhyDidYouNotCollect } from "."; +import { FormData } from "../types"; + +interface Props { + measureName: string; + measureAbbreviation: string; + reportingYear: string; +} + +export const Reporting = ({ reportingYear }: Props) => { + const register = useCustomRegister(); + const { watch } = useFormContext(); + const watchRadioStatus = watch("DidCollect"); + + return ( + <> + + + + {watchRadioStatus?.includes("no") && } + + ); +}; diff --git a/services/ui-src/src/measures/2024/CPAAD/questions/WhyDidYouNotCollect.tsx b/services/ui-src/src/measures/2024/CPAAD/questions/WhyDidYouNotCollect.tsx new file mode 100644 index 0000000000..6e4ebc943b --- /dev/null +++ b/services/ui-src/src/measures/2024/CPAAD/questions/WhyDidYouNotCollect.tsx @@ -0,0 +1,192 @@ +import * as QMR from "components"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import { FormData } from "../types"; +import { useFlags } from "launchdarkly-react-client-sdk"; + +export const WhyDidYouNotCollect = () => { + const pheIsCurrent = useFlags()?.["periodOfHealthEmergency2024"]; + const register = useCustomRegister(); + return ( + + , + ], + }, + ]} + />, + ], + }, + { + displayValue: `Data not available`, + value: "DataNotAvailable", + children: [ + , + ], + }, + { + displayValue: "Data source not easily accessible", + value: "DataSourceNotEasilyAccessible", + children: [ + , + ], + }, + ]} + />, + ], + }, + { + displayValue: "Information not collected", + value: "InformationNotCollected", + children: [ + , + ], + }, + ]} + />, + ], + }, + { + displayValue: "Other", + value: "Other", + children: [ + , + ], + }, + ]} + />, + ], + }, + ...(pheIsCurrent + ? [ + { + displayValue: + "Limitations with data collection, reporting, or accuracy due to the COVID-19 pandemic", + value: "LimitationWithDatCollecitonReportAccuracyCovid", + children: [ + , + ], + }, + ] + : []), + { + displayValue: "Small sample size (less than 30)", + value: "SmallSampleSizeLessThan30", + children: [ + , + ], + }, + { + displayValue: "Other", + value: "Other", + children: [ + , + ], + }, + ]} + /> + + ); +}; diff --git a/services/ui-src/src/measures/2024/CPAAD/questions/index.tsx b/services/ui-src/src/measures/2024/CPAAD/questions/index.tsx new file mode 100644 index 0000000000..3d81873b8a --- /dev/null +++ b/services/ui-src/src/measures/2024/CPAAD/questions/index.tsx @@ -0,0 +1,6 @@ +export * from "./DataSource"; +export * from "./DefinitionOfPopulation"; +export * from "./HowDidYouReport"; +export * from "./PerformanceMeasure"; +export * from "./Reporting"; +export * from "./WhyDidYouNotCollect"; diff --git a/services/ui-src/src/measures/2024/CPAAD/types.ts b/services/ui-src/src/measures/2024/CPAAD/types.ts new file mode 100644 index 0000000000..2f54f9053b --- /dev/null +++ b/services/ui-src/src/measures/2024/CPAAD/types.ts @@ -0,0 +1,39 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export interface FormData + extends Types.AdditionalNotes, + Types.MeasurementSpecification, + Types.DidCollect { + //HowDidYouReport + HowDidYouReport: string[]; + "HowDidYouReport-Explanation": string; + + //DataSource + "DataSource-Included-ItemSets": string[]; + "DataSource-Included-ItemSets-Other": string; + "DataSource-CAHPS-Version": string; + "DataSource-CAHPS-Version-Other": string; + "DataSource-Admin-Protocol": string; + "DataSource-Admin-Protocol-Other": string; + + //WhyDidYouNotCollect + WhyDidYouNotCollect: string[]; + AmountOfPopulationNotCovered: string; + PopulationNotCovered: string; + PartialPopulationNotCoveredExplanation: string; + WhyIsDataNotAvailable: string; + "WhyIsDataNotAvailable-Other": string; + DataInconsistenciesAccuracyIssues: string; + DataSourceNotEasilyAccessible: string; + "DataSourceNotEasilyAccessible-Other": string; + InformationNotCollected: string; + "InformationNotCollected-Other": string; + LimitationWithDatCollecitonReportAccuracyCovid: string; + SmallSampleSizeLessThan30: string; + "WhyDidYouNotCollect-Other": string; + + //DefinitionOfPopulation + DefinitionOfSurveySample: string[]; + "DefinitionOfSurveySample-Other": string; + "DefinitionOfSurveySample-Changes": string; +} diff --git a/services/ui-src/src/measures/2024/CPAAD/validation.ts b/services/ui-src/src/measures/2024/CPAAD/validation.ts new file mode 100644 index 0000000000..6e3919edd8 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPAAD/validation.ts @@ -0,0 +1,25 @@ +import { FormData } from "./types"; +import { validateReasonForNotReporting } from "measures/2024/shared/globalValidations"; +import * as DC from "dataConstants"; + +const CPAADValidation = (data: FormData) => { + let errorArray: any[] = []; + const whyDidYouNotCollect = data["WhyDidYouNotCollect"]; + + if (data["DidCollect"] === undefined) { + errorArray.push({ + errorLocation: "Did you collect this measure", + errorMessage: + "You must select at least one option for Did you collect this measure?", + }); + } + + if (data["DidCollect"] === DC.NO) { + errorArray = [...validateReasonForNotReporting(whyDidYouNotCollect, true)]; + return errorArray; + } + + return errorArray; +}; + +export const validationFunctions = [CPAADValidation]; diff --git a/services/ui-src/src/measures/2024/CPCCH/index.test.tsx b/services/ui-src/src/measures/2024/CPCCH/index.test.tsx new file mode 100644 index 0000000000..cd3562f317 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPCCH/index.test.tsx @@ -0,0 +1,158 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { clearMocks } from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "CPC-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.getByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Did you collect question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.getByText("Did you collect this measure?")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + expect( + screen.getByText( + "Which Supplemental Item Sets were included in the Survey" + ) + ).toBeInTheDocument(); + expect( + screen.getByText( + "Which administrative protocol was used to administer the survey?" + ) + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + expect( + screen.queryByText( + "Which Supplemental Item Sets were included in the Survey" + ) + ).not.toBeInTheDocument(); + expect( + screen.queryByText( + "Which administrative protocol was used to administer the survey?" + ) + ).not.toBeInTheDocument(); + expect( + screen.getByText("Why did you not collect this measure") + ).toBeInTheDocument(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidCollect: "no", +}; + +const completedMeasureData = { + MeasurementSpecification: "AHRQ-NCQA", + DidCollect: "yes", +}; diff --git a/services/ui-src/src/measures/2024/CPCCH/index.tsx b/services/ui-src/src/measures/2024/CPCCH/index.tsx new file mode 100644 index 0000000000..b3e5829273 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPCCH/index.tsx @@ -0,0 +1,44 @@ +import * as Q from "./questions"; +import * as QMR from "components"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import { useParams } from "react-router-dom"; +import * as Types from "measures/2024/shared/CommonQuestions/types"; +import { useFormContext } from "react-hook-form"; +import { validationFunctions } from "./validation"; +import { useEffect } from "react"; + +export const CPCCH = ({ + name, + year, + setValidationFunctions, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const { coreSetId } = useParams(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + return ( + <> + + {data["DidCollect"] !== "no" && ( + <> + + + + + + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/CPCCH/questions/DataSource.tsx b/services/ui-src/src/measures/2024/CPCCH/questions/DataSource.tsx new file mode 100644 index 0000000000..733718fced --- /dev/null +++ b/services/ui-src/src/measures/2024/CPCCH/questions/DataSource.tsx @@ -0,0 +1,86 @@ +import * as QMR from "components"; +import * as CUI from "@chakra-ui/react"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import { FormData } from "../types"; + +export const DataSource = () => { + const register = useCustomRegister(); + + return ( + + , + ], + }, + ]} + /> + + Which Supplemental Item Sets were included in the Survey + + , + ], + }, + ]} + label="Select all that apply:" + /> + , + ], + }, + ]} + /> + + ); +}; diff --git a/services/ui-src/src/measures/2024/CPCCH/questions/DefinitionOfPopulation.tsx b/services/ui-src/src/measures/2024/CPCCH/questions/DefinitionOfPopulation.tsx new file mode 100644 index 0000000000..9c8c01ad12 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPCCH/questions/DefinitionOfPopulation.tsx @@ -0,0 +1,49 @@ +import * as QMR from "components"; +import * as CUI from "@chakra-ui/react"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import { FormData } from "../types"; + +export const DefinitionOfPopulation = () => { + const register = useCustomRegister(); + + return ( + + + Definition of population included in the survey sample + + + + + ); +}; diff --git a/services/ui-src/src/measures/2024/CPCCH/questions/HowDidYouReport.tsx b/services/ui-src/src/measures/2024/CPCCH/questions/HowDidYouReport.tsx new file mode 100644 index 0000000000..afeb83a2c4 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPCCH/questions/HowDidYouReport.tsx @@ -0,0 +1,38 @@ +import * as QMR from "components"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import { FormData } from "../types"; + +export const HowDidYouReport = () => { + const register = useCustomRegister(); + return ( + + , + ], + }, + ]} + formLabelProps={{ fontWeight: "bold" }} + /> + + ); +}; diff --git a/services/ui-src/src/measures/2024/CPCCH/questions/PerformanceMeasure.tsx b/services/ui-src/src/measures/2024/CPCCH/questions/PerformanceMeasure.tsx new file mode 100644 index 0000000000..b3d4c84430 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPCCH/questions/PerformanceMeasure.tsx @@ -0,0 +1,22 @@ +import * as QMR from "components"; +import * as CUI from "@chakra-ui/react"; + +export const PerformanceMeasure = () => { + return ( + + + This measure provides information on parents’ experiences with their + child’s health care. Results summarize children’s experiences through + ratings, composites, and individual question summary rates. + + + The Children with Chronic Conditions Supplemental Items provides + information on parents’ experience with their child’s health care for + the population of children with chronic conditions. + + + ); +}; diff --git a/services/ui-src/src/measures/2024/CPCCH/questions/Reporting.tsx b/services/ui-src/src/measures/2024/CPCCH/questions/Reporting.tsx new file mode 100644 index 0000000000..c0cea60ced --- /dev/null +++ b/services/ui-src/src/measures/2024/CPCCH/questions/Reporting.tsx @@ -0,0 +1,41 @@ +import * as QMR from "components"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import { useFormContext } from "react-hook-form"; +import { WhyDidYouNotCollect } from "."; +import { FormData } from "../types"; + +interface Props { + measureName: string; + measureAbbreviation: string; + reportingYear: string; +} + +export const Reporting = ({ reportingYear }: Props) => { + const register = useCustomRegister(); + const { watch } = useFormContext(); + const watchRadioStatus = watch("DidCollect"); + + return ( + <> + + + + {watchRadioStatus?.includes("no") && } + + ); +}; diff --git a/services/ui-src/src/measures/2024/CPCCH/questions/WhyDidYouNotCollect.tsx b/services/ui-src/src/measures/2024/CPCCH/questions/WhyDidYouNotCollect.tsx new file mode 100644 index 0000000000..ab32cd24c5 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPCCH/questions/WhyDidYouNotCollect.tsx @@ -0,0 +1,184 @@ +import * as QMR from "components"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import { FormData } from "../types"; + +export const WhyDidYouNotCollect = () => { + const register = useCustomRegister(); + return ( + + , + ], + }, + ]} + />, + ], + }, + { + displayValue: `Data not available`, + value: "DataNotAvailable", + children: [ + , + ], + }, + { + displayValue: "Data source not easily accessible", + value: "DataSourceNotEasilyAccessible", + children: [ + , + ], + }, + ]} + />, + ], + }, + { + displayValue: "Information not collected", + value: "InformationNotCollected", + children: [ + , + ], + }, + ]} + />, + ], + }, + { + displayValue: "Other", + value: "Other", + children: [ + , + ], + }, + ]} + />, + ], + }, + { + displayValue: + "Limitations with data collection, reporting, or accuracy due to the COVID-19 pandemic", + value: "LimitationWithDatCollecitonReportAccuracyCovid", + children: [ + , + ], + }, + { + displayValue: "Small sample size (less than 30)", + value: "SmallSampleSizeLessThan30", + children: [ + , + ], + }, + { + displayValue: "Other", + value: "Other", + children: [ + , + ], + }, + ]} + /> + + ); +}; diff --git a/services/ui-src/src/measures/2024/CPCCH/questions/index.tsx b/services/ui-src/src/measures/2024/CPCCH/questions/index.tsx new file mode 100644 index 0000000000..3d81873b8a --- /dev/null +++ b/services/ui-src/src/measures/2024/CPCCH/questions/index.tsx @@ -0,0 +1,6 @@ +export * from "./DataSource"; +export * from "./DefinitionOfPopulation"; +export * from "./HowDidYouReport"; +export * from "./PerformanceMeasure"; +export * from "./Reporting"; +export * from "./WhyDidYouNotCollect"; diff --git a/services/ui-src/src/measures/2024/CPCCH/types.ts b/services/ui-src/src/measures/2024/CPCCH/types.ts new file mode 100644 index 0000000000..2f54f9053b --- /dev/null +++ b/services/ui-src/src/measures/2024/CPCCH/types.ts @@ -0,0 +1,39 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export interface FormData + extends Types.AdditionalNotes, + Types.MeasurementSpecification, + Types.DidCollect { + //HowDidYouReport + HowDidYouReport: string[]; + "HowDidYouReport-Explanation": string; + + //DataSource + "DataSource-Included-ItemSets": string[]; + "DataSource-Included-ItemSets-Other": string; + "DataSource-CAHPS-Version": string; + "DataSource-CAHPS-Version-Other": string; + "DataSource-Admin-Protocol": string; + "DataSource-Admin-Protocol-Other": string; + + //WhyDidYouNotCollect + WhyDidYouNotCollect: string[]; + AmountOfPopulationNotCovered: string; + PopulationNotCovered: string; + PartialPopulationNotCoveredExplanation: string; + WhyIsDataNotAvailable: string; + "WhyIsDataNotAvailable-Other": string; + DataInconsistenciesAccuracyIssues: string; + DataSourceNotEasilyAccessible: string; + "DataSourceNotEasilyAccessible-Other": string; + InformationNotCollected: string; + "InformationNotCollected-Other": string; + LimitationWithDatCollecitonReportAccuracyCovid: string; + SmallSampleSizeLessThan30: string; + "WhyDidYouNotCollect-Other": string; + + //DefinitionOfPopulation + DefinitionOfSurveySample: string[]; + "DefinitionOfSurveySample-Other": string; + "DefinitionOfSurveySample-Changes": string; +} diff --git a/services/ui-src/src/measures/2024/CPCCH/validation.ts b/services/ui-src/src/measures/2024/CPCCH/validation.ts new file mode 100644 index 0000000000..57faa3c97f --- /dev/null +++ b/services/ui-src/src/measures/2024/CPCCH/validation.ts @@ -0,0 +1,25 @@ +import { FormData } from "./types"; +import { validateReasonForNotReporting } from "measures/2024/shared/globalValidations"; +import * as DC from "dataConstants"; + +const CPCCHValidation = (data: FormData) => { + let errorArray: any[] = []; + const whyDidYouNotCollect = data["WhyDidYouNotCollect"]; + + if (data["DidCollect"] === undefined) { + errorArray.push({ + errorLocation: "Did you collect this measure", + errorMessage: + "You must select at least one option for Did you collect this measure?", + }); + } + + if (data["DidCollect"] === DC.NO) { + errorArray = [...validateReasonForNotReporting(whyDidYouNotCollect, true)]; + return errorArray; + } + + return errorArray; +}; + +export const validationFunctions = [CPCCHValidation]; diff --git a/services/ui-src/src/measures/2024/CPUAD/data.ts b/services/ui-src/src/measures/2024/CPUAD/data.ts new file mode 100644 index 0000000000..084dcfcf13 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPUAD/data.ts @@ -0,0 +1,45 @@ +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; +import { DataDrivenTypes } from "../shared/CommonQuestions/types"; + +export const { categories, qualifiers } = getCatQualLabels("CPU-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of beneficiaries receiving long-term services and supports (LTSS) services ages 18 and older who have documentation of a comprehensive long-term services and supports (LTSS) care plan in a specified time frame that includes core elements. The following rates are reported:", + ], + questionListItems: [ + "Care Plan with Core Elements Documented. Beneficiaries who had a comprehensive LTSS care plan with 9 core elements documented within 120 days of enrollment (for new beneficiaries) or during the measurement year (for established beneficiaries).", + "Care Plan with Supplemental Elements Documented. Beneficiaries who had a comprehensive LTSS care plan with 9 core elements and at least 4 supplemental elements documented within 120 days of enrollment (for new beneficiaries) or during the measurement year (for established beneficiaries).", + ], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If Reporting entities (e.g. health plans) used different data sources, please select all applicable data sources used below", + options: [ + { + value: DC.CASE_MANAGEMENT_RECORD_REVIEW, + description: false, + subOptions: [ + { + label: "What is the case management record data source?", + options: [ + { + value: DC.EHR_DATA, + }, + { + value: DC.PAPER, + }, + ], + }, + ], + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/CPUAD/index.test.tsx b/services/ui-src/src/measures/2024/CPUAD/index.test.tsx new file mode 100644 index 0000000000..941b029ae9 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPUAD/index.test.tsx @@ -0,0 +1,253 @@ +import { act, fireEvent, screen, waitFor } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "CPU-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + it("should not allow non state users to edit forms by disabling buttons", async () => { + useApiMock(apiData); + renderWithHookForm(component); + + expect(screen.getByTestId("measure-wrapper-form")).toBeInTheDocument(); + const completeButton = screen.getByText("Complete Measure"); + fireEvent.click(completeButton); + expect(completeButton).toHaveAttribute("disabled"); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Care Plan with Core Elements Documented", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Care Plan with Supplemental Elements Documented", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/CPUAD/index.tsx b/services/ui-src/src/measures/2024/CPUAD/index.tsx new file mode 100644 index 0000000000..8f27cb4edf --- /dev/null +++ b/services/ui-src/src/measures/2024/CPUAD/index.tsx @@ -0,0 +1,54 @@ +import * as PMD from "./data"; +import * as QMR from "components"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import { useEffect } from "react"; +import { validationFunctions } from "./validation"; + +export const CPUAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + isOtherMeasureSpecSelected, + showOptionalMeasureStrat, +}: QMR.MeasureWrapperProps) => { + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && ( + + )} + + {showOptionalMeasureStrat && } + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/CPUAD/types.ts b/services/ui-src/src/measures/2024/CPUAD/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/CPUAD/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/CPUAD/validation.ts b/services/ui-src/src/measures/2024/CPUAD/validation.ts new file mode 100644 index 0000000000..65d1fc29b3 --- /dev/null +++ b/services/ui-src/src/measures/2024/CPUAD/validation.ts @@ -0,0 +1,60 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; + +const CPUADValidation = (data: FormData) => { + const carePlans = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + + let errorArray: any[] = []; + const OPM = data[DC.OPM_RATES]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const dateRange = data[DC.DATE_RANGE]; + + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + carePlans, + PMD.categories + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + carePlans + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, carePlans), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, carePlans, data), + ...GV.validateEqualCategoryDenominatorsPM(data, PMD.categories, carePlans), + ...GV.validateOneQualRateHigherThanOtherQualPM(data, PMD), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [CPUADValidation]; diff --git a/services/ui-src/src/measures/2024/DEVCH/data.ts b/services/ui-src/src/measures/2024/DEVCH/data.ts new file mode 100644 index 0000000000..0798cb1281 --- /dev/null +++ b/services/ui-src/src/measures/2024/DEVCH/data.ts @@ -0,0 +1,69 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("DEV-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of children screened for risk of developmental, behavioral, and social delays using a standardized screening tool in the 12 months preceding or on their first, second, or third birthday.", + ], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.HYBRID_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.OTHER, + description: true, + }, + ], + }, + { + label: "What is the Medical Records Data Source?", + options: [ + { + value: DC.EHR_DATA, + }, + { + value: DC.PAPER, + }, + ], + }, + ], + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/DEVCH/index.test.tsx b/services/ui-src/src/measures/2024/DEVCH/index.test.tsx new file mode 100644 index 0000000000..3cff2c8e8f --- /dev/null +++ b/services/ui-src/src/measures/2024/DEVCH/index.test.tsx @@ -0,0 +1,268 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "DEV-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(33000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Children screened by 12 months of age", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Children screened by 24 months of age", + }, + { + label: "Children screened by 36 months of age", + }, + { + label: "Children ", + isTotal: true, + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + }, + }, + MeasurementSpecification: "OHSU", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/DEVCH/index.tsx b/services/ui-src/src/measures/2024/DEVCH/index.tsx new file mode 100644 index 0000000000..e676aa8d06 --- /dev/null +++ b/services/ui-src/src/measures/2024/DEVCH/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +import { FormData } from "./types"; + +export const DEVCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/DEVCH/types.ts b/services/ui-src/src/measures/2024/DEVCH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/DEVCH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/DEVCH/validation.ts b/services/ui-src/src/measures/2024/DEVCH/validation.ts new file mode 100644 index 0000000000..c7d16aa1f0 --- /dev/null +++ b/services/ui-src/src/measures/2024/DEVCH/validation.ts @@ -0,0 +1,75 @@ +import * as PMD from "./data"; +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const DEVCHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + dataSource: data[DC.DATA_SOURCE], + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + customTotalLabel: "Children", + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateOneCatRateHigherThanOtherCatOMS(), + GV.validateOMSTotalNDR(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateTotalNDR(performanceMeasureArray), + ]; + + return errorArray; +}; + +export const validationFunctions = [DEVCHValidation]; diff --git a/services/ui-src/src/measures/2024/FUAAD/data.ts b/services/ui-src/src/measures/2024/FUAAD/data.ts new file mode 100644 index 0000000000..8afd9f589c --- /dev/null +++ b/services/ui-src/src/measures/2024/FUAAD/data.ts @@ -0,0 +1,16 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("FUA-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of emergency department (ED) visits for beneficiaries age 18 and Older with a principal diagnosis of Substance Use Disorder(SUD), or any diagnosis of drug overdose, for which there was follow-up. Two rates are reported:", + ], + questionListItems: [ + "Percentage of ED visits for which the beneficiary received follow-up within 30 days of the ED visit (31 total days)", + "Percentage of ED visits for which the beneficiary received follow-up within 7 days of the ED visit (8 total days)", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/FUAAD/index.test.tsx b/services/ui-src/src/measures/2024/FUAAD/index.test.tsx new file mode 100644 index 0000000000..5215cd48cf --- /dev/null +++ b/services/ui-src/src/measures/2024/FUAAD/index.test.tsx @@ -0,0 +1,268 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "FUA-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + Followupwithin7daysofEDvisit: [ + { + label: "Ages 18 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Age 65 and older", + rate: "100.0", + denominator: "55", + numerator: "55", + }, + ], + Followupwithin30daysofEDvisit: [ + { + label: "Ages 18 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Age 65 and older", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/FUAAD/index.tsx b/services/ui-src/src/measures/2024/FUAAD/index.tsx new file mode 100644 index 0000000000..d8b3fde9b5 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUAAD/index.tsx @@ -0,0 +1,67 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +import { FormData } from "./types"; + +export const FUAAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/FUAAD/types.ts b/services/ui-src/src/measures/2024/FUAAD/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/FUAAD/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/FUAAD/validation.ts b/services/ui-src/src/measures/2024/FUAAD/validation.ts new file mode 100644 index 0000000000..75222f3a19 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUAAD/validation.ts @@ -0,0 +1,87 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const FUAADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const sixtyDaysIndex = 1; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.validateEqualQualifierDenominatorsPM( + performanceMeasureArray, + ageGroups + ), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + sixtyDaysIndex, + DefinitionOfDenominator + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD.data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateOPMRates(OPM), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateEqualQualifierDenominatorsOMS(), + GV.validateOneCatRateHigherThanOtherCatOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [FUAADValidation]; diff --git a/services/ui-src/src/measures/2024/FUACH/data.ts b/services/ui-src/src/measures/2024/FUACH/data.ts new file mode 100644 index 0000000000..f3c075bbf0 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUACH/data.ts @@ -0,0 +1,16 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("FUA-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of emergency department (ED) visits for beneficiaries ages 13 to 17 years with a principal diagnosis of substance use disorder (SUD) or any diagnosis of drug overdose, for which there was follow-up. Two rates are reported:", + ], + questionListItems: [ + "Percentage of ED visits for which the beneficiary received follow-up within 30 days of the ED visit (31 total days)", + "Percentage of ED visits for which the beneficiary received follow-up within 7 days of the ED visit (8 total days)", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/FUACH/index.test.tsx b/services/ui-src/src/measures/2024/FUACH/index.test.tsx new file mode 100644 index 0000000000..52a9b00378 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUACH/index.test.tsx @@ -0,0 +1,268 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "FUA-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(33000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Children screened by 12 months of age", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Children screened by 24 months of age", + }, + { + label: "Children screened by 36 months of age", + }, + { + label: "Children ", + isTotal: true, + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + }, + }, + MeasurementSpecification: "OHSU", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/FUACH/index.tsx b/services/ui-src/src/measures/2024/FUACH/index.tsx new file mode 100644 index 0000000000..43f725d7c7 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUACH/index.tsx @@ -0,0 +1,67 @@ +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { FormData } from "./types"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import { validationFunctions } from "./validation"; + +export const FUACH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/FUACH/types.ts b/services/ui-src/src/measures/2024/FUACH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/FUACH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/FUACH/validation.ts b/services/ui-src/src/measures/2024/FUACH/validation.ts new file mode 100644 index 0000000000..51473eca93 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUACH/validation.ts @@ -0,0 +1,79 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { getPerfMeasureRateArray } from "../shared/globalValidations"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const FUACHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + if (data[DC.DID_REPORT] === DC.NO) { + return [...GV.validateReasonForNotReporting(whyNotReporting)]; + } + + let errorArray: any[] = [ + ...GV.validateEqualQualifierDenominatorsPM( + performanceMeasureArray, + ageGroups + ), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD.data), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateEqualQualifierDenominatorsOMS(), + GV.validateOneCatRateHigherThanOtherCatOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [FUACHValidation]; diff --git a/services/ui-src/src/measures/2024/FUAHH/data.ts b/services/ui-src/src/measures/2024/FUAHH/data.ts new file mode 100644 index 0000000000..906a7ceabc --- /dev/null +++ b/services/ui-src/src/measures/2024/FUAHH/data.ts @@ -0,0 +1,16 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("FUA-HH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of emergency department (ED) visits for health home enrollees age 13 and older with a principal diagnosis of substance use disorder (SUD), or any diagnosis of drug overdose, for which there was follow-up. Two rates are reported:", + ], + questionListItems: [ + "Percentage of ED visits for which the enrollee received follow-up within 30 days of the ED visit (31 total days)", + "Percentage of ED visits for which the enrollee received follow-up within 7 days of the ED visit (8 total days)", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/FUAHH/index.test.tsx b/services/ui-src/src/measures/2024/FUAHH/index.test.tsx new file mode 100644 index 0000000000..4fe51ad800 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUAHH/index.test.tsx @@ -0,0 +1,258 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "FUA-HH"; +const coreSet = "HHCS"; +const state = "DC"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/FUAHH/index.tsx b/services/ui-src/src/measures/2024/FUAHH/index.tsx new file mode 100644 index 0000000000..8a5d49cc02 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUAHH/index.tsx @@ -0,0 +1,69 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +import { FormData } from "./types"; + +export const FUAHH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/FUAHH/types.ts b/services/ui-src/src/measures/2024/FUAHH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/FUAHH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/FUAHH/validation.ts b/services/ui-src/src/measures/2024/FUAHH/validation.ts new file mode 100644 index 0000000000..264b02da4a --- /dev/null +++ b/services/ui-src/src/measures/2024/FUAHH/validation.ts @@ -0,0 +1,97 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const FUAHHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const dateRange = data[DC.DATE_RANGE]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const sixtyDaysIndex = 2; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + let sameDenominatorError = [ + ...GV.validateEqualQualifierDenominatorsPM( + performanceMeasureArray, + ageGroups + ), + ]; + sameDenominatorError = + sameDenominatorError.length > 0 ? [...sameDenominatorError] : []; + errorArray = [ + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + + ...GV.validateAtLeastOneDataSourceType(data), + // Performance Measure Validations + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + sixtyDaysIndex, + DefinitionOfDenominator + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...sameDenominatorError, + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateTotalNDR(performanceMeasureArray), + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD.data), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateEqualQualifierDenominatorsOMS(), + GV.validateOneCatRateHigherThanOtherCatOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateOMSTotalNDR(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [FUAHHValidation]; diff --git a/services/ui-src/src/measures/2024/FUHAD/data.ts b/services/ui-src/src/measures/2024/FUHAD/data.ts new file mode 100644 index 0000000000..65fa8059d2 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUHAD/data.ts @@ -0,0 +1,16 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("FUH-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "The percentage of discharges for beneficiaries age 18 and older who were hospitalized for treatment of selected mental illness or intentional self-harm diagnoses and who had a follow-up visit with a mental health provider. Two rates are reported:", + ], + questionListItems: [ + "Percentage of discharges for which the beneficiary received follow-up within 30 days after discharge", + "Percentage of discharges for which the beneficiary received follow-up within 7 days after discharge", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/FUHAD/index.test.tsx b/services/ui-src/src/measures/2024/FUHAD/index.test.tsx new file mode 100644 index 0000000000..8a374ef364 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUHAD/index.test.tsx @@ -0,0 +1,259 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "FUH-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + FollowUpwithin7daysafterdischarge: [ + { + label: "Ages 18 to 64", + }, + { + label: "Age 65 and older", + }, + ], + FollowUpwithin30daysafterdischarge: [ + { + label: "Ages 18 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Age 65 and older", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/FUHAD/index.tsx b/services/ui-src/src/measures/2024/FUHAD/index.tsx new file mode 100644 index 0000000000..3ccb39e36a --- /dev/null +++ b/services/ui-src/src/measures/2024/FUHAD/index.tsx @@ -0,0 +1,67 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +import { FormData } from "./types"; + +export const FUHAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/FUHAD/types.ts b/services/ui-src/src/measures/2024/FUHAD/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/FUHAD/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/FUHAD/validation.ts b/services/ui-src/src/measures/2024/FUHAD/validation.ts new file mode 100644 index 0000000000..658c68ef7d --- /dev/null +++ b/services/ui-src/src/measures/2024/FUHAD/validation.ts @@ -0,0 +1,102 @@ +import * as DC from "dataConstants"; +import * as PMD from "./data"; +import * as GV from "../shared/globalValidations"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const FUHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + let unfilteredSameDenominatorErrors: any[] = []; + for (let i = 0; i < performanceMeasureArray.length; i += 2) { + unfilteredSameDenominatorErrors = [ + ...unfilteredSameDenominatorErrors, + ...GV.validateEqualQualifierDenominatorsPM( + [performanceMeasureArray[i], performanceMeasureArray[i + 1]], + ageGroups + ), + ]; + } + + let filteredSameDenominatorErrors: any = []; + let errorList: string[] = []; + unfilteredSameDenominatorErrors.forEach((error) => { + if (!(errorList.indexOf(error.errorMessage) > -1)) { + errorList.push(error.errorMessage); + filteredSameDenominatorErrors.push(error); + } + }); + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + 1, + DefinitionOfDenominator + ), + ...filteredSameDenominatorErrors, + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateOneCatRateHigherThanOtherCatOMS(), + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateEqualQualifierDenominatorsOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [FUHValidation]; diff --git a/services/ui-src/src/measures/2024/FUHCH/data.ts b/services/ui-src/src/measures/2024/FUHCH/data.ts new file mode 100644 index 0000000000..c752385c1e --- /dev/null +++ b/services/ui-src/src/measures/2024/FUHCH/data.ts @@ -0,0 +1,16 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("FUH-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of discharges for beneficiaries ages 6 to 17 who were hospitalized for treatment of selected mental illness or intentional self-harm diagnoses and who had a follow-up visit with a mental health provider. Two rates are reported:", + ], + questionListItems: [ + "Percentage of discharges for which the beneficiary received follow-up within 30 days after discharge", + "Percentage of discharges for which the beneficiary received follow-up within 7 days after discharge", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/FUHCH/index.test.tsx b/services/ui-src/src/measures/2024/FUHCH/index.test.tsx new file mode 100644 index 0000000000..5c5d729699 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUHCH/index.test.tsx @@ -0,0 +1,260 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "FUH-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(33000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + FollowUpwithin7daysafterdischarge: [ + { + label: "Ages 6 to 17", + }, + ], + FollowUpwithin30daysafterdischarge: [ + { + label: "Ages 6 to 17", + rate: "100.0", + numerator: "65", + denominator: "65", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/FUHCH/index.tsx b/services/ui-src/src/measures/2024/FUHCH/index.tsx new file mode 100644 index 0000000000..287de00fed --- /dev/null +++ b/services/ui-src/src/measures/2024/FUHCH/index.tsx @@ -0,0 +1,67 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +import { FormData } from "./types"; + +export const FUHCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/FUHCH/types.ts b/services/ui-src/src/measures/2024/FUHCH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/FUHCH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/FUHCH/validation.ts b/services/ui-src/src/measures/2024/FUHCH/validation.ts new file mode 100644 index 0000000000..f24363e22c --- /dev/null +++ b/services/ui-src/src/measures/2024/FUHCH/validation.ts @@ -0,0 +1,95 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const FUHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + let unfilteredSameDenominatorErrors: any[] = []; + for (let i = 0; i < performanceMeasureArray.length; i += 2) { + unfilteredSameDenominatorErrors = [ + ...unfilteredSameDenominatorErrors, + ...GV.validateEqualQualifierDenominatorsPM( + [performanceMeasureArray[i], performanceMeasureArray[i + 1]], + ageGroups + ), + ]; + } + + let filteredSameDenominatorErrors: any = []; + let errorList: string[] = []; + unfilteredSameDenominatorErrors.forEach((error) => { + if (!(errorList.indexOf(error.errorMessage) > -1)) { + errorList.push(error.errorMessage); + filteredSameDenominatorErrors.push(error); + } + }); + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...filteredSameDenominatorErrors, + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateOneCatRateHigherThanOtherCatOMS(), + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateEqualQualifierDenominatorsOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [FUHValidation]; diff --git a/services/ui-src/src/measures/2024/FUHHH/data.ts b/services/ui-src/src/measures/2024/FUHHH/data.ts new file mode 100644 index 0000000000..53c559a8f0 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUHHH/data.ts @@ -0,0 +1,16 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("FUH-HH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of discharges for health home enrollees age 6 and older who were hospitalized for treatment of selected mental illness or intentional self-harm diagnoses and who had a follow-up visit with a mental health provider. Two rates are reported:", + ], + questionListItems: [ + "Percentage of discharges for which the enrollee received follow-up within 30 days after discharge", + "Percentage of discharges for which the enrollee received follow-up within 7 days after discharge", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/FUHHH/index.test.tsx b/services/ui-src/src/measures/2024/FUHHH/index.test.tsx new file mode 100644 index 0000000000..49d916afe9 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUHHH/index.test.tsx @@ -0,0 +1,258 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "FUH-HH"; +const coreSet = "HHCS"; +const state = "DC"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/FUHHH/index.tsx b/services/ui-src/src/measures/2024/FUHHH/index.tsx new file mode 100644 index 0000000000..bc1b42207f --- /dev/null +++ b/services/ui-src/src/measures/2024/FUHHH/index.tsx @@ -0,0 +1,69 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +import { FormData } from "./types"; + +export const FUHHH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/FUHHH/types.ts b/services/ui-src/src/measures/2024/FUHHH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/FUHHH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/FUHHH/validation.ts b/services/ui-src/src/measures/2024/FUHHH/validation.ts new file mode 100644 index 0000000000..4a766c72d3 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUHHH/validation.ts @@ -0,0 +1,96 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const FUHHHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const dateRange = data[DC.DATE_RANGE]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const sixtyDaysIndex = 2; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + let sameDenominatorError = [ + ...GV.validateEqualQualifierDenominatorsPM( + performanceMeasureArray, + ageGroups + ), + ]; + sameDenominatorError = + sameDenominatorError.length > 0 ? [...sameDenominatorError] : []; + errorArray = [ + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + // Performance Measure Validations + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + sixtyDaysIndex, + DefinitionOfDenominator + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...sameDenominatorError, + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateTotalNDR(performanceMeasureArray), + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD.data), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateEqualQualifierDenominatorsOMS(), + GV.validateOneCatRateHigherThanOtherCatOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateOMSTotalNDR(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [FUHHHValidation]; diff --git a/services/ui-src/src/measures/2024/FUMAD/data.ts b/services/ui-src/src/measures/2024/FUMAD/data.ts new file mode 100644 index 0000000000..52780d8c56 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUMAD/data.ts @@ -0,0 +1,16 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("FUM-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of emergency department (ED) visits for beneficiaries age 18 and Older with a principal diagnosis of mental illness or intentional self-harm and who had a follow-up visit for mental illness. Two rates are reported:", + ], + questionListItems: [ + "Percentage of ED visits for mental illness for which the beneficiary received follow-up within 30 days of the ED visit (31 total days)", + "Percentage of ED visits for mental illness for which the beneficiary received follow-up within 7 days of the ED visit (8 total days)", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/FUMAD/index.test.tsx b/services/ui-src/src/measures/2024/FUMAD/index.test.tsx new file mode 100644 index 0000000000..b12064245c --- /dev/null +++ b/services/ui-src/src/measures/2024/FUMAD/index.test.tsx @@ -0,0 +1,259 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "FUM-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + FollowUpwithin7daysafterdischarge: [ + { + label: "Ages 18 to 64", + }, + { + label: "Age 65 and older", + }, + ], + FollowUpwithin30daysafterdischarge: [ + { + label: "Ages 18 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Age 65 and older", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/FUMAD/index.tsx b/services/ui-src/src/measures/2024/FUMAD/index.tsx new file mode 100644 index 0000000000..b9fa7f3263 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUMAD/index.tsx @@ -0,0 +1,66 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as QMR from "components"; +import * as PMD from "./data"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { validationFunctions } from "./validation"; +import { FormData } from "./types"; + +export const FUMAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/FUMAD/types.ts b/services/ui-src/src/measures/2024/FUMAD/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/FUMAD/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/FUMAD/validation.ts b/services/ui-src/src/measures/2024/FUMAD/validation.ts new file mode 100644 index 0000000000..bc2cc2903a --- /dev/null +++ b/services/ui-src/src/measures/2024/FUMAD/validation.ts @@ -0,0 +1,93 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const FUMADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const sixtyDaysIndex = 1; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data["DidReport"] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + let sameDenominatorError = [ + ...GV.validateEqualQualifierDenominatorsPM( + performanceMeasureArray, + ageGroups + ), + ]; + sameDenominatorError = + sameDenominatorError.length > 0 ? [...sameDenominatorError] : []; + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + sixtyDaysIndex, + DefinitionOfDenominator + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...sameDenominatorError, + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateOPMRates(OPM), + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD.data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateEqualQualifierDenominatorsOMS(), + GV.validateOneCatRateHigherThanOtherCatOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [FUMADValidation]; diff --git a/services/ui-src/src/measures/2024/FUMCH/data.ts b/services/ui-src/src/measures/2024/FUMCH/data.ts new file mode 100644 index 0000000000..55c2196ee6 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUMCH/data.ts @@ -0,0 +1,16 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("FUM-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of emergency department (ED) visits for beneficiaries ages 6 to 17 with a principal diagnosis of mental illness or intentional self-harm and who had a follow-up visit for mental illness. Two rates are reported:", + ], + questionListItems: [ + "Percentage of ED visits for mental illness for which the beneficiary received follow-up within 30 days of the ED visit (31 total days)", + "Percentage of ED visits for mental illness for which the beneficiary received follow-up within 7 days of the ED visit (8 total days)", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/FUMCH/index.test.tsx b/services/ui-src/src/measures/2024/FUMCH/index.test.tsx new file mode 100644 index 0000000000..cfd005e78e --- /dev/null +++ b/services/ui-src/src/measures/2024/FUMCH/index.test.tsx @@ -0,0 +1,260 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "FUM-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(33000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + "30dayfollowupafterEDvisitformentalillness": [ + { + label: "Ages 6 to 17", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + "7dayfollowupafterEDvisitformentalillness": [ + { + label: "Ages 6 to 17", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/FUMCH/index.tsx b/services/ui-src/src/measures/2024/FUMCH/index.tsx new file mode 100644 index 0000000000..b8ff696023 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUMCH/index.tsx @@ -0,0 +1,67 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +import { FormData } from "./types"; + +export const FUMCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/FUMCH/types.ts b/services/ui-src/src/measures/2024/FUMCH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/FUMCH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/FUMCH/validation.ts b/services/ui-src/src/measures/2024/FUMCH/validation.ts new file mode 100644 index 0000000000..e999d68c67 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUMCH/validation.ts @@ -0,0 +1,94 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const FUMCHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + let unfilteredSameDenominatorErrors: any[] = []; + for (let i = 0; i < performanceMeasureArray.length; i += 2) { + unfilteredSameDenominatorErrors = [ + ...unfilteredSameDenominatorErrors, + ...GV.validateEqualQualifierDenominatorsPM( + [performanceMeasureArray[i], performanceMeasureArray[i + 1]], + ageGroups + ), + ]; + } + + let filteredSameDenominatorErrors: any = []; + let errorList: string[] = []; + unfilteredSameDenominatorErrors.forEach((error) => { + if (!(errorList.indexOf(error.errorMessage) > -1)) { + errorList.push(error.errorMessage); + filteredSameDenominatorErrors.push(error); + } + }); + + errorArray = [ + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...filteredSameDenominatorErrors, + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateOneCatRateHigherThanOtherCatOMS(), + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateEqualQualifierDenominatorsOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [FUMCHValidation]; diff --git a/services/ui-src/src/measures/2024/FUMHH/data.ts b/services/ui-src/src/measures/2024/FUMHH/data.ts new file mode 100644 index 0000000000..e9f0ffd5ca --- /dev/null +++ b/services/ui-src/src/measures/2024/FUMHH/data.ts @@ -0,0 +1,16 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("FUM-HH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of emergency department (ED) visits for health home enrollees age 6 and older with a principal diagnosis of mental illness or intentional self-harm and who had a follow-up visit for mental illness. Two rates are reported:", + ], + questionListItems: [ + "Percentage of ED visits for mental illness for which the enrollee received follow-up within 30 days of the ED visit (31 total days)", + "Percentage of ED visits for mental illness for which the enrollee received follow-up within 7 days of the ED visit (8 total days)", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/FUMHH/index.test.tsx b/services/ui-src/src/measures/2024/FUMHH/index.test.tsx new file mode 100644 index 0000000000..3db42b02ce --- /dev/null +++ b/services/ui-src/src/measures/2024/FUMHH/index.test.tsx @@ -0,0 +1,258 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "FUM-HH"; +const coreSet = "HHCS"; +const state = "DC"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/FUMHH/index.tsx b/services/ui-src/src/measures/2024/FUMHH/index.tsx new file mode 100644 index 0000000000..8c49bb25ed --- /dev/null +++ b/services/ui-src/src/measures/2024/FUMHH/index.tsx @@ -0,0 +1,72 @@ +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { FormData } from "./types"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import { validationFunctions } from "./validation"; + +export const FUMHH = ({ + isNotReportingData, + isOtherMeasureSpecSelected, + isPrimaryMeasureSpecSelected, + measureId, + name, + setValidationFunctions, + showOptionalMeasureStrat, + year, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && ( + + )} + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/FUMHH/types.ts b/services/ui-src/src/measures/2024/FUMHH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/FUMHH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/FUMHH/validation.ts b/services/ui-src/src/measures/2024/FUMHH/validation.ts new file mode 100644 index 0000000000..0092b08ad9 --- /dev/null +++ b/services/ui-src/src/measures/2024/FUMHH/validation.ts @@ -0,0 +1,99 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const FUMHHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const dateRange = data[DC.DATE_RANGE]; + const definitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + + let errorArray: any[] = []; + + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + const validateDualPopInformationArray = [ + performanceMeasureArray?.[0].filter((pm) => { + return pm?.label === "Age 65 and older"; + }), + ]; + + const age65PlusIndex = 0; + + errorArray = [ + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + + // Performance Measure Validations + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateDualPopInformationPM( + validateDualPopInformationArray, + OPM, + age65PlusIndex, + definitionOfDenominator + ), + ...GV.validateEqualQualifierDenominatorsPM( + performanceMeasureArray, + PMD.qualifiers + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD), + ...GV.validateTotalNDR(performanceMeasureArray), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateEqualQualifierDenominatorsOMS(), + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateOMSTotalNDR(), + GV.validateOneCatRateHigherThanOtherCatOMS(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [FUMHHValidation]; diff --git a/services/ui-src/src/measures/2024/FVAAD/data.ts b/services/ui-src/src/measures/2024/FVAAD/data.ts new file mode 100644 index 0000000000..bcf1dd1fb9 --- /dev/null +++ b/services/ui-src/src/measures/2024/FVAAD/data.ts @@ -0,0 +1,13 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("FVA-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of beneficiaries ages 18 to 64 who received a flu vaccination between July 1 of the measurement year and the date when the CAHPS 5.1H Adult Survey was completed.", + ], + questionListItems: [], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/FVAAD/index.test.tsx b/services/ui-src/src/measures/2024/FVAAD/index.test.tsx new file mode 100644 index 0000000000..b6d44e1e57 --- /dev/null +++ b/services/ui-src/src/measures/2024/FVAAD/index.test.tsx @@ -0,0 +1,246 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "FVA-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 18 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/FVAAD/index.tsx b/services/ui-src/src/measures/2024/FVAAD/index.tsx new file mode 100644 index 0000000000..6990ff8b57 --- /dev/null +++ b/services/ui-src/src/measures/2024/FVAAD/index.tsx @@ -0,0 +1,53 @@ +import { useEffect } from "react"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import * as QMR from "components"; + +export const FVAAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && } + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/FVAAD/types.ts b/services/ui-src/src/measures/2024/FVAAD/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/FVAAD/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/FVAAD/validation.ts b/services/ui-src/src/measures/2024/FVAAD/validation.ts new file mode 100644 index 0000000000..19239c9d62 --- /dev/null +++ b/services/ui-src/src/measures/2024/FVAAD/validation.ts @@ -0,0 +1,75 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const FVAADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + let errorArray: any[] = []; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [FVAADValidation]; diff --git a/services/ui-src/src/measures/2024/HBDAD/data.ts b/services/ui-src/src/measures/2024/HBDAD/data.ts new file mode 100644 index 0000000000..79fb8e3a7d --- /dev/null +++ b/services/ui-src/src/measures/2024/HBDAD/data.ts @@ -0,0 +1,74 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("HBD-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of beneficiaries ages 18 to 75 with diabetes (type 1 and type 2) whose hemoglobin A1c (HbA1c) was at the following levels during the measurement year:", + ], + questionListItems: ["HbA1c control (<8.0%)", "HbA1c poor control (>9.0%)"], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.HYBRID_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.OTHER, + description: true, + }, + ], + }, + { + label: "What is the Medical Records Data Source?", + options: [ + { + value: DC.EHR_DATA, + }, + { + value: DC.PAPER, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/HBDAD/index.test.tsx b/services/ui-src/src/measures/2024/HBDAD/index.test.tsx new file mode 100644 index 0000000000..1c4fcf4271 --- /dev/null +++ b/services/ui-src/src/measures/2024/HBDAD/index.test.tsx @@ -0,0 +1,243 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "HBD-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 18 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 65 to 75", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/HBDAD/index.tsx b/services/ui-src/src/measures/2024/HBDAD/index.tsx new file mode 100644 index 0000000000..14413340f1 --- /dev/null +++ b/services/ui-src/src/measures/2024/HBDAD/index.tsx @@ -0,0 +1,67 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { FormData } from "./types"; + +export const HBDAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/HBDAD/types.ts b/services/ui-src/src/measures/2024/HBDAD/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/HBDAD/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/HBDAD/validation.ts b/services/ui-src/src/measures/2024/HBDAD/validation.ts new file mode 100644 index 0000000000..d8e07447fa --- /dev/null +++ b/services/ui-src/src/measures/2024/HBDAD/validation.ts @@ -0,0 +1,83 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const HBDADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 1; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = + GV.getPerfMeasureRateArray(data, PMD.data) ?? []; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + + errorArray = [ + ...errorArray, + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + dataSource: data[DC.DATA_SOURCE], + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + ], + }), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator, + "Ages 65 to 75" + ), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ]; + + return errorArray; +}; + +export const validationFunctions = [HBDADValidation]; diff --git a/services/ui-src/src/measures/2024/HPCMIAD/data.ts b/services/ui-src/src/measures/2024/HPCMIAD/data.ts new file mode 100644 index 0000000000..afadd5e29b --- /dev/null +++ b/services/ui-src/src/measures/2024/HPCMIAD/data.ts @@ -0,0 +1,71 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("HPCMI-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of beneficiaries ages 18 to 75 with a serious mental illness and diabetes (type 1 and type 2) who had hemoglobin A1c (HbA1c) in poor control (> 9.0%).", + ], + questionListItems: [], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.HYBRID_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.OTHER, + description: true, + }, + ], + }, + { + label: "What is the Medical Records Data Source?", + options: [ + { + value: DC.EHR_DATA, + }, + { + value: DC.PAPER, + }, + ], + }, + ], + }, + + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/HPCMIAD/index.test.tsx b/services/ui-src/src/measures/2024/HPCMIAD/index.test.tsx new file mode 100644 index 0000000000..dfd9d8c746 --- /dev/null +++ b/services/ui-src/src/measures/2024/HPCMIAD/index.test.tsx @@ -0,0 +1,245 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "HPCMI-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 18 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 65 to 75", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/HPCMIAD/index.tsx b/services/ui-src/src/measures/2024/HPCMIAD/index.tsx new file mode 100644 index 0000000000..2ab8774c06 --- /dev/null +++ b/services/ui-src/src/measures/2024/HPCMIAD/index.tsx @@ -0,0 +1,67 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +import { FormData } from "./types"; + +export const HPCMIAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/HPCMIAD/types.ts b/services/ui-src/src/measures/2024/HPCMIAD/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/HPCMIAD/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/HPCMIAD/validation.ts b/services/ui-src/src/measures/2024/HPCMIAD/validation.ts new file mode 100644 index 0000000000..d756e3448e --- /dev/null +++ b/services/ui-src/src/measures/2024/HPCMIAD/validation.ts @@ -0,0 +1,83 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const HPCMIADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 1; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator, + PMD.qualifiers[1].label + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + dataSource: data[DC.DATA_SOURCE], + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [HPCMIADValidation]; diff --git a/services/ui-src/src/measures/2024/HVLAD/data.ts b/services/ui-src/src/measures/2024/HVLAD/data.ts new file mode 100644 index 0000000000..a76b313992 --- /dev/null +++ b/services/ui-src/src/measures/2024/HVLAD/data.ts @@ -0,0 +1,46 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("HVL-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of beneficiaries age 18 and older with a diagnosis of Human Immunodeficiency Virus (HIV) who had a HIV viral load less than 200 copies/mL at last HIV viral load test during the measurement year.", + ], + questionListItems: [], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/HVLAD/index.test.tsx b/services/ui-src/src/measures/2024/HVLAD/index.test.tsx new file mode 100644 index 0000000000..11c88efbae --- /dev/null +++ b/services/ui-src/src/measures/2024/HVLAD/index.test.tsx @@ -0,0 +1,259 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "HVL-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + FollowUpwithin7daysafterdischarge: [ + { + label: "Ages 18 to 64", + }, + { + label: "Age 65 and older", + }, + ], + FollowUpwithin30daysafterdischarge: [ + { + label: "Ages 18 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Age 65 and older", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/HVLAD/index.tsx b/services/ui-src/src/measures/2024/HVLAD/index.tsx new file mode 100644 index 0000000000..9556dc9388 --- /dev/null +++ b/services/ui-src/src/measures/2024/HVLAD/index.tsx @@ -0,0 +1,67 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +import { FormData } from "./types"; + +export const HVLAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/HVLAD/types.ts b/services/ui-src/src/measures/2024/HVLAD/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/HVLAD/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/HVLAD/validation.ts b/services/ui-src/src/measures/2024/HVLAD/validation.ts new file mode 100644 index 0000000000..f25dc81ef0 --- /dev/null +++ b/services/ui-src/src/measures/2024/HVLAD/validation.ts @@ -0,0 +1,80 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const HVLADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 1; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [HVLADValidation]; diff --git a/services/ui-src/src/measures/2024/IETAD/data.ts b/services/ui-src/src/measures/2024/IETAD/data.ts new file mode 100644 index 0000000000..7059ea5aa4 --- /dev/null +++ b/services/ui-src/src/measures/2024/IETAD/data.ts @@ -0,0 +1,49 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("IET-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of new substance use disorder (SUD) episodes that result in treatment initiation and engagement. Two rates are reported:", + ], + questionListItems: [ + "Initiation of SUD Treatment. The percentage of new SUD episodes that result in treatment initiation through an inpatient SUD admission, outpatient visit, intensive outpatient encounter, partial hospitalization, telehealth visit, or medication treatment within 14 days.", + "Engagement of SUD Treatment. The percentage of new SUD episodes that have evidence of treatment engagement within 34 days of initiation.", + ], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/IETAD/index.test.tsx b/services/ui-src/src/measures/2024/IETAD/index.test.tsx new file mode 100644 index 0000000000..1d89e5a12b --- /dev/null +++ b/services/ui-src/src/measures/2024/IETAD/index.test.tsx @@ -0,0 +1,307 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "IET-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsPM).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + EngagementofAODTreatmentAlcoholAbuseorDependence: [ + { + label: "Ages 18 to 64", + }, + { + label: "Age 65 and older", + }, + ], + InitiationofAODTreatmentOtherDrugAbuseorDependence: [ + { + label: "Ages 18 to 64", + }, + { + label: "Age 65 and older", + }, + ], + EngagementofAODTreatmentTotalAODAbuseorDependence: [ + { + label: "Ages 18 to 64", + }, + { + label: "Age 65 and older", + }, + ], + EngagementofAODTreatmentOtherDrugAbuseorDependence: [ + { + label: "Ages 18 to 64", + }, + { + label: "Age 65 and older", + }, + ], + InitiationofAODTreatmentOpioidAbuseorDependence: [ + { + label: "Ages 18 to 64", + }, + { + label: "Age 65 and older", + }, + ], + InitiationofAODTreatmentTotalAODAbuseorDependence: [ + { + label: "Ages 18 to 64", + }, + { + label: "Age 65 and older", + }, + ], + InitiationofAODTreatmentAlcoholAbuseorDependence: [ + { + label: "Ages 18 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Age 65 and older", + }, + ], + EngagementofAODTreatmentOpioidAbuseorDependence: [ + { + label: "Ages 18 to 64", + }, + { + label: "Age 65 and older", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/IETAD/index.tsx b/services/ui-src/src/measures/2024/IETAD/index.tsx new file mode 100644 index 0000000000..e38e8ef68c --- /dev/null +++ b/services/ui-src/src/measures/2024/IETAD/index.tsx @@ -0,0 +1,71 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +import { FormData } from "./types"; + +export const IETAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/IETAD/types.ts b/services/ui-src/src/measures/2024/IETAD/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/IETAD/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/IETAD/validation.ts b/services/ui-src/src/measures/2024/IETAD/validation.ts new file mode 100644 index 0000000000..ebbc3c8cd1 --- /dev/null +++ b/services/ui-src/src/measures/2024/IETAD/validation.ts @@ -0,0 +1,115 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const IETValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 1; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + + const locationDictionary = GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ); + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + let unfilteredSameDenominatorErrors: any[] = []; + for (let i = 0; i < performanceMeasureArray.length; i += 2) { + unfilteredSameDenominatorErrors = [ + ...unfilteredSameDenominatorErrors, + ...GV.validateEqualQualifierDenominatorsPM( + [performanceMeasureArray[i], performanceMeasureArray[i + 1]], + ageGroups, + undefined, + (qual) => + `Denominators must be the same for ${locationDictionary([ + qual, + ])} for ${PMD.categories[i].label} and ${ + PMD.categories[i + 1].label + }.` + ), + ]; + } + + let filteredSameDenominatorErrors: any = []; + let errorList: string[] = []; + unfilteredSameDenominatorErrors.forEach((error) => { + if (!(errorList.indexOf(error.errorMessage) > -1)) { + errorList.push(error.errorMessage); + filteredSameDenominatorErrors.push(error); + } + }); + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...filteredSameDenominatorErrors, + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD.data, 0, 1, 2), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary, + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateSameDenominatorSets(), + GV.validateOneCatRateHigherThanOtherCatOMS(0, 1, 2), + ], + }), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [IETValidation]; diff --git a/services/ui-src/src/measures/2024/IETHH/data.ts b/services/ui-src/src/measures/2024/IETHH/data.ts new file mode 100644 index 0000000000..03de2cdf41 --- /dev/null +++ b/services/ui-src/src/measures/2024/IETHH/data.ts @@ -0,0 +1,49 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("IET-HH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of new substance use disorder (SUD) episodes that result in treatment initiation and engagement. Two rates are reported:", + ], + questionListItems: [ + "Initiation of SUD Treatment. The percentage of new SUD episodes that result in treatment initiation through an inpatient SUD admission, outpatient visit, intensive outpatient encounter, partial hospitalization, telehealth visit, or medication treatment within 14 days.", + "Engagement of SUD Treatment. The percentage of new SUD episodes that have evidence of treatment engagement within 34 days of initiation.", + ], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/IETHH/index.test.tsx b/services/ui-src/src/measures/2024/IETHH/index.test.tsx new file mode 100644 index 0000000000..227475acfa --- /dev/null +++ b/services/ui-src/src/measures/2024/IETHH/index.test.tsx @@ -0,0 +1,258 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "IET-HH"; +const coreSet = "HHCS"; +const state = "DC"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/IETHH/index.tsx b/services/ui-src/src/measures/2024/IETHH/index.tsx new file mode 100644 index 0000000000..a3b723cc91 --- /dev/null +++ b/services/ui-src/src/measures/2024/IETHH/index.tsx @@ -0,0 +1,73 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +import { FormData } from "./types"; + +export const IETHH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/IETHH/types.ts b/services/ui-src/src/measures/2024/IETHH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/IETHH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/IETHH/validation.ts b/services/ui-src/src/measures/2024/IETHH/validation.ts new file mode 100644 index 0000000000..63bba4e43e --- /dev/null +++ b/services/ui-src/src/measures/2024/IETHH/validation.ts @@ -0,0 +1,116 @@ +import * as DC from "dataConstants"; +import * as GV from "../shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "../shared/CommonQuestions/OptionalMeasureStrat/data"; + +const IETValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 2; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + + const locationDictionary = GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ); + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + let unfilteredSameDenominatorErrors: any[] = []; + for (let i = 0; i < performanceMeasureArray.length; i += 2) { + unfilteredSameDenominatorErrors = [ + ...unfilteredSameDenominatorErrors, + ...GV.validateEqualQualifierDenominatorsPM( + [performanceMeasureArray[i], performanceMeasureArray[i + 1]], + ageGroups, + undefined, + (qual) => + `Denominators must be the same for ${locationDictionary([ + qual, + ])} for ${PMD.categories[i].label} and ${ + PMD.categories[i + 1].label + }.` + ), + ]; + } + + let filteredSameDenominatorErrors: any = []; + let errorList: string[] = []; + unfilteredSameDenominatorErrors.forEach((error) => { + if (!(errorList.indexOf(error.errorMessage) > -1)) { + errorList.push(error.errorMessage); + filteredSameDenominatorErrors.push(error); + } + }); + + errorArray = [ + ...errorArray, + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD.data, 0, 1, 2), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateTotalNDR(performanceMeasureArray, undefined, undefined), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...filteredSameDenominatorErrors, + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary, + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateOMSTotalNDR(), + GV.validateSameDenominatorSets(), + GV.validateOneCatRateHigherThanOtherCatOMS(0, 1, 2), + ], + }), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [IETValidation]; diff --git a/services/ui-src/src/measures/2024/IMACH/data.ts b/services/ui-src/src/measures/2024/IMACH/data.ts new file mode 100644 index 0000000000..2186f00979 --- /dev/null +++ b/services/ui-src/src/measures/2024/IMACH/data.ts @@ -0,0 +1,80 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("IMA-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of adolescents age 13 who had one dose of meningococcal vaccine, one tetanus, diphtheria toxoids and acellular pertussis (Tdap) vaccine, and have completed the human papillomavirus (HPV) vaccine series by their 13th birthday. The measure calculates a rate for each vaccine and two combination rates.", + ], + questionListItems: [], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.IMMUNIZATION_REGISTRY_INFORMATION_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.HYBRID_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.IMMUNIZATION_REGISTRY_INFORMATION_SYSTEM, + }, + { + value: DC.OTHER, + description: true, + }, + ], + }, + { + label: "What is the Medical Records Data Source?", + options: [ + { + value: DC.EHR_DATA, + }, + { + value: DC.PAPER, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_CLINIC_DATA_SYSTEMS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/IMACH/index.test.tsx b/services/ui-src/src/measures/2024/IMACH/index.test.tsx new file mode 100644 index 0000000000..464bf4c9ba --- /dev/null +++ b/services/ui-src/src/measures/2024/IMACH/index.test.tsx @@ -0,0 +1,267 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "IMA-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(33000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Meningococcal", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Tdap", + }, + { + label: "Human Papillomavirus (HPV)", + }, + { + label: "Combination 1 (Meningococcal, Tdap)", + }, + { + label: "Combination 2 (Meningococcal, Tdap, HPV)", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/IMACH/index.tsx b/services/ui-src/src/measures/2024/IMACH/index.tsx new file mode 100644 index 0000000000..5bdde296a0 --- /dev/null +++ b/services/ui-src/src/measures/2024/IMACH/index.tsx @@ -0,0 +1,67 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +import { FormData } from "./types"; + +export const IMACH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/IMACH/types.ts b/services/ui-src/src/measures/2024/IMACH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/IMACH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/IMACH/validation.ts b/services/ui-src/src/measures/2024/IMACH/validation.ts new file mode 100644 index 0000000000..5e086eeb09 --- /dev/null +++ b/services/ui-src/src/measures/2024/IMACH/validation.ts @@ -0,0 +1,76 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const DEVCHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + dataSource: data[DC.DATA_SOURCE], + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateEqualQualifierDenominatorsOMS(), + GV.validateOneCatRateHigherThanOtherCatOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateEqualCategoryDenominatorsOMS(), + ], + }), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateEqualCategoryDenominatorsPM(data, PMD.categories, ageGroups), + ]; + + return errorArray; +}; + +export const validationFunctions = [DEVCHValidation]; diff --git a/services/ui-src/src/measures/2024/IUHH/data.ts b/services/ui-src/src/measures/2024/IUHH/data.ts new file mode 100644 index 0000000000..4fb35e0026 --- /dev/null +++ b/services/ui-src/src/measures/2024/IUHH/data.ts @@ -0,0 +1,70 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("IU-HH"); + +const measureName = "IUHH"; + +const inputFieldNames = [ + { + label: "Number of Enrollee Months", + text: "Number of Enrollee Months", + id: "G3AVs1", + }, + { label: "Discharges", text: "Discharges", id: "MwGkaA" }, + { + label: "Discharges per 1,000 Enrollee Months", + text: "Discharges per 1,000 Enrollee Months", + id: "CGEK9m", + }, + { label: "Days", text: "Days", id: "jSSEHA" }, + { + label: "Days per 1,000 Enrollee Months", + text: "Days per 1,000 Enrollee Months", + id: "qjHhDk", + }, + { + label: "Average Length of Stay", + text: "Average Length of Stay", + id: "coDyWU", + }, +]; + +// Rate structure by index in row +const ndrFormulas = [ + // Discharges per 1,000 Enrollee Months + { + num: 1, + denom: 0, + rate: 2, + mult: 1000, + }, + // Days per 1,000 Enrollee Months + { + num: 3, + denom: 0, + rate: 4, + mult: 1000, + }, + // Average Length of Stay + { + num: 3, + denom: 1, + rate: 5, + mult: 1, + }, +]; + +export const data: DataDrivenTypes.PerformanceMeasure = { + customPrompt: + "Enter the appropriate data below. Completion of at least one set of Numerator/Denominator/Rate (numeric entry, other than zero) is required.", + questionText: [ + "Rate of acute inpatient care and services (total, mental and behavioral disorders, surgery, and medicine) per 1,000 enrollee months among health home enrollees.", + ], + questionListItems: [], + measureName, + inputFieldNames, + ndrFormulas, + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/IUHH/index.test.tsx b/services/ui-src/src/measures/2024/IUHH/index.test.tsx new file mode 100644 index 0000000000..efe93cbc86 --- /dev/null +++ b/services/ui-src/src/measures/2024/IUHH/index.test.tsx @@ -0,0 +1,783 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "IU-HH"; +const coreSet = "HHCS"; +const state = "DC"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.ComplexValidateDualPopInformation).not.toHaveBeenCalled(); + expect(V.ComplexNoNonZeroNumOrDenom).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.ComplexValidateNDRTotals).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.ComplexAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.ComplexNoNonZeroNumOrDenom).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.ComplexValidateNDRTotals).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + Inpatient: [ + { + fields: [ + { + value: "1", + label: "Number of Enrollee Months", + }, + { + value: "1", + label: "Discharges", + }, + { + value: "1000.0", + label: "Discharges per 1,000 Enrollee Months", + }, + { + value: "1", + label: "Days", + }, + { + value: "1000.0", + label: "Days per 1,000 Enrollee Months", + }, + { + value: "1.0", + label: "Average Length of Stay", + }, + ], + label: "Ages 0 to 17", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Ages 18 to 64", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Age 65 and older", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Ages unknown", + }, + { + fields: [ + { + value: "1", + label: "Number of Enrollee Months", + }, + { + value: 1, + label: "Discharges", + }, + { + value: "1000.0", + label: "Discharges per 1,000 Enrollee Months", + }, + { + value: 1, + label: "Days", + }, + { + value: "1000.0", + label: "Days per 1,000 Enrollee Months", + }, + { + value: "1.0", + label: "Average Length of Stay", + }, + ], + isTotal: true, + label: "Total", + }, + ], + Maternity: [ + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Ages 18 to 64", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Ages unknown", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + isTotal: true, + label: "Total", + }, + ], + Medicine: [ + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Ages 0 to 17", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Ages 18 to 64", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Age 65 and older", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Ages unknown", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + isTotal: true, + label: "Total", + }, + ], + MentalandBehavioralDisorders: [ + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Ages 0 to 17", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Ages 18 to 64", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Age 65 and older", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Ages unknown", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + isTotal: true, + label: "Total", + }, + ], + Surgery: [ + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Ages 0 to 17", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Ages 18 to 64", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Age 65 and older", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + label: "Ages unknown", + }, + { + fields: [ + { + label: "Number of Enrollee Months", + }, + { + label: "Discharges", + }, + { + label: "Discharges per 1,000 Enrollee Months", + }, + { + label: "Days", + }, + { + label: "Days per 1,000 Enrollee Months", + }, + { + label: "Average Length of Stay", + }, + ], + isTotal: true, + label: "Total", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/IUHH/index.tsx b/services/ui-src/src/measures/2024/IUHH/index.tsx new file mode 100644 index 0000000000..b54d035cff --- /dev/null +++ b/services/ui-src/src/measures/2024/IUHH/index.tsx @@ -0,0 +1,85 @@ +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { FormData } from "./types"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import { validationFunctions } from "./validation"; +import { xNumbersYDecimals } from "utils"; + +export const IUHH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && ( + + )} + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/IUHH/types.ts b/services/ui-src/src/measures/2024/IUHH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/IUHH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/IUHH/validation.ts b/services/ui-src/src/measures/2024/IUHH/validation.ts new file mode 100644 index 0000000000..1b787f3db3 --- /dev/null +++ b/services/ui-src/src/measures/2024/IUHH/validation.ts @@ -0,0 +1,130 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +// Rate structure by index in row +const ndrForumlas = [ + // Discharges per 1,000 Enrollee Months + { + numerator: 1, + denominator: 0, + rateIndex: 2, + }, + // Days per 1,000 Enrollee Months + { + numerator: 3, + denominator: 0, + rateIndex: 4, + }, + // Average Length of Stay + { + numerator: 3, + denominator: 1, + rateIndex: 5, + }, +]; + +let OPM: any; + +const IUHHValidation = (data: FormData) => { + let errorArray: any[] = []; + const dateRange = data[DC.DATE_RANGE]; + const definitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + OPM = data[DC.OPM_RATES]; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + // Quick reference list of all rate indices + errorArray = [ + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.ComplexValidateDualPopInformation( + performanceMeasureArray, + OPM, + definitionOfDenominator + ), + + // Performance Measure Validations + ...GV.ComplexAtLeastOneRateComplete(performanceMeasureArray, OPM), + ...GV.ComplexNoNonZeroNumOrDenom(performanceMeasureArray, OPM, ndrForumlas), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.ComplexValidateNDRTotals( + performanceMeasureArray, + PMD.categories, + ndrForumlas + ), + ...GV.ComplexValueSameCrossCategory({ + rateData: performanceMeasureArray, + OPM, + }), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [OMSValidations], + }), + ]; + return errorArray; +}; + +const OMSValidations: GV.Types.OmsValidationCallback = ({ + rateData, + locationDictionary, + label, +}) => { + const rates = Object.keys(rateData?.rates ?? {}).map((x) => { + return { rate: [rateData?.rates?.[x]?.OPM?.[0]] }; + }); + return OPM === undefined + ? [ + ...GV.ComplexNoNonZeroNumOrDenomOMS( + rateData?.["iuhh-rate"]?.rates ?? {}, + rates ?? [], + ndrForumlas, + `Optional Measure Stratification: ${locationDictionary(label)}` + ), + ...GV.ComplexValueSameCrossCategoryOMS( + rateData?.["iuhh-rate"]?.rates ?? {}, + PMD.categories, + PMD.qualifiers, + `Optional Measure Stratification: ${locationDictionary(label)}` + ), + ] + : [ + ...GV.ComplexNoNonZeroNumOrDenomOMS( + rateData?.rates, + rates ?? [], + ndrForumlas, + `Optional Measure Stratification: ${locationDictionary(label)}` + ), + ]; +}; + +export const validationFunctions = [IUHHValidation]; diff --git a/services/ui-src/src/measures/2024/LBWCH/index.test.tsx b/services/ui-src/src/measures/2024/LBWCH/index.test.tsx new file mode 100644 index 0000000000..5e24fee106 --- /dev/null +++ b/services/ui-src/src/measures/2024/LBWCH/index.test.tsx @@ -0,0 +1,90 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { clearMocks } from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "LBW-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.getByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); diff --git a/services/ui-src/src/measures/2024/LBWCH/index.tsx b/services/ui-src/src/measures/2024/LBWCH/index.tsx new file mode 100644 index 0000000000..ed5a2096b5 --- /dev/null +++ b/services/ui-src/src/measures/2024/LBWCH/index.tsx @@ -0,0 +1,17 @@ +import * as QMR from "components"; + +interface Props { + name: string; + year: string; +} + +export const LBWCH = ({ name, year }: Props) => { + return ( + + ); +}; diff --git a/services/ui-src/src/measures/2024/LRCDCH/index.test.tsx b/services/ui-src/src/measures/2024/LRCDCH/index.test.tsx new file mode 100644 index 0000000000..e0b5d2a8d0 --- /dev/null +++ b/services/ui-src/src/measures/2024/LRCDCH/index.test.tsx @@ -0,0 +1,90 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { clearMocks } from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "LRCD-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.getByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); diff --git a/services/ui-src/src/measures/2024/LRCDCH/index.tsx b/services/ui-src/src/measures/2024/LRCDCH/index.tsx new file mode 100644 index 0000000000..31d02c9d91 --- /dev/null +++ b/services/ui-src/src/measures/2024/LRCDCH/index.tsx @@ -0,0 +1,20 @@ +import * as QMR from "components"; + +interface Props { + name: string; + year: string; +} + +export const LRCDCH = ({ name, year }: Props) => { + return ( + + ); +}; diff --git a/services/ui-src/src/measures/2024/LSCCH/data.ts b/services/ui-src/src/measures/2024/LSCCH/data.ts new file mode 100644 index 0000000000..ebb9d8dd3d --- /dev/null +++ b/services/ui-src/src/measures/2024/LSCCH/data.ts @@ -0,0 +1,70 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("LSC-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of children 2 years of age who had one or more capillary or venous lead blood test for lead poisoning by their second birthday.", + ], + questionListItems: [], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.HYBRID_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.OTHER, + description: true, + }, + ], + }, + { + label: "What is the Medical Records Data Source?", + options: [ + { + value: DC.EHR_DATA, + }, + { + value: DC.PAPER, + }, + ], + }, + ], + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/LSCCH/index.test.tsx b/services/ui-src/src/measures/2024/LSCCH/index.test.tsx new file mode 100644 index 0000000000..3a3b2fdd51 --- /dev/null +++ b/services/ui-src/src/measures/2024/LSCCH/index.test.tsx @@ -0,0 +1,271 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "LSC-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(33000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + performanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/LSCCH/index.tsx b/services/ui-src/src/measures/2024/LSCCH/index.tsx new file mode 100644 index 0000000000..e2cbd41ab3 --- /dev/null +++ b/services/ui-src/src/measures/2024/LSCCH/index.tsx @@ -0,0 +1,67 @@ +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { FormData } from "./types"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import { validationFunctions } from "./validation"; + +export const LSCCH = ({ + name, + year, + measureId, + setValidationFunctions, + showOptionalMeasureStrat, + isNotReportingData, + isPrimaryMeasureSpecSelected, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/LSCCH/types.ts b/services/ui-src/src/measures/2024/LSCCH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/LSCCH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/LSCCH/validation.ts b/services/ui-src/src/measures/2024/LSCCH/validation.ts new file mode 100644 index 0000000000..a1aab407e6 --- /dev/null +++ b/services/ui-src/src/measures/2024/LSCCH/validation.ts @@ -0,0 +1,77 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const LSCCHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateOPMRates(OPM), + + // Performance Measure Validations + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateTotalNDR(performanceMeasureArray, undefined, undefined), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateOMSTotalNDR(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [LSCCHValidation]; diff --git a/services/ui-src/src/measures/2024/MSCAD/data.ts b/services/ui-src/src/measures/2024/MSCAD/data.ts new file mode 100644 index 0000000000..9ea49bf5cb --- /dev/null +++ b/services/ui-src/src/measures/2024/MSCAD/data.ts @@ -0,0 +1,22 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("MSC-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "The following components of this measure assess different facets of providing medical assistance with smoking and tobacco use cessation:", + ], + questionListItems: [ + "– A rolling average represents the percentage of beneficiaries age 18 and Older who were current smokers or tobacco users and who received advice to quit during the measurement year", + "– A rolling average represents the percentage of beneficiaries age 18 and Older who were current smokers or tobacco users and who discussed or were recommended cessation medications during the measurement year", + "– A rolling average represents the percentage of beneficiaries age 18 and Older who were current smokers or tobacco users and who discussed or were provided cessation methods or strategies during the measurement year", + ], + questionListTitles: [ + "Advising Smokers and Tobacco Users to Quit", + "Discussing Cessation Medications", + "Discussing Cessation Strategies", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/MSCAD/index.test.tsx b/services/ui-src/src/measures/2024/MSCAD/index.test.tsx new file mode 100644 index 0000000000..eb6af84ec6 --- /dev/null +++ b/services/ui-src/src/measures/2024/MSCAD/index.test.tsx @@ -0,0 +1,269 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "MSC-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + DiscussingCessationMedications: [ + { + label: "Ages 18 to 64", + }, + { + label: "Age 65 and older", + }, + ], + AdvisingSmokersandTobaccoUserstoQuit: [ + { + label: "Ages 18 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Age 65 and older", + }, + ], + DiscussingCessationStrategies: [ + { + label: "Ages 18 to 64", + }, + { + label: "Age 65 and older", + }, + ], + PercentageofCurrentSmokersandTobaccoUsers: [ + { + label: "Ages 18 to 64", + }, + { + label: "Age 65 and older", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/MSCAD/index.tsx b/services/ui-src/src/measures/2024/MSCAD/index.tsx new file mode 100644 index 0000000000..208f883eb7 --- /dev/null +++ b/services/ui-src/src/measures/2024/MSCAD/index.tsx @@ -0,0 +1,57 @@ +import { useEffect } from "react"; +import * as Q from "./questions"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; + +export const MSCAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {/* Show Other Performance Measures when isHedis is not true */} + {isOtherMeasureSpecSelected && ( + + )} + + {showOptionalMeasureStrat && } + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/MSCAD/questions/DataSource.tsx b/services/ui-src/src/measures/2024/MSCAD/questions/DataSource.tsx new file mode 100644 index 0000000000..b620776912 --- /dev/null +++ b/services/ui-src/src/measures/2024/MSCAD/questions/DataSource.tsx @@ -0,0 +1,40 @@ +import * as QMR from "components"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import { FormData } from "../types"; + +export const DataSource = () => { + const register = useCustomRegister(); + + return ( + + + Describe the data source ( + + text in this field is included in publicly-reported + state-specific comments + + ): + + } + {...register("DataSource-CAHPS-Version-Other")} + />, + ], + }, + ]} + /> + + ); +}; diff --git a/services/ui-src/src/measures/2024/MSCAD/questions/index.tsx b/services/ui-src/src/measures/2024/MSCAD/questions/index.tsx new file mode 100644 index 0000000000..ab26354b72 --- /dev/null +++ b/services/ui-src/src/measures/2024/MSCAD/questions/index.tsx @@ -0,0 +1 @@ +export * from "./DataSource"; diff --git a/services/ui-src/src/measures/2024/MSCAD/types.ts b/services/ui-src/src/measures/2024/MSCAD/types.ts new file mode 100644 index 0000000000..3b3b4a7ea7 --- /dev/null +++ b/services/ui-src/src/measures/2024/MSCAD/types.ts @@ -0,0 +1,19 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export interface FormData + extends Types.DefinitionOfPopulation, + Types.StatusOfData, + Types.DateRange, + Types.DidReport, + Types.AdditionalNotes, + Types.WhyAreYouNotReporting, + Types.CombinedRates, + Types.OtherPerformanceMeasure, + Types.MeasurementSpecification, + Types.PerformanceMeasure, + Types.DeviationFromMeasureSpecification, + Types.OptionalMeasureStratification { + //DataSource + "DataSource-CAHPS-Version": string; + "DataSource-CAHPS-Version-Other": string; +} diff --git a/services/ui-src/src/measures/2024/MSCAD/validation.ts b/services/ui-src/src/measures/2024/MSCAD/validation.ts new file mode 100644 index 0000000000..5b66245cb6 --- /dev/null +++ b/services/ui-src/src/measures/2024/MSCAD/validation.ts @@ -0,0 +1,80 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import * as Types from "measures/2024/shared/CommonQuestions/types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const MSCADValidation = (data: Types.DefaultFormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 1; + const OPM = data[DC.OPM_RATES]; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [MSCADValidation]; diff --git a/services/ui-src/src/measures/2024/NCIDDSAD/index.test.tsx b/services/ui-src/src/measures/2024/NCIDDSAD/index.test.tsx new file mode 100644 index 0000000000..51f23104f5 --- /dev/null +++ b/services/ui-src/src/measures/2024/NCIDDSAD/index.test.tsx @@ -0,0 +1,90 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { clearMocks } from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "NCIDDS-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.getByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); diff --git a/services/ui-src/src/measures/2024/NCIDDSAD/index.tsx b/services/ui-src/src/measures/2024/NCIDDSAD/index.tsx new file mode 100644 index 0000000000..efad50f17b --- /dev/null +++ b/services/ui-src/src/measures/2024/NCIDDSAD/index.tsx @@ -0,0 +1,24 @@ +import * as QMR from "components"; + +interface Props { + name: string; + year: string; +} + +export const NCIDDSAD = ({ name, year }: Props) => { + return ( + + ); +}; diff --git a/services/ui-src/src/measures/2024/OEVCH/data.ts b/services/ui-src/src/measures/2024/OEVCH/data.ts new file mode 100644 index 0000000000..04d5babecb --- /dev/null +++ b/services/ui-src/src/measures/2024/OEVCH/data.ts @@ -0,0 +1,12 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("OEV-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of enrolled children under age 21 who received a comprehensive or periodic oral evaluation within the measurement year.", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/OEVCH/index.test.tsx b/services/ui-src/src/measures/2024/OEVCH/index.test.tsx new file mode 100644 index 0000000000..8a5131cbdd --- /dev/null +++ b/services/ui-src/src/measures/2024/OEVCH/index.test.tsx @@ -0,0 +1,286 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "OEV-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(33000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Age <1", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 1 to 2", + }, + { + label: "Ages 3 to 5", + }, + { + label: "Ages 6 to 7", + }, + { + label: "Ages 8 to 9", + }, + { + label: "Ages 10 to 11", + }, + { + label: "Ages 12 to 14", + }, + { + label: "Ages 15 to 18", + }, + { + label: "Ages 19 to 20", + }, + { + label: "Total ages <1 to 20 ", + isTotal: true, + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + }, + }, + MeasurementSpecification: "ADA-DQA", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/OEVCH/index.tsx b/services/ui-src/src/measures/2024/OEVCH/index.tsx new file mode 100644 index 0000000000..529823d499 --- /dev/null +++ b/services/ui-src/src/measures/2024/OEVCH/index.tsx @@ -0,0 +1,68 @@ +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as DC from "dataConstants"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { FormData } from "./types"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import { validationFunctions } from "./validation"; + +export const OEVCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/OEVCH/types.ts b/services/ui-src/src/measures/2024/OEVCH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/OEVCH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/OEVCH/validation.ts b/services/ui-src/src/measures/2024/OEVCH/validation.ts new file mode 100644 index 0000000000..aee7ac54e3 --- /dev/null +++ b/services/ui-src/src/measures/2024/OEVCH/validation.ts @@ -0,0 +1,76 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const OEVCHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + + // Performance Measure Validations + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateTotalNDR(performanceMeasureArray, undefined, undefined), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateOMSTotalNDR(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [OEVCHValidation]; diff --git a/services/ui-src/src/measures/2024/OHDAD/data.ts b/services/ui-src/src/measures/2024/OHDAD/data.ts new file mode 100644 index 0000000000..b6f55dee76 --- /dev/null +++ b/services/ui-src/src/measures/2024/OHDAD/data.ts @@ -0,0 +1,13 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("OHD-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "The percentage of beneficiaries age 18 and older who received prescriptions for opioids with an average daily dosage greater than or equal to 90 morphine milligram equivalents (MME) over a period of 90 days or more. Beneficiaries with a cancer diagnosis, sickle cell disease diagnosis, or in hospice or palliative care are excluded.", + ], + questionListItems: [], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/OHDAD/index.test.tsx b/services/ui-src/src/measures/2024/OHDAD/index.test.tsx new file mode 100644 index 0000000000..e7758bce90 --- /dev/null +++ b/services/ui-src/src/measures/2024/OHDAD/index.test.tsx @@ -0,0 +1,258 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "OHD-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/OHDAD/index.tsx b/services/ui-src/src/measures/2024/OHDAD/index.tsx new file mode 100644 index 0000000000..f7c29e84aa --- /dev/null +++ b/services/ui-src/src/measures/2024/OHDAD/index.tsx @@ -0,0 +1,67 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +import { FormData } from "./types"; + +export const OHDAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/OHDAD/types.ts b/services/ui-src/src/measures/2024/OHDAD/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/OHDAD/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/OHDAD/validation.ts b/services/ui-src/src/measures/2024/OHDAD/validation.ts new file mode 100644 index 0000000000..3a2dfe18ea --- /dev/null +++ b/services/ui-src/src/measures/2024/OHDAD/validation.ts @@ -0,0 +1,78 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const OHDValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const age65PlusIndex = 1; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const DefinitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDualPopInformationPM( + performanceMeasureArray, + OPM, + age65PlusIndex, + DefinitionOfDenominator + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [OHDValidation]; diff --git a/services/ui-src/src/measures/2024/OUDAD/data.ts b/services/ui-src/src/measures/2024/OUDAD/data.ts new file mode 100644 index 0000000000..ec44784982 --- /dev/null +++ b/services/ui-src/src/measures/2024/OUDAD/data.ts @@ -0,0 +1,20 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("OUD-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of Medicaid beneficiaries ages 18 to 64 with an opioid use disorder (OUD) who filled a prescription for or were administered or dispensed an FDA-approved medication for the disorder during the measurement year. Five rates are reported:", + "A total (overall) rate capturing any medications used in medication assisted treatment of opioid dependence and addiction (Rate 1)", + "Four separate rates representing the following types of FDA-approved drug products:", + ], + questionListItems: [ + "Buprenorphine (Rate 2)", + "Oral naltrexone (Rate 3)", + "Long-acting, injectable naltrexone (Rate 4)", + "Methadone (Rate 5)", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/OUDAD/index.test.tsx b/services/ui-src/src/measures/2024/OUDAD/index.test.tsx new file mode 100644 index 0000000000..e1bf707c59 --- /dev/null +++ b/services/ui-src/src/measures/2024/OUDAD/index.test.tsx @@ -0,0 +1,264 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "OUD-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/OUDAD/index.tsx b/services/ui-src/src/measures/2024/OUDAD/index.tsx new file mode 100644 index 0000000000..270b7fe8e7 --- /dev/null +++ b/services/ui-src/src/measures/2024/OUDAD/index.tsx @@ -0,0 +1,66 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { FormData } from "./types"; + +export const OUDAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/OUDAD/types.ts b/services/ui-src/src/measures/2024/OUDAD/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/OUDAD/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/OUDAD/validation.ts b/services/ui-src/src/measures/2024/OUDAD/validation.ts new file mode 100644 index 0000000000..f0c8f23fb0 --- /dev/null +++ b/services/ui-src/src/measures/2024/OUDAD/validation.ts @@ -0,0 +1,75 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const OUDValidation = (data: FormData) => { + const OPM = data[DC.OPM_RATES]; + const ageGroups = PMD.qualifiers; + const performanceMeasureArray = + GV.getPerfMeasureRateArray(data, PMD.data) ?? []; + const dateRange = data[DC.DATE_RANGE]; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + PMD.qualifiers, + PMD.categories + ), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateOPMRates(OPM), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + PMD.qualifiers + ), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateEqualCategoryDenominatorsPM(data, PMD.categories, ageGroups), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateEqualQualifierDenominatorsOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateEqualCategoryDenominatorsOMS(), + ], + }), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ]; + + return errorArray; +}; + +export const validationFunctions = [OUDValidation]; diff --git a/services/ui-src/src/measures/2024/OUDHH/data.ts b/services/ui-src/src/measures/2024/OUDHH/data.ts new file mode 100644 index 0000000000..6e0d3dae4d --- /dev/null +++ b/services/ui-src/src/measures/2024/OUDHH/data.ts @@ -0,0 +1,20 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("OUD-HH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of Medicaid health home enrollees ages 18 to 64 with an opioid use disorder (OUD) who filled a prescription for or were administered or dispensed an FDA-approved medication for the disorder during the measurement year. Five rates are reported:", + "A total (overall) rate capturing any medications used in medication assisted treatment of opioid dependence and addiction (Rate 1)", + "Four separate rates representing the following types of FDA-approved drug products:", + ], + questionListItems: [ + "Buprenorphine (Rate 2)", + "Oral naltrexone (Rate 3)", + "Long-acting, injectable naltrexone (Rate 4)", + "Methadone (Rate 5)", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/OUDHH/index.test.tsx b/services/ui-src/src/measures/2024/OUDHH/index.test.tsx new file mode 100644 index 0000000000..efdc30747e --- /dev/null +++ b/services/ui-src/src/measures/2024/OUDHH/index.test.tsx @@ -0,0 +1,258 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "OUD-HH"; +const coreSet = "HHCS"; +const state = "DC"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/OUDHH/index.tsx b/services/ui-src/src/measures/2024/OUDHH/index.tsx new file mode 100644 index 0000000000..b94422f0cc --- /dev/null +++ b/services/ui-src/src/measures/2024/OUDHH/index.tsx @@ -0,0 +1,66 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { FormData } from "./types"; + +export const OUDHH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/OUDHH/types.ts b/services/ui-src/src/measures/2024/OUDHH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/OUDHH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/OUDHH/validation.ts b/services/ui-src/src/measures/2024/OUDHH/validation.ts new file mode 100644 index 0000000000..7a1b0312c5 --- /dev/null +++ b/services/ui-src/src/measures/2024/OUDHH/validation.ts @@ -0,0 +1,74 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const OUDValidation = (data: FormData) => { + const OPM = data[DC.OPM_RATES]; + const ageGroups = PMD.qualifiers; + const performanceMeasureArray = + GV.getPerfMeasureRateArray(data, PMD.data) ?? []; + const dateRange = data[DC.DATE_RANGE]; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + PMD.qualifiers + ), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + PMD.qualifiers + ), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateEqualCategoryDenominatorsPM(data, PMD.categories, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateEqualQualifierDenominatorsOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateEqualCategoryDenominatorsOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [OUDValidation]; diff --git a/services/ui-src/src/measures/2024/PCRAD/data.ts b/services/ui-src/src/measures/2024/PCRAD/data.ts new file mode 100644 index 0000000000..e8ad1732a8 --- /dev/null +++ b/services/ui-src/src/measures/2024/PCRAD/data.ts @@ -0,0 +1,17 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("PCR-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "For beneficiaries ages 18 to 64, the number of acute inpatient and observation stays during the measurement year that were followed by an unplanned acute readmission for any diagnosis within 30 days and the predicted probability of an acute readmission. Data are reported in the following categories:", + ], + questionListItems: [ + "Count of Index Hospital Stays (IHS)", + "Count of Observed 30-Day Readmissions", + "Count of Expected 30-Day Readmissions", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/PCRAD/index.test.tsx b/services/ui-src/src/measures/2024/PCRAD/index.test.tsx new file mode 100644 index 0000000000..0b04212ae5 --- /dev/null +++ b/services/ui-src/src/measures/2024/PCRAD/index.test.tsx @@ -0,0 +1,284 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "PCR-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.PCRatLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.PCRnoNonZeroNumOrDenom).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.PCRatLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.PCRnoNonZeroNumOrDenom).toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + value: "1", + label: "Count of Index Hospital Stays", + id: 0, + }, + { + value: "1", + label: "Count of Observed 30-Day Readmissions", + id: 1, + }, + { + value: "100.0000", + label: "Observed Readmission Rate", + id: 2, + }, + { + value: "1.3333", + label: "Count of Expected 30-Day Readmissions", + id: 3, + }, + { + value: "133.3300", + label: "Expected Readmission Rate", + id: 4, + }, + { + value: "0.7500", + label: + "O/E Ratio (Count of Observed 30-Day Readmissions/Count of Expected 30-Day Readmissions)", + id: 5, + }, + { + value: "1", + label: "Count of Beneficiaries in Medicaid Population", + id: 6, + }, + { + value: "2", + label: "Number of Outliers", + id: 7, + }, + { + value: "2000.0", + label: + "Outlier Rate (Number of Outliers/Count of Beneficiaries in Medicaid Population) x 1,000", + id: 8, + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/PCRAD/index.tsx b/services/ui-src/src/measures/2024/PCRAD/index.tsx new file mode 100644 index 0000000000..8d087d85be --- /dev/null +++ b/services/ui-src/src/measures/2024/PCRAD/index.tsx @@ -0,0 +1,54 @@ +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { PCRADPerformanceMeasure } from "./questions/PerformanceMeasure"; +import { useEffect } from "react"; +import { validationFunctions } from "./validation"; + +export const PCRAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && } + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/PCRAD/questions/PerformanceMeasure.tsx b/services/ui-src/src/measures/2024/PCRAD/questions/PerformanceMeasure.tsx new file mode 100644 index 0000000000..c55bde1720 --- /dev/null +++ b/services/ui-src/src/measures/2024/PCRAD/questions/PerformanceMeasure.tsx @@ -0,0 +1,196 @@ +import * as QMR from "components"; +import * as CUI from "@chakra-ui/react"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import * as Types from "../../shared/CommonQuestions/types"; +import { PerformanceMeasureData } from "../../shared/CommonQuestions/PerformanceMeasure/data"; +import { useWatch } from "react-hook-form"; +import { PCRRate } from "components/PCRRate"; +import { LabelData } from "utils"; +import * as DC from "dataConstants"; + +interface Props { + data: PerformanceMeasureData; + rateReadOnly?: boolean; + calcTotal?: boolean; + rateScale?: number; + customMask?: RegExp; +} + +interface NdrSetProps { + categories?: LabelData[]; + qualifiers?: LabelData[]; + rateReadOnly: boolean; + calcTotal: boolean; + rateScale?: number; + customMask?: RegExp; +} + +/** Maps over the categories given and creates rate sets based on the qualifiers, with a default of one rate */ +const CategoryNdrSets = ({ + rateReadOnly, + categories = [], + qualifiers, + rateScale, + customMask, +}: NdrSetProps) => { + const register = useCustomRegister(); + + return ( + <> + {categories.map((cat) => { + let rates: QMR.IRate[] | undefined = qualifiers?.map((qual, idx) => ({ + label: qual.label, + uid: `${cat.id}.${qual.id}`, + id: idx, + })); + + rates = rates?.length ? rates : [{ id: 0 }]; + + return ( + <> + + {cat.label} + + + + ); + })} + + ); +}; + +/** If no categories, we still need a rate for the PM + * 2023 and onward, categories are expected to have at least object filled for creating uid in database + */ +const QualifierNdrSets = ({ + rateReadOnly, + categories = [], + qualifiers = [], + customMask, +}: NdrSetProps) => { + const register = useCustomRegister(); + const categoryID = categories[0]?.id ? categories[0].id : DC.SINGLE_CATEGORY; + + const rates: QMR.IRate[] = qualifiers.map((item, idx) => ({ + label: item.label, + uid: `${categoryID}.${item.id}`, + id: idx, + })); + + return ( + + ); +}; + +/** Creates the NDR sets based on given categories and qualifiers */ +const PerformanceMeasureNdrs = (props: NdrSetProps) => { + let ndrSets; + + //if there is a category and the category labels are filled out, create the NDR using the categories array + if ( + props.categories?.length && + props.categories?.some((item) => item.label) + ) { + ndrSets = ; + } else if (props.qualifiers?.length) { + ndrSets = ; + } + + return {ndrSets}; +}; + +/** Data Driven Performance Measure Comp */ +export const PCRADPerformanceMeasure = ({ + data, + calcTotal = false, + rateReadOnly, + rateScale, + customMask, +}: Props) => { + const register = useCustomRegister(); + const dataSourceWatch = useWatch({ name: "DataSource" }) as + | string[] + | undefined; + const readOnly = + rateReadOnly ?? + dataSourceWatch?.every((source) => source === "AdministrativeData") ?? + true; + + return ( + + {data.questionText} + {data.questionListItems && ( + + {data.questionListItems.map((item, idx) => { + return ( + + {data.questionListTitles?.[idx] && ( + + {data.questionListTitles?.[idx]} + + )} + {item} + + ); + })} + + )} + + For beneficiaries ages 18 to 64, states should also report the rate of + beneficiaries who are identified as outliers based on high rates of + inpatient and observation stays during the measurement year. Data are + reported in the following categories: + + + {[ + "Count of Beneficiaries in Medicaid Population", + "Number of Outliers", + ].map((item, idx) => { + return ( + + {data.questionListTitles?.[idx] && ( + + {data.questionListTitles?.[idx]} + + )} + {item} + + ); + })} + + + + Enter values below: + + + + ); +}; diff --git a/services/ui-src/src/measures/2024/PCRAD/types.ts b/services/ui-src/src/measures/2024/PCRAD/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/PCRAD/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/PCRAD/validation.ts b/services/ui-src/src/measures/2024/PCRAD/validation.ts new file mode 100644 index 0000000000..c292635ebd --- /dev/null +++ b/services/ui-src/src/measures/2024/PCRAD/validation.ts @@ -0,0 +1,109 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const ndrForumlas = [ + { + numerator: 1, + denominator: 0, + rateIndex: 2, + }, + { + numerator: 3, + denominator: 0, + rateIndex: 4, + }, + { + numerator: 1, + denominator: 3, + rateIndex: 5, + }, + { + numerator: 7, + denominator: 6, + rateIndex: 8, + }, +]; + +const PCRADValidation = (data: FormData) => { + let errorArray: any[] = []; + const ageGroups = PMD.qualifiers; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const OPM = data[DC.OPM_RATES]; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + // Quick reference list of all rate indices + // const rateLocations = ndrForumlas.map((ndr) => ndr.rateIndex); + errorArray = [ + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + + // Performance Measure Validations + ...GV.PCRatLeastOneRateComplete(performanceMeasureArray, OPM, ageGroups), + ...GV.PCRnoNonZeroNumOrDenom(performanceMeasureArray, OPM, ndrForumlas), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [OMSValidations], + }), + ]; + return errorArray; +}; + +const OMSValidations: GV.Types.OmsValidationCallback = ({ + rateData, + locationDictionary, + label, +}) => { + const rates = Object.keys(rateData?.rates ?? {}).map((x) => { + return { rate: [rateData?.rates?.[x].OPM[0]] }; + }); + return [ + ...GV.PCRnoNonZeroNumOrDenom( + [rateData?.["pcr-rate"] ?? []], + rates ?? [], + ndrForumlas, + `Optional Measure Stratification: ${locationDictionary(label)}` + ), + ...GV.PCRatLeastOneRateComplete( + [rateData?.["pcr-rate"] ?? []], + rates ?? [], + PMD.qualifiers, + `Optional Measure Stratification: ${locationDictionary(label)}`, + true + ), + ]; +}; + +export const validationFunctions = [PCRADValidation]; diff --git a/services/ui-src/src/measures/2024/PCRHH/data.ts b/services/ui-src/src/measures/2024/PCRHH/data.ts new file mode 100644 index 0000000000..9fbc3ec16c --- /dev/null +++ b/services/ui-src/src/measures/2024/PCRHH/data.ts @@ -0,0 +1,17 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("PCR-HH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "For health home enrollees ages 18 to 64, the number of acute inpatient and observation stays during the measurement year that were followed by an unplanned acute readmission for any diagnosis within 30 days and the predicted probability of an acute readmission. Data are reported in the following categories:", + ], + questionListItems: [ + "Count of Index Hospital Stays (IHS) ", + "Count of Observed 30-Day Readmissions", + "Count of Expected 30-Day Readmissions", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/PCRHH/index.test.tsx b/services/ui-src/src/measures/2024/PCRHH/index.test.tsx new file mode 100644 index 0000000000..eb8b93242d --- /dev/null +++ b/services/ui-src/src/measures/2024/PCRHH/index.test.tsx @@ -0,0 +1,276 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "PCR-HH"; +const coreSet = "HHCS"; +const state = "DC"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.ComplexValidateDualPopInformation).not.toHaveBeenCalled(); + expect(V.ComplexNoNonZeroNumOrDenom).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.ComplexValidateNDRTotals).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.ComplexAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.ComplexNoNonZeroNumOrDenom).not.toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.ComplexValidateNDRTotals).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + value: "1", + label: "Count of Index Hospital Stays", + id: 0, + }, + { + value: "1", + label: "Count of Observed 30-Day Readmissions", + id: 1, + }, + { + value: "100.0000", + label: "Observed Readmission Rate", + id: 2, + }, + { + value: "1.0000", + label: "Count of Expected 30-Day Readmissions", + id: 3, + }, + { + value: "100.0000", + label: "Expected Readmission Rate", + id: 4, + }, + { + value: "1.0000", + label: + "O/E Ratio (Count of Observed 30-Day Readmissions/Count of Expected 30-Day Readmissions)", + id: 5, + }, + { + value: "1", + label: "Count of Enrollees in Health Home Population", + id: 6, + }, + { + value: "1", + label: "Number of Outliers", + id: 7, + }, + { + value: "1000.0", + label: + "Outlier Rate (Number of Outliers/Count of Enrollees in Health Home Population) x 1,000", + id: 8, + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/PCRHH/index.tsx b/services/ui-src/src/measures/2024/PCRHH/index.tsx new file mode 100644 index 0000000000..7b14df872f --- /dev/null +++ b/services/ui-src/src/measures/2024/PCRHH/index.tsx @@ -0,0 +1,55 @@ +import { useEffect } from "react"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { PCRHHPerformanceMeasure } from "./questions/PerformanceMeasure"; + +export const PCRHH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && } + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/PCRHH/questions/PerformanceMeasure.tsx b/services/ui-src/src/measures/2024/PCRHH/questions/PerformanceMeasure.tsx new file mode 100644 index 0000000000..86248e130d --- /dev/null +++ b/services/ui-src/src/measures/2024/PCRHH/questions/PerformanceMeasure.tsx @@ -0,0 +1,196 @@ +import * as CUI from "@chakra-ui/react"; +import * as Types from "measures/2024/shared/CommonQuestions/types"; +import * as QMR from "components"; +import { PerformanceMeasureData } from "measures/2024/shared/CommonQuestions/PerformanceMeasure/data"; +import { PCRRate } from "components/PCRRate"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import { useWatch } from "react-hook-form"; +import { LabelData } from "utils"; +import * as DC from "dataConstants"; + +interface Props { + data: PerformanceMeasureData; + rateReadOnly?: boolean; + calcTotal?: boolean; + rateScale?: number; + customMask?: RegExp; +} + +interface NdrSetProps { + categories?: LabelData[]; + qualifiers?: LabelData[]; + rateReadOnly: boolean; + calcTotal: boolean; + rateScale?: number; + customMask?: RegExp; +} + +/** Maps over the categories given and creates rate sets based on the qualifiers, with a default of one rate */ +const CategoryNdrSets = ({ + rateReadOnly, + categories = [], + qualifiers, + rateScale, + customMask, +}: NdrSetProps) => { + const register = useCustomRegister(); + + return ( + <> + {categories.map((cat) => { + let rates: QMR.IRate[] | undefined = qualifiers?.map((qual, idx) => ({ + label: qual.label, + uid: `${cat.id}.${qual.id}`, + id: idx, + })); + + rates = rates?.length ? rates : [{ id: 0 }]; + + return ( + <> + + {cat.label} + + + + ); + })} + + ); +}; + +/** If no categories, we still need a rate for the PM + * 2023 and onward, categories should have 1 LabelData object filled for creating uid in database + */ +const QualifierNdrSets = ({ + rateReadOnly, + categories = [], + qualifiers = [], + customMask, +}: NdrSetProps) => { + const register = useCustomRegister(); + const categoryID = categories[0]?.id ? categories[0].id : DC.SINGLE_CATEGORY; + + const rates: QMR.IRate[] = qualifiers.map((item, idx) => ({ + label: item.label, + uid: `${categoryID}.${item.id}`, + id: idx, + })); + + return ( + + ); +}; + +/** Creates the NDR sets based on given categories and qualifiers */ +const PerformanceMeasureNdrs = (props: NdrSetProps) => { + let ndrSets; + + //if there is a category and the category labels are filled out, create the NDR using the categories array + if ( + props.categories?.length && + props.categories?.some((item) => item.label) + ) { + ndrSets = ; + } else if (props.qualifiers?.length) { + ndrSets = ; + } + + return {ndrSets}; +}; + +/** Data Driven Performance Measure Comp */ +export const PCRHHPerformanceMeasure = ({ + data, + calcTotal = false, + rateReadOnly, + rateScale, + customMask, +}: Props) => { + const register = useCustomRegister(); + const dataSourceWatch = useWatch({ name: "DataSource" }) as + | string[] + | undefined; + const readOnly = + rateReadOnly ?? + dataSourceWatch?.every((source) => source === "AdministrativeData") ?? + true; + + return ( + + {data.questionText} + {data.questionListItems && ( + + {data.questionListItems.map((item, idx) => { + return ( + + {data.questionListTitles?.[idx] && ( + + {data.questionListTitles?.[idx]} + + )} + {item} + + ); + })} + + )} + + For Health Home enrollees ages 18 to 64, states should also report the + rate of enrollees who are identified as outliers based on high rates of + inpatient and observation stays during the measurement year. Data are + reported in the following categories: + + + {[ + "Count of Enrollees in Health Home Population", + "Number of Outliers", + ].map((item, idx) => { + return ( + + {data.questionListTitles?.[idx] && ( + + {data.questionListTitles?.[idx]} + + )} + {item} + + ); + })} + + + + Enter values below: + + + + ); +}; diff --git a/services/ui-src/src/measures/2024/PCRHH/types.ts b/services/ui-src/src/measures/2024/PCRHH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/PCRHH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/PCRHH/validation.ts b/services/ui-src/src/measures/2024/PCRHH/validation.ts new file mode 100644 index 0000000000..ae9c76b3bb --- /dev/null +++ b/services/ui-src/src/measures/2024/PCRHH/validation.ts @@ -0,0 +1,109 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const ndrForumlas = [ + { + numerator: 1, + denominator: 0, + rateIndex: 2, + }, + { + numerator: 3, + denominator: 0, + rateIndex: 4, + }, + { + numerator: 1, + denominator: 3, + rateIndex: 5, + }, + { + numerator: 7, + denominator: 6, + rateIndex: 8, + }, +]; + +const PCRHHValidation = (data: FormData) => { + let errorArray: any[] = []; + const ageGroups = PMD.qualifiers; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const OPM = data[DC.OPM_RATES]; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + // Quick reference list of all rate indices + // const rateLocations = ndrForumlas.map((ndr) => ndr.rateIndex); + errorArray = [ + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + // Performance Measure Validations + ...GV.PCRatLeastOneRateComplete(performanceMeasureArray, OPM, ageGroups), + ...GV.PCRnoNonZeroNumOrDenom(performanceMeasureArray, OPM, ndrForumlas), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [OMSValidations], + }), + ]; + return errorArray; +}; + +const OMSValidations: GV.Types.OmsValidationCallback = ({ + rateData, + locationDictionary, + label, +}) => { + const rates = Object.keys(rateData?.rates ?? {}).map((x) => { + return { rate: [rateData?.rates?.[x].OPM[0]] }; + }); + return [ + ...GV.PCRnoNonZeroNumOrDenom( + [rateData?.["pcr-rate"] ?? []], + rates ?? [], + ndrForumlas, + `Optional Measure Stratification: ${locationDictionary(label)}` + ), + ...GV.PCRatLeastOneRateComplete( + [rateData?.["pcr-rate"] ?? []], + rates ?? [], + PMD.qualifiers, + `Optional Measure Stratification: ${locationDictionary(label)}`, + true + ), + ]; +}; + +export const validationFunctions = [PCRHHValidation]; diff --git a/services/ui-src/src/measures/2024/PPCAD/data.ts b/services/ui-src/src/measures/2024/PPCAD/data.ts new file mode 100644 index 0000000000..37d536ecca --- /dev/null +++ b/services/ui-src/src/measures/2024/PPCAD/data.ts @@ -0,0 +1,71 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("PPC-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of deliveries of live births on or between October 8 of the year prior to the measurement year and October 7 of the measurement year that had a postpartum visit on or between 7 and 84 days after delivery.", + ], + questionListItems: [], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.HYBRID_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + + { + value: DC.OTHER, + description: true, + }, + ], + }, + { + label: "What is the Medical Records Data Source?", + options: [ + { + value: DC.EHR_DATA, + }, + { + value: DC.PAPER, + }, + ], + }, + ], + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/PPCAD/index.test.tsx b/services/ui-src/src/measures/2024/PPCAD/index.test.tsx new file mode 100644 index 0000000000..96cffc8181 --- /dev/null +++ b/services/ui-src/src/measures/2024/PPCAD/index.test.tsx @@ -0,0 +1,258 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "PPC-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/PPCAD/index.tsx b/services/ui-src/src/measures/2024/PPCAD/index.tsx new file mode 100644 index 0000000000..1bf62b0783 --- /dev/null +++ b/services/ui-src/src/measures/2024/PPCAD/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { FormData } from "./types"; + +export const PPCAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/PPCAD/types.ts b/services/ui-src/src/measures/2024/PPCAD/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/PPCAD/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/PPCAD/validation.ts b/services/ui-src/src/measures/2024/PPCAD/validation.ts new file mode 100644 index 0000000000..fc024c5f4f --- /dev/null +++ b/services/ui-src/src/measures/2024/PPCAD/validation.ts @@ -0,0 +1,74 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const PPCADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = + GV.getPerfMeasureRateArray(data, PMD.data) ?? []; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + dataSource: data[DC.DATA_SOURCE], + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + ], + }), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ]; + + return errorArray; +}; + +export const validationFunctions = [PPCADValidation]; diff --git a/services/ui-src/src/measures/2024/PPCCH/data.ts b/services/ui-src/src/measures/2024/PPCCH/data.ts new file mode 100644 index 0000000000..db19abf7c8 --- /dev/null +++ b/services/ui-src/src/measures/2024/PPCCH/data.ts @@ -0,0 +1,76 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("PPC-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of deliveries of live births on or between October 8 of the year prior to the measurement year and October 7 of the measurement year that received a prenatal care visit in the first trimester, on or before the enrollment start date or within 42 days of enrollment in Medicaid/CHIP.", + ], + questionListItems: [], + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.VITAL_DATA_SOURCE, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.HYBRID_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.VITAL_DATA_SOURCE, + }, + { + value: DC.OTHER, + description: true, + }, + ], + }, + { + label: "What is the Medical Records Data Source?", + options: [ + { + value: DC.EHR_DATA, + }, + { + value: DC.PAPER, + }, + ], + }, + ], + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/PPCCH/index.test.tsx b/services/ui-src/src/measures/2024/PPCCH/index.test.tsx new file mode 100644 index 0000000000..e90f9ee4ab --- /dev/null +++ b/services/ui-src/src/measures/2024/PPCCH/index.test.tsx @@ -0,0 +1,269 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "PPC-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualOMS).not.toHaveBeenCalled(); + expect(V.validateOneQualRateHigherThanOtherQualPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + LongactingreversiblemethodofcontraceptionLARC: [ + { + label: "Three Days Postpartum Rate", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + { + label: "Sixty Days Postpartum Rate", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + ], + Mosteffectiveormoderatelyeffectivemethodofcontraception: [ + { + label: "Three Days Postpartum Rate", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + { + label: "Sixty Days Postpartum Rate", + rate: "100.0", + numerator: "1", + denominator: "1", + }, + ], + }, + }, + MeasurementSpecification: "OPA", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/PPCCH/index.tsx b/services/ui-src/src/measures/2024/PPCCH/index.tsx new file mode 100644 index 0000000000..d8938dc4d7 --- /dev/null +++ b/services/ui-src/src/measures/2024/PPCCH/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { FormData } from "./types"; + +export const PPCCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/PPCCH/types.ts b/services/ui-src/src/measures/2024/PPCCH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/PPCCH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/PPCCH/validation.ts b/services/ui-src/src/measures/2024/PPCCH/validation.ts new file mode 100644 index 0000000000..89e7286e86 --- /dev/null +++ b/services/ui-src/src/measures/2024/PPCCH/validation.ts @@ -0,0 +1,74 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const PPCCHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = + GV.getPerfMeasureRateArray(data, PMD.data) ?? []; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + dataSource: data[DC.DATA_SOURCE], + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateNotZeroOMS(), + GV.validateRateZeroOMS(), + ], + }), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ]; + + return errorArray; +}; + +export const validationFunctions = [PPCCHValidation]; diff --git a/services/ui-src/src/measures/2024/PQI01AD/data.ts b/services/ui-src/src/measures/2024/PQI01AD/data.ts new file mode 100644 index 0000000000..1f69e3bbf1 --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI01AD/data.ts @@ -0,0 +1,12 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("PQI01-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Hospitalizations for a principal diagnosis of diabetes with short-term complications (ketoacidosis, hyperosmolarity, or coma) per 100,000 beneficiary months for beneficiaries age 18 and older.", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/PQI01AD/index.test.tsx b/services/ui-src/src/measures/2024/PQI01AD/index.test.tsx new file mode 100644 index 0000000000..0a14aff236 --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI01AD/index.test.tsx @@ -0,0 +1,258 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "PQI01-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/PQI01AD/index.tsx b/services/ui-src/src/measures/2024/PQI01AD/index.tsx new file mode 100644 index 0000000000..6ffd2723a5 --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI01AD/index.tsx @@ -0,0 +1,82 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { positiveNumbersWithMaxDecimalPlaces } from "utils"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { FormData } from "./types"; + +export const PQI01AD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && ( + + )} + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/PQI01AD/types.ts b/services/ui-src/src/measures/2024/PQI01AD/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI01AD/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/PQI01AD/validation.ts b/services/ui-src/src/measures/2024/PQI01AD/validation.ts new file mode 100644 index 0000000000..d64f97c324 --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI01AD/validation.ts @@ -0,0 +1,84 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const PQI01Validation = (data: FormData) => { + const OPM = data[DC.OPM_RATES]; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const age65PlusIndex = 0; + const definitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + const validateDualPopInformationArray = [ + performanceMeasureArray?.[0].filter((pm) => { + return pm?.label === "Age 65 and older"; + }), + ]; + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + PMD.qualifiers, + PMD.categories + ), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, PMD.qualifiers), + ...GV.validateRateZeroPM( + performanceMeasureArray, + OPM, + PMD.qualifiers, + data + ), + ...GV.validateDualPopInformationPM( + validateDualPopInformationArray, + OPM, + age65PlusIndex, + definitionOfDenominator + ), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [PQI01Validation]; diff --git a/services/ui-src/src/measures/2024/PQI05AD/data.ts b/services/ui-src/src/measures/2024/PQI05AD/data.ts new file mode 100644 index 0000000000..153ddbc424 --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI05AD/data.ts @@ -0,0 +1,12 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("PQI05-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Hospitalizations with a principal diagnosis of chronic obstructive pulmonary disease (COPD) or asthma per 100,000 beneficiary months for beneficiaries age 40 and older.", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/PQI05AD/index.test.tsx b/services/ui-src/src/measures/2024/PQI05AD/index.test.tsx new file mode 100644 index 0000000000..f9aaa34578 --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI05AD/index.test.tsx @@ -0,0 +1,258 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "PQI05-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/PQI05AD/index.tsx b/services/ui-src/src/measures/2024/PQI05AD/index.tsx new file mode 100644 index 0000000000..fa8c7d27f1 --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI05AD/index.tsx @@ -0,0 +1,82 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { positiveNumbersWithMaxDecimalPlaces } from "utils"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { FormData } from "./types"; + +export const PQI05AD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && ( + + )} + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/PQI05AD/types.ts b/services/ui-src/src/measures/2024/PQI05AD/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI05AD/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/PQI05AD/validation.ts b/services/ui-src/src/measures/2024/PQI05AD/validation.ts new file mode 100644 index 0000000000..b4b81d1497 --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI05AD/validation.ts @@ -0,0 +1,89 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const PQI05Validation = (data: FormData) => { + const OPM = data[DC.OPM_RATES]; + const ageGroups = PMD.qualifiers; + const dateRange = data[DC.DATE_RANGE]; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + const age65PlusIndex = 0; + const validateDualPopInformationArray = [ + performanceMeasureArray?.[0].filter((pm) => { + return pm?.label === "Age 65 and older"; + }), + ]; + const definitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + errorArray = [ + ...errorArray, + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, PMD.qualifiers), + ...GV.validateRateZeroPM( + performanceMeasureArray, + OPM, + PMD.qualifiers, + data + ), + ...GV.validateDualPopInformationPM( + validateDualPopInformationArray, + OPM, + 1, + ageGroups.map((item) => item.label) + ), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateDualPopInformationPM( + validateDualPopInformationArray, + OPM, + age65PlusIndex, + definitionOfDenominator + ), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [PQI05Validation]; diff --git a/services/ui-src/src/measures/2024/PQI08AD/data.ts b/services/ui-src/src/measures/2024/PQI08AD/data.ts new file mode 100644 index 0000000000..07b56d8793 --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI08AD/data.ts @@ -0,0 +1,12 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("PQI08-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Hospitalizations with a principal diagnosis of heart failure per 100,000 beneficiary months for beneficiaries age 18 and older.", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/PQI08AD/index.test.tsx b/services/ui-src/src/measures/2024/PQI08AD/index.test.tsx new file mode 100644 index 0000000000..00037624cf --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI08AD/index.test.tsx @@ -0,0 +1,258 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "PQI08-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/PQI08AD/index.tsx b/services/ui-src/src/measures/2024/PQI08AD/index.tsx new file mode 100644 index 0000000000..3aef3df84d --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI08AD/index.tsx @@ -0,0 +1,82 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { positiveNumbersWithMaxDecimalPlaces } from "utils"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { FormData } from "./types"; + +export const PQI08AD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && ( + + )} + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/PQI08AD/types.ts b/services/ui-src/src/measures/2024/PQI08AD/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI08AD/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/PQI08AD/validation.ts b/services/ui-src/src/measures/2024/PQI08AD/validation.ts new file mode 100644 index 0000000000..b205969e96 --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI08AD/validation.ts @@ -0,0 +1,80 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const PQI08Validation = (data: FormData) => { + const OPM = data[DC.OPM_RATES]; + const age65PlusIndex = 0; + const dateRange = data[DC.DATE_RANGE]; + const definitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const validateDualPopInformationArray = [ + performanceMeasureArray?.[0].filter((pm) => { + return pm?.label === "Age 65 and older"; + }), + ]; + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + PMD.qualifiers, + PMD.categories + ), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateDualPopInformationPM( + validateDualPopInformationArray, + OPM, + age65PlusIndex, + definitionOfDenominator + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, PMD.qualifiers), + ...GV.validateRateZeroPM( + performanceMeasureArray, + OPM, + PMD.qualifiers, + data + ), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [PQI08Validation]; diff --git a/services/ui-src/src/measures/2024/PQI15AD/data.ts b/services/ui-src/src/measures/2024/PQI15AD/data.ts new file mode 100644 index 0000000000..3f792bacc1 --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI15AD/data.ts @@ -0,0 +1,12 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("PQI15-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Hospitalizations with a principal diagnosis of asthma per 100,000 beneficiary months for beneficiaries ages 18 to 39.", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/PQI15AD/index.test.tsx b/services/ui-src/src/measures/2024/PQI15AD/index.test.tsx new file mode 100644 index 0000000000..719cd2076b --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI15AD/index.test.tsx @@ -0,0 +1,258 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "PQI15-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/PQI15AD/index.tsx b/services/ui-src/src/measures/2024/PQI15AD/index.tsx new file mode 100644 index 0000000000..9c8cd5755d --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI15AD/index.tsx @@ -0,0 +1,81 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { positiveNumbersWithMaxDecimalPlaces } from "utils"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { FormData } from "./types"; + +export const PQI15AD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && ( + + )} + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/PQI15AD/types.ts b/services/ui-src/src/measures/2024/PQI15AD/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI15AD/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/PQI15AD/validation.ts b/services/ui-src/src/measures/2024/PQI15AD/validation.ts new file mode 100644 index 0000000000..6ae1331cbe --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI15AD/validation.ts @@ -0,0 +1,64 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const PQI15Validation = (data: FormData) => { + const OPM = data[DC.OPM_RATES]; + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const dateRange = data[DC.DATE_RANGE]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + let errorArray: any[] = []; + + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + errorArray = [ + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [PQI15Validation]; diff --git a/services/ui-src/src/measures/2024/PQI92HH/data.ts b/services/ui-src/src/measures/2024/PQI92HH/data.ts new file mode 100644 index 0000000000..c76af75d17 --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI92HH/data.ts @@ -0,0 +1,12 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("PQI92-HH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Hospitalizations for ambulatory care sensitive chronic conditions per 100,000 enrollee months for health home enrollees age 18 and older. This measure includes adult hospitalizations for diabetes with short-term complications, diabetes with long-term complications, uncontrolled diabetes without complications, diabetes with lower-extremity amputation, chronic obstructive pulmonary disease, asthma, hypertension, or heart failure without a cardiac procedure.", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/PQI92HH/index.test.tsx b/services/ui-src/src/measures/2024/PQI92HH/index.test.tsx new file mode 100644 index 0000000000..7943191d76 --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI92HH/index.test.tsx @@ -0,0 +1,258 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "PQI92-HH"; +const coreSet = "HHCS"; +const state = "DC"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/PQI92HH/index.tsx b/services/ui-src/src/measures/2024/PQI92HH/index.tsx new file mode 100644 index 0000000000..4e62df286a --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI92HH/index.tsx @@ -0,0 +1,84 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { positiveNumbersWithMaxDecimalPlaces } from "utils"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { FormData } from "./types"; + +export const PQI92HH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && ( + + )} + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/PQI92HH/types.ts b/services/ui-src/src/measures/2024/PQI92HH/types.ts new file mode 100644 index 0000000000..7b8b8b9ad8 --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI92HH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2023/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/PQI92HH/validation.ts b/services/ui-src/src/measures/2024/PQI92HH/validation.ts new file mode 100644 index 0000000000..1a1cd1e6fe --- /dev/null +++ b/services/ui-src/src/measures/2024/PQI92HH/validation.ts @@ -0,0 +1,83 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const PQI92Validation = (data: FormData) => { + const definitionOfDenominator = data[DC.DEFINITION_OF_DENOMINATOR]; + const OPM = data[DC.OPM_RATES]; + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + const dateRange = data[DC.DATE_RANGE]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + let errorArray: any[] = []; + + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + const validateDualPopInformationArray = [ + performanceMeasureArray?.[0].filter((pm) => { + return pm?.label === "Age 65 and older"; + }), + ]; + + const age65PlusIndex = 0; + + errorArray = [ + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + // Performance Measure Validations + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateDualPopInformationPM( + validateDualPopInformationArray, + OPM, + age65PlusIndex, + definitionOfDenominator + ), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateTotalNDR(performanceMeasureArray), + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateOMSTotalNDR(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [PQI92Validation]; diff --git a/services/ui-src/src/measures/2024/SAAAD/data.ts b/services/ui-src/src/measures/2024/SAAAD/data.ts new file mode 100644 index 0000000000..531d5a27fc --- /dev/null +++ b/services/ui-src/src/measures/2024/SAAAD/data.ts @@ -0,0 +1,13 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("SAA-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of beneficiaries age 18 and older during the measurement year with schizophrenia or schizoaffective disorder who were dispensed and remained on an antipsychotic medication for at least 80 percent of their treatment period.", + ], + questionListItems: [], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/SAAAD/index.test.tsx b/services/ui-src/src/measures/2024/SAAAD/index.test.tsx new file mode 100644 index 0000000000..a5915344e7 --- /dev/null +++ b/services/ui-src/src/measures/2024/SAAAD/index.test.tsx @@ -0,0 +1,258 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "SAA-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/SAAAD/index.tsx b/services/ui-src/src/measures/2024/SAAAD/index.tsx new file mode 100644 index 0000000000..99ef8af9da --- /dev/null +++ b/services/ui-src/src/measures/2024/SAAAD/index.tsx @@ -0,0 +1,67 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +import { FormData } from "./types"; + +export const SAAAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/SAAAD/types.ts b/services/ui-src/src/measures/2024/SAAAD/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/SAAAD/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/SAAAD/validation.ts b/services/ui-src/src/measures/2024/SAAAD/validation.ts new file mode 100644 index 0000000000..3d957fdb15 --- /dev/null +++ b/services/ui-src/src/measures/2024/SAAAD/validation.ts @@ -0,0 +1,75 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const SAAADValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [SAAADValidation]; diff --git a/services/ui-src/src/measures/2024/SFMCH/data.ts b/services/ui-src/src/measures/2024/SFMCH/data.ts new file mode 100644 index 0000000000..8dfcec5f31 --- /dev/null +++ b/services/ui-src/src/measures/2024/SFMCH/data.ts @@ -0,0 +1,13 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("SFM-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of enrolled children who have ever received sealants on permanent first molar teeth: (1) at least one sealant and (2) all four molars sealed by the 10th birthdate.", + ], + questionListItems: [], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/SFMCH/index.test.tsx b/services/ui-src/src/measures/2024/SFMCH/index.test.tsx new file mode 100644 index 0000000000..b03b986e3e --- /dev/null +++ b/services/ui-src/src/measures/2024/SFMCH/index.test.tsx @@ -0,0 +1,268 @@ +import { act, fireEvent, screen, waitFor } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "SFM-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsPM).toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsPM).toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateEqualCategoryDenominatorsOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + it("should not allow non state users to edit forms by disabling buttons", async () => { + useApiMock(apiData); + renderWithHookForm(component); + + expect(screen.getByTestId("measure-wrapper-form")).toBeInTheDocument(); + const completeButton = screen.getByText("Complete Measure"); + fireEvent.click(completeButton); + expect(completeButton).toHaveAttribute("disabled"); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Rate 1 - At Least One Sealant", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Rate 2 - All Four Molars Sealed", + }, + ], + }, + }, + MeasurementSpecification: "ADA-DQA", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/SFMCH/index.tsx b/services/ui-src/src/measures/2024/SFMCH/index.tsx new file mode 100644 index 0000000000..3b453df39d --- /dev/null +++ b/services/ui-src/src/measures/2024/SFMCH/index.tsx @@ -0,0 +1,67 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +import { FormData } from "./types"; + +export const SFMCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/SFMCH/types.ts b/services/ui-src/src/measures/2024/SFMCH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/SFMCH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/SFMCH/validation.ts b/services/ui-src/src/measures/2024/SFMCH/validation.ts new file mode 100644 index 0000000000..71c9160af0 --- /dev/null +++ b/services/ui-src/src/measures/2024/SFMCH/validation.ts @@ -0,0 +1,96 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const SFMCHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const dateRange = data[DC.DATE_RANGE]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + let errorArray: any[] = []; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + let unfilteredSameDenominatorErrors: any[] = []; + for (let i = 0; i < performanceMeasureArray.length; i += 2) { + unfilteredSameDenominatorErrors = [ + ...unfilteredSameDenominatorErrors, + ...GV.validateEqualQualifierDenominatorsPM( + [performanceMeasureArray[i], performanceMeasureArray[i + 1]], + ageGroups + ), + ]; + } + + let filteredSameDenominatorErrors: any = []; + let errorList: string[] = []; + unfilteredSameDenominatorErrors.forEach((error) => { + if (!(errorList.indexOf(error.errorMessage) > -1)) { + errorList.push(error.errorMessage); + filteredSameDenominatorErrors.push(error); + } + }); + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateOneQualRateHigherThanOtherQualPM(data, PMD), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...filteredSameDenominatorErrors, + ...GV.validateEqualCategoryDenominatorsPM(data, PMD.categories, ageGroups), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateOneQualRateHigherThanOtherQualOMS(), + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateEqualCategoryDenominatorsOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [SFMCHValidation]; diff --git a/services/ui-src/src/measures/2024/SSDAD/data.ts b/services/ui-src/src/measures/2024/SSDAD/data.ts new file mode 100644 index 0000000000..fbf15e1649 --- /dev/null +++ b/services/ui-src/src/measures/2024/SSDAD/data.ts @@ -0,0 +1,13 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("SSD-AD"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of beneficiaries ages 18 to 64 with schizophrenia, schizoaffective disorder, or bipolar disorder, who were dispensed an antipsychotic medication and had a diabetes screening test during the measurement year.", + ], + questionListItems: [], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/SSDAD/index.test.tsx b/services/ui-src/src/measures/2024/SSDAD/index.test.tsx new file mode 100644 index 0000000000..11fc8c0f63 --- /dev/null +++ b/services/ui-src/src/measures/2024/SSDAD/index.test.tsx @@ -0,0 +1,245 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "SSD-AD"; +const coreSet = "ACS"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateDualPopInformationPM).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 50 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 65 to 74", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/SSDAD/index.tsx b/services/ui-src/src/measures/2024/SSDAD/index.tsx new file mode 100644 index 0000000000..620ab95a5c --- /dev/null +++ b/services/ui-src/src/measures/2024/SSDAD/index.tsx @@ -0,0 +1,67 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +import { FormData } from "./types"; + +export const SSDAD = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/SSDAD/types.ts b/services/ui-src/src/measures/2024/SSDAD/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/SSDAD/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/SSDAD/validation.ts b/services/ui-src/src/measures/2024/SSDAD/validation.ts new file mode 100644 index 0000000000..7f2cd91a01 --- /dev/null +++ b/services/ui-src/src/measures/2024/SSDAD/validation.ts @@ -0,0 +1,74 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const SSDValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ]; + + return errorArray; +}; + +export const validationFunctions = [SSDValidation]; diff --git a/services/ui-src/src/measures/2024/SSHH/data.ts b/services/ui-src/src/measures/2024/SSHH/data.ts new file mode 100644 index 0000000000..b966ed5889 --- /dev/null +++ b/services/ui-src/src/measures/2024/SSHH/data.ts @@ -0,0 +1,62 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.HYBRID_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: "Other", + description: true, + }, + ], + }, + { + label: "What is the Medical Records Data Source?", + options: [ + { + value: DC.EHR_DATA, + }, + { + value: DC.PAPER, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/SSHH/index.test.tsx b/services/ui-src/src/measures/2024/SSHH/index.test.tsx new file mode 100644 index 0000000000..2f6eeda2e5 --- /dev/null +++ b/services/ui-src/src/measures/2024/SSHH/index.test.tsx @@ -0,0 +1,168 @@ +import { screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { axe, toHaveNoViolations } from "jest-axe"; +import { mockLDFlags } from "../../../../setupJest"; +import { clearMocks } from "../shared/util/validationsMock"; + +expect.extend(toHaveNoViolations); + +mockLDFlags.setDefault({ periodOfHealthEmergency2024: false }); + +// Test Setup +const measureAbbr = "SS-1-HH"; +const coreSet = "HHCS"; +const state = "CT"; +const year = 2024; +const description = "test"; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("SS-1-HH measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + it("Always shows What is the status of the data being reported? question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + const firstQuestion = screen.getByText( + "What is the status of the data being reported?" + ); + expect(firstQuestion).toBeVisible(); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByText("Status of Data Reported")).toBeInTheDocument(); + expect(screen.queryByText("Data Source")).toBeInTheDocument(); + expect(screen.queryByText("Date Range")).toBeInTheDocument(); + expect( + screen.queryByText("Definition of Population Included in the Measure") + ).toBeInTheDocument(); + expect(screen.queryByText("Performance Measure")).toBeInTheDocument(); + }); + + it("does not show covid text if periodOfHealthEmergency2024 flag is disabled", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect( + screen.queryByText( + "Describe any COVID-related difficulties encountered while collecting this data:" + ) + ).not.toBeInTheDocument(); + }); + + it("shows covid text if periodOfHealthEmergency2024 flag is enabled", async () => { + mockLDFlags.setDefault({ periodOfHealthEmergency2024: true }); + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect( + screen.queryByText( + "Describe any COVID-related difficulties encountered while collecting this data:" + ) + ).toBeInTheDocument(); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 19 to 50", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 51 to 64", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "110", + denominator: "110", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/SSHH/index.tsx b/services/ui-src/src/measures/2024/SSHH/index.tsx new file mode 100644 index 0000000000..37f7f68d2d --- /dev/null +++ b/services/ui-src/src/measures/2024/SSHH/index.tsx @@ -0,0 +1,37 @@ +import { useEffect } from "react"; +import * as CUI from "@chakra-ui/react"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as QMR from "components"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { PerformanceMeasure } from "./questions/PerformanceMeasure"; + +export const SSHH = ({ + detailedDescription, + setValidationFunctions, +}: QMR.MeasureWrapperProps) => { + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + return ( + <> + + {detailedDescription} + + + + + + + + + ); +}; diff --git a/services/ui-src/src/measures/2024/SSHH/questions/PerformanceMeasure.tsx b/services/ui-src/src/measures/2024/SSHH/questions/PerformanceMeasure.tsx new file mode 100644 index 0000000000..cd95c7ae06 --- /dev/null +++ b/services/ui-src/src/measures/2024/SSHH/questions/PerformanceMeasure.tsx @@ -0,0 +1,153 @@ +import { useFormContext, useFieldArray } from "react-hook-form"; +import * as CUI from "@chakra-ui/react"; +import * as QMR from "components"; +import * as DC from "dataConstants"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import * as Types from "measures/2024/shared/CommonQuestions/types"; +import { useEffect } from "react"; +import { useFlags } from "launchdarkly-react-client-sdk"; + +interface Props { + hybridMeasure?: boolean; + rateAlwaysEditable?: boolean; +} + +const stringIsReadOnly = (dataSource: string) => { + return dataSource === "AdministrativeData"; +}; + +const arrayIsReadOnly = (dataSource: string[]) => { + if (dataSource.length === 0) { + return false; + } + return ( + dataSource?.every((source) => source === "AdministrativeData") ?? false + ); +}; + +export const PerformanceMeasure = ({ + hybridMeasure, + rateAlwaysEditable, +}: Props) => { + const { control, reset } = useFormContext(); + + const pheIsCurrent = useFlags()?.["periodOfHealthEmergency2024"]; + + const { fields, remove, append } = useFieldArray({ + name: DC.OPM_RATES, + control, + shouldUnregister: true, + }); + + useEffect(() => { + if (fields.length === 0) { + reset({ + name: DC.OPM_RATES, + [DC.OPM_RATES]: [ + { + [DC.DESCRIPTION]: "", + }, + ], + }); + } + }, [fields, reset]); + + const register = useCustomRegister(); + + const { watch } = useFormContext(); + + // Watch for dataSource data + const dataSourceWatch = watch(DC.DATA_SOURCE); + + // Conditional check to let rate be readonly when administrative data is the only option or no option is selected + let rateReadOnly = false; + if (rateAlwaysEditable !== undefined) { + rateReadOnly = false; + } else if (dataSourceWatch && Array.isArray(dataSourceWatch)) { + rateReadOnly = arrayIsReadOnly(dataSourceWatch); + } else if (dataSourceWatch) { + rateReadOnly = stringIsReadOnly(dataSourceWatch); + } + + return ( + + + {hybridMeasure && pheIsCurrent && ( + + + CMS recognizes that social distancing will make onsite medical chart + reviews inadvisable during the COVID-19 pandemic. As such, hybrid + measures that rely on such techniques will be particularly + challenging during this time. While reporting of the Core Sets is + voluntary, CMS encourages states that can collect information safely + to continue reporting the measures they have reported in the past. + + + + )} + + + {fields.map((_item, index) => { + return ( + remove(index)} + key={_item.id} + > + + + Describe the Rate: + + + + Enter a number for the numerator and the denominator. Rate + will auto-calculate: + + {(dataSourceWatch?.[0] !== "AdministrativeData" || + dataSourceWatch?.length !== 1) && ( + + Please review the auto-calculated rate and revise if needed. + + )} + + + + ); + })} + + { + append({}); + }} + /> + + + ); +}; diff --git a/services/ui-src/src/measures/2024/SSHH/types.ts b/services/ui-src/src/measures/2024/SSHH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/SSHH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/SSHH/validation.ts b/services/ui-src/src/measures/2024/SSHH/validation.ts new file mode 100644 index 0000000000..a7404c0773 --- /dev/null +++ b/services/ui-src/src/measures/2024/SSHH/validation.ts @@ -0,0 +1,136 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import { FormData } from "./types"; + +export interface FormRateField { + denominator?: string; + numerator?: string; + label?: string; + rate?: string; +} + +const validateAtLeastOneRateComplete = (data: any) => { + const errorArray: FormError[] = []; + const PMData = data[DC.OPM_RATES]; + + let rateCompletionError = true; + + PMData && + PMData.forEach((measure: any) => { + if (measure.rate && measure.rate[0] && measure.rate[0].rate) { + rateCompletionError = false; + } + }); + + if (rateCompletionError) { + errorArray.push({ + errorLocation: `Performance Measure`, + errorMessage: `At least one Performance Measure Numerator, Denominator, and Rate must be completed`, + }); + } + return errorArray; +}; + +// If a user manually over-rides a rate it must not violate two rules: +// It must be zero if the numerator is zero or +// It Must be greater than zero if the Num and Denom are greater than zero +const validateNoNonZeroNumOrDenomPM = (OPM: any, data: any) => { + const errorArray: FormError[] = []; + const hybridData = data?.[DC.DATA_SOURCE]?.includes( + DC.HYBRID_ADMINSTRATIVE_AND_MEDICAL_RECORDS_DATA + ); + const location = `Performance Measure`; + const rateDataOPM = GV.getOtherPerformanceMeasureRateArray(OPM); + + const nonZeroErrors = [ + ...GV.validationRateNotZero({ location, rateData: rateDataOPM }), + ]; + const zeroErrors = [ + ...GV.validationRateZero({ location, rateData: rateDataOPM, hybridData }), + ]; + + if (!!nonZeroErrors.length) errorArray.push(nonZeroErrors[0]); + if (!!zeroErrors.length) errorArray.push(zeroErrors[0]); + return errorArray; +}; + +/** + * Checks user-created performance measures for numerator greater than denominator errors + */ +const validateNumeratorsLessThanDenominatorsPM = (OPM: any) => { + const location = `Performance Measure`; + const errorMessage = `Numerators must be less than Denominators for all applicable performance measures`; + const rateDataOPM = GV.getOtherPerformanceMeasureRateArray(OPM); + + const errorArray: FormError[] = []; + + for (const fieldSet of rateDataOPM) { + for (const [, rate] of fieldSet.entries()) { + if ( + rate.numerator && + rate.denominator && + parseFloat(rate.denominator) < parseFloat(rate.numerator) + ) { + errorArray.push({ + errorLocation: location, + errorMessage: errorMessage, + }); + } + } + } + + return !!errorArray.length ? [errorArray[0]] : []; +}; + +/** + * Checks for NDR field sets that have been partially filled out and reports them. + * + * @param OPM opm data + */ +export const validatePartialRateCompletion = (OPM: any) => { + const errors: FormError[] = []; + const rateDataOPM = GV.getOtherPerformanceMeasureRateArray(OPM); + + const location = `Performance Measure`; + + for (const [, rateSet] of rateDataOPM.entries()) { + for (const [, rate] of rateSet.entries()) { + if ( + rate && + ((rate.numerator && !rate.denominator) || + (rate.denominator && !rate.numerator)) + ) { + errors.push({ + errorLocation: location, + errorMessage: `Should not have partially filled NDR sets`, + }); + } + } + } + + return errors; +}; + +const SSHHValidation = (data: FormData) => { + let errorArray: any[] = []; + const OPM = data[DC.OPM_RATES]; + const dateRange = data[DC.DATE_RANGE]; + + errorArray = [ + ...validateAtLeastOneRateComplete(data), + ...validateNoNonZeroNumOrDenomPM(OPM, data), + ...validateNumeratorsLessThanDenominatorsPM(OPM), + ...validatePartialRateCompletion(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ]; + + return errorArray; +}; + +export const validationFunctions = [SSHHValidation]; diff --git a/services/ui-src/src/measures/2024/TFLCH/data.ts b/services/ui-src/src/measures/2024/TFLCH/data.ts new file mode 100644 index 0000000000..a0a3ec50ee --- /dev/null +++ b/services/ui-src/src/measures/2024/TFLCH/data.ts @@ -0,0 +1,13 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("TFL-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of enrolled children ages 1 through 20 who received at least two topical fluoride applications as: (1) dental or oral health services, (2) dental services, and (3) oral health services within the measurement year.", + ], + questionListItems: [], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/TFLCH/index.test.tsx b/services/ui-src/src/measures/2024/TFLCH/index.test.tsx new file mode 100644 index 0000000000..9a2747027d --- /dev/null +++ b/services/ui-src/src/measures/2024/TFLCH/index.test.tsx @@ -0,0 +1,349 @@ +import { fireEvent, screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "TFL-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsPM).toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + it("should not allow non state users to edit forms by disabling buttons", async () => { + useApiMock(apiData); + renderWithHookForm(component); + + expect(screen.getByTestId("measure-wrapper-form")).toBeInTheDocument(); + const completeButton = screen.getByText("Complete Measure"); + fireEvent.click(completeButton); + expect(completeButton).toHaveAttribute("disabled"); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + Dentalservices: [ + { + label: "Ages 1 to 2", + }, + { + label: "Ages 3 to 5", + }, + { + label: "Ages 6 to 7", + }, + { + label: "Ages 8 to 9", + }, + { + label: "Ages 10 to 11", + }, + { + label: "Ages 12 to 14", + }, + { + label: "Ages 15 to 18", + }, + { + label: "Ages 19 to 20", + }, + { + isTotal: true, + label: "Total Ages 1 through 20", + }, + ], + Dentalororalhealthservices: [ + { + label: "Ages 1 to 2", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 3 to 5", + }, + { + label: "Ages 6 to 7", + }, + { + label: "Ages 8 to 9", + }, + { + label: "Ages 10 to 11", + }, + { + label: "Ages 12 to 14", + }, + { + label: "Ages 15 to 18", + }, + { + label: "Ages 19 to 20", + }, + { + label: "Total Ages 1 through 20", + isTotal: true, + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + Oralhealthservices: [ + { + label: "Ages 1 to 2", + }, + { + label: "Ages 3 to 5", + }, + { + label: "Ages 6 to 7", + }, + { + label: "Ages 8 to 9", + }, + { + label: "Ages 10 to 11", + }, + { + label: "Ages 12 to 14", + }, + { + label: "Ages 15 to 18", + }, + { + label: "Ages 19 to 20", + }, + { + isTotal: true, + label: "Total Ages 1 through 20", + }, + ], + }, + }, + MeasurementSpecification: "ADA-DQA", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/TFLCH/index.tsx b/services/ui-src/src/measures/2024/TFLCH/index.tsx new file mode 100644 index 0000000000..24c250b73a --- /dev/null +++ b/services/ui-src/src/measures/2024/TFLCH/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +import { FormData } from "./types"; + +export const TFLCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/TFLCH/types.ts b/services/ui-src/src/measures/2024/TFLCH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/TFLCH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/TFLCH/validation.ts b/services/ui-src/src/measures/2024/TFLCH/validation.ts new file mode 100644 index 0000000000..bb96b09a59 --- /dev/null +++ b/services/ui-src/src/measures/2024/TFLCH/validation.ts @@ -0,0 +1,92 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const TFLCHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const dateRange = data[DC.DATE_RANGE]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + let errorArray: any[] = []; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + let sameDenominatorError = [ + ...GV.validateEqualQualifierDenominatorsPM( + performanceMeasureArray, + ageGroups + ), + ]; + sameDenominatorError = + sameDenominatorError.length > 0 ? [...sameDenominatorError] : []; + + errorArray = [ + ...errorArray, + // Dental Services rate cannot be larger than the Dental or Oral Health Services rate + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD.data), + // Oral Health Services rate cannot be larger than the Dental or Oral Health Services rate + ...GV.validateOneCatRateHigherThanOtherCatPM(data, PMD.data, 0, 2), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateTotalNDR(performanceMeasureArray), + ...sameDenominatorError, + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + // Dental Services rate cannot be larger than the Dental or Oral Health Services rate + GV.validateOneCatRateHigherThanOtherCatOMS(), + // Oral Health Services rate cannot be larger than the Dental or Oral Health Services rate + GV.validateOneCatRateHigherThanOtherCatOMS(0, 2), + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + GV.validateOMSTotalNDR(), + GV.validateEqualQualifierDenominatorsOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [TFLCHValidation]; diff --git a/services/ui-src/src/measures/2024/W30CH/data.ts b/services/ui-src/src/measures/2024/W30CH/data.ts new file mode 100644 index 0000000000..a8b64e118c --- /dev/null +++ b/services/ui-src/src/measures/2024/W30CH/data.ts @@ -0,0 +1,20 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("W30-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of children who had the following number of well-child visits with a primary care practitioner (PCP) during the last 15 months. The following rates are reported:", + ], + questionListItems: [ + " Children who turned age 15 months during the measurement year: Six or more well-child visits.", + " Children who turned age 30 months during the measurement year: Two or more well-child visits.", + ], + questionListTitles: [ + "Well-Child Visits in the First 15 Months.", + "Well-Child Visits for Age 15 Months-30 Months.", + ], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/W30CH/index.test.tsx b/services/ui-src/src/measures/2024/W30CH/index.test.tsx new file mode 100644 index 0000000000..513081ce87 --- /dev/null +++ b/services/ui-src/src/measures/2024/W30CH/index.test.tsx @@ -0,0 +1,262 @@ +import { fireEvent, screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "W30-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + it("should not allow non state users to edit forms by disabling buttons", async () => { + useApiMock(apiData); + renderWithHookForm(component); + + expect(screen.getByTestId("measure-wrapper-form")).toBeInTheDocument(); + const completeButton = screen.getByText("Complete Measure"); + fireEvent.click(completeButton); + expect(completeButton).toHaveAttribute("disabled"); + }); + + jest.setTimeout(15000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: + "Rate 1 - Six or more well-child visits in the first 15 months ", + rate: "100.0", + numerator: "50", + denominator: "50", + }, + { + label: + "Rate 2 - Two or more well-child visits for ages 15 months to 30 months", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/W30CH/index.tsx b/services/ui-src/src/measures/2024/W30CH/index.tsx new file mode 100644 index 0000000000..5783ceb8c7 --- /dev/null +++ b/services/ui-src/src/measures/2024/W30CH/index.tsx @@ -0,0 +1,67 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +import { FormData } from "./types"; + +export const W30CH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/W30CH/types.ts b/services/ui-src/src/measures/2024/W30CH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/W30CH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/W30CH/validation.ts b/services/ui-src/src/measures/2024/W30CH/validation.ts new file mode 100644 index 0000000000..f0e5b9c590 --- /dev/null +++ b/services/ui-src/src/measures/2024/W30CH/validation.ts @@ -0,0 +1,93 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const W30CHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + let errorArray: any[] = []; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + let unfilteredSameDenominatorErrors: any[] = []; + for (let i = 0; i < performanceMeasureArray.length; i += 2) { + unfilteredSameDenominatorErrors = [ + ...unfilteredSameDenominatorErrors, + ...GV.validateEqualQualifierDenominatorsPM( + [performanceMeasureArray[i], performanceMeasureArray[i + 1]], + ageGroups + ), + ]; + } + + let filteredSameDenominatorErrors: any = []; + let errorList: string[] = []; + unfilteredSameDenominatorErrors.forEach((error) => { + if (!(errorList.indexOf(error.errorMessage) > -1)) { + errorList.push(error.errorMessage); + filteredSameDenominatorErrors.push(error); + } + }); + + errorArray = [ + ...errorArray, + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...filteredSameDenominatorErrors, + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateZeroOMS(), + GV.validateRateNotZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [W30CHValidation]; diff --git a/services/ui-src/src/measures/2024/WCCCH/data.ts b/services/ui-src/src/measures/2024/WCCCH/data.ts new file mode 100644 index 0000000000..692b655418 --- /dev/null +++ b/services/ui-src/src/measures/2024/WCCCH/data.ts @@ -0,0 +1,74 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import * as DC from "dataConstants"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("WCC-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "The percentage of children ages 3 to 17 who had an outpatient visit with a primary care practitioner (PCP) or obstetrician/gynecologist (OB/GYN) and who had evidence of the following during the measurement year:", + ], + questionListItems: categories.map((item) => item.label), + categories, + qualifiers, +}; + +export const dataSourceData: DataDrivenTypes.DataSource = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.HYBRID_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: "Medicaid Management Information System (MMIS)", + }, + { + value: "Other", + description: true, + }, + ], + }, + { + label: "What is the Medical Records Data Source?", + options: [ + { + value: DC.EHR_DATA, + }, + { + value: DC.PAPER, + }, + ], + }, + ], + }, + { + value: DC.ELECTRONIC_HEALTH_RECORDS, + description: true, + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/WCCCH/index.test.tsx b/services/ui-src/src/measures/2024/WCCCH/index.test.tsx new file mode 100644 index 0000000000..a2575ea6f8 --- /dev/null +++ b/services/ui-src/src/measures/2024/WCCCH/index.test.tsx @@ -0,0 +1,295 @@ +import { fireEvent, screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "WCC-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsPM).toHaveBeenCalled(); + expect(V.validateEqualQualifierDenominatorsOMS).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + it("should not allow non state users to edit forms by disabling buttons", async () => { + useApiMock(apiData); + renderWithHookForm(component); + + expect(screen.getByTestId("measure-wrapper-form")).toBeInTheDocument(); + const completeButton = screen.getByText("Complete Measure"); + fireEvent.click(completeButton); + expect(completeButton).toHaveAttribute("disabled"); + }); + + jest.setTimeout(33000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + CounselingforPhysicalActivity: [ + { + label: "Ages 3 to 11", + }, + { + label: "Ages 12 to 17", + }, + { + isTotal: true, + label: "Total (Ages 3 to 17)", + }, + ], + BodymassindexBMIpercentiledocumentation: [ + { + label: "Ages 3 to 11", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 12 to 17", + }, + { + label: "Total (Ages 3 to 17)", + isTotal: true, + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + CounselingforNutrition: [ + { + label: "Ages 3 to 11", + }, + { + label: "Ages 12 to 17", + }, + { + isTotal: true, + label: "Total (Ages 3 to 17)", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/WCCCH/index.tsx b/services/ui-src/src/measures/2024/WCCCH/index.tsx new file mode 100644 index 0000000000..9f519bedfb --- /dev/null +++ b/services/ui-src/src/measures/2024/WCCCH/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import * as QMR from "components"; +import { FormData } from "./types"; + +export const WCCCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/WCCCH/types.ts b/services/ui-src/src/measures/2024/WCCCH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/WCCCH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/WCCCH/validation.ts b/services/ui-src/src/measures/2024/WCCCH/validation.ts new file mode 100644 index 0000000000..60fb61a756 --- /dev/null +++ b/services/ui-src/src/measures/2024/WCCCH/validation.ts @@ -0,0 +1,108 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const WCCHValidation = (data: FormData) => { + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const dateRange = data[DC.DATE_RANGE]; + const performanceMeasureArray = GV.getPerfMeasureRateArray(data, PMD.data); + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + const validateEqualQualifierDenominatorsErrorMessage = ( + qualifier: string + ) => { + const isTotal = qualifier.split(" ")[0] === "Total"; + return `${ + isTotal ? "" : "The " + }${qualifier} denominator must be the same for each indicator.`; + }; + + const validateTotalNDRErrorMessage = ( + qualifier: string, + fieldType: string + ) => { + return `${fieldType} for the ${qualifier} Total rate is not equal to the sum of the ${qualifier} age-specific ${fieldType.toLowerCase()}s.`; + }; + + errorArray = [ + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + PMD.qualifiers, + PMD.categories + ), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + PMD.qualifiers + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, PMD.qualifiers), + ...GV.validateRateZeroPM( + performanceMeasureArray, + OPM, + PMD.qualifiers, + data + ), + ...GV.validateEqualQualifierDenominatorsPM( + performanceMeasureArray, + PMD.qualifiers, + undefined, + validateEqualQualifierDenominatorsErrorMessage + ), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateTotalNDR( + performanceMeasureArray, + undefined, + PMD.categories, + validateTotalNDRErrorMessage + ), + + // OMS Validations + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + dataSource: data[DC.DATA_SOURCE], + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateEqualQualifierDenominatorsOMS(), + GV.validateRateNotZeroOMS(), + GV.validateOMSTotalNDR(), + GV.validateRateZeroOMS(), + ], + }), + ]; + + return errorArray; +}; + +export const validationFunctions = [WCCHValidation]; diff --git a/services/ui-src/src/measures/2024/WCVCH/data.ts b/services/ui-src/src/measures/2024/WCVCH/data.ts new file mode 100644 index 0000000000..6c4fb8e424 --- /dev/null +++ b/services/ui-src/src/measures/2024/WCVCH/data.ts @@ -0,0 +1,13 @@ +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { getCatQualLabels } from "../rateLabelText"; + +export const { categories, qualifiers } = getCatQualLabels("WCV-CH"); + +export const data: DataDrivenTypes.PerformanceMeasure = { + questionText: [ + "Percentage of children ages 3 to 21 who had at least one comprehensive well-care visit with a primary care practitioner (PCP) or an obstetrician/gynecologist (OB/GYN) during the measurement year.", + ], + questionListItems: [], + categories, + qualifiers, +}; diff --git a/services/ui-src/src/measures/2024/WCVCH/index.test.tsx b/services/ui-src/src/measures/2024/WCVCH/index.test.tsx new file mode 100644 index 0000000000..aced36f6c2 --- /dev/null +++ b/services/ui-src/src/measures/2024/WCVCH/index.test.tsx @@ -0,0 +1,270 @@ +import { fireEvent, screen, waitFor, act } from "@testing-library/react"; +import { createElement } from "react"; +import { RouterWrappedComp } from "utils/testing"; +import { MeasureWrapper } from "components/MeasureWrapper"; +import { useApiMock } from "utils/testUtils/useApiMock"; +import { useUser } from "hooks/authHooks"; +import Measures from "measures"; +import { Suspense } from "react"; +import { MeasuresLoading } from "views"; +import { measureDescriptions } from "measures/measureDescriptions"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { validationFunctions } from "./validation"; +import { + mockValidateAndSetErrors, + clearMocks, + validationsMockObj as V, +} from "measures/2024/shared/util/validationsMock"; +import { axe, toHaveNoViolations } from "jest-axe"; +expect.extend(toHaveNoViolations); + +// Test Setup +const measureAbbr = "WCV-CH"; +const coreSet = "CCSC"; +const state = "AL"; +const year = 2024; +const description = measureDescriptions[`${year}`][measureAbbr]; +const apiData: any = {}; + +jest.mock("hooks/authHooks"); +const mockUseUser = useUser as jest.Mock; + +describe(`Test FFY ${year} ${measureAbbr}`, () => { + let component: JSX.Element; + beforeEach(() => { + clearMocks(); + apiData.useGetMeasureValues = { + data: { + Item: { + compoundKey: `${state}${year}${coreSet}${measureAbbr}`, + coreSet, + createdAt: 1642517935305, + description, + lastAltered: 1642517935305, + lastAlteredBy: "undefined", + measure: measureAbbr, + state, + status: "incomplete", + year, + data: {}, + }, + }, + isLoading: false, + refetch: jest.fn(), + isError: false, + error: undefined, + }; + + mockUseUser.mockImplementation(() => { + return { isStateUser: false }; + }); + + const measure = createElement(Measures[year][measureAbbr]); + component = ( + + + + + + ); + }); + + it("measure should render", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("measure-wrapper-form")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText(measureAbbr + " - " + description)); + }); + }); + + /** + * Render the measure and confirm that all expected components exist. + * */ + it("Always shows Are you reporting question", async () => { + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("reporting")); + }); + + it("shows corresponding questions if yes to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).toBeInTheDocument(); + }); + + it("does not show corresponding questions if no to reporting then ", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("status-of-data")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("measurement-specification") + ).not.toBeInTheDocument(); + expect(screen.queryByTestId("data-source")).not.toBeInTheDocument(); + expect(screen.queryByTestId("date-range")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("definition-of-population") + ).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is selected", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("performance-measure")).toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).toBeInTheDocument(); + expect(screen.queryByTestId("OPM")).not.toBeInTheDocument(); + }); + + it("shows corresponding components and hides others when primary measure is NOT selected", async () => { + apiData.useGetMeasureValues.data.Item.data = OPMData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OPM")); + expect(screen.queryByTestId("performance-measure")).not.toBeInTheDocument(); + expect( + screen.queryByTestId("deviation-from-measure-specification") + ).not.toBeInTheDocument(); + }); + + it("shows OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = completedMeasureData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")); + }); + it("does not show OMS when performance measure data has been entered", async () => { + apiData.useGetMeasureValues.data.Item.data = notReportingData; + useApiMock(apiData); + renderWithHookForm(component); + expect(screen.queryByTestId("OMS")).not.toBeInTheDocument(); + }); + + /** Validations Test + * + * Confirm that correct functions are called. Comprehensive testing of the validations is done in specific test files + * for each validation function. See globalValidations directory. + */ + it("(Not Reporting) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, notReportingData); // trigger validations + expect(V.validateReasonForNotReporting).toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).not.toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).not.toHaveBeenCalled(); + expect(V.validateRateZeroPM).not.toHaveBeenCalled(); + expect( + V.validateRequiredRadioButtonForCombinedRates + ).not.toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).not.toHaveBeenCalled(); + expect(V.validateRateZeroOMS).not.toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).not.toHaveBeenCalled(); + expect(V.validateTotalNDR).not.toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).not.toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).not.toHaveBeenCalled(); + }); + + it("(Completed) validationFunctions should call all expected validation functions", async () => { + mockValidateAndSetErrors(validationFunctions, completedMeasureData); // trigger validations + expect(V.validateReasonForNotReporting).not.toHaveBeenCalled(); + expect(V.validateAtLeastOneRateComplete).toHaveBeenCalled(); + expect(V.validateNumeratorsLessThanDenominatorsPM).toHaveBeenCalled(); + expect(V.validateRateNotZeroPM).toHaveBeenCalled(); + expect(V.validateRateZeroPM).toHaveBeenCalled(); + expect(V.validateRequiredRadioButtonForCombinedRates).toHaveBeenCalled(); + expect(V.validateBothDatesCompleted).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSource).toHaveBeenCalled(); + expect(V.validateAtLeastOneDataSourceType).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeviationFieldFilled).toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatPM).not.toHaveBeenCalled(); + expect(V.validateOneCatRateHigherThanOtherCatOMS).not.toHaveBeenCalled(); + expect(V.validateNumeratorLessThanDenominatorOMS).toHaveBeenCalled(); + expect(V.validateRateZeroOMS).toHaveBeenCalled(); + expect(V.validateRateNotZeroOMS).toHaveBeenCalled(); + expect(V.validateTotalNDR).toHaveBeenCalled(); + expect(V.validateOMSTotalNDR).toHaveBeenCalled(); + expect(V.validateAtLeastOneDeliverySystem).toHaveBeenCalled(); + expect(V.validateFfsRadioButtonCompletion).toHaveBeenCalled(); + expect(V.validateAtLeastOneDefinitionOfPopulation).toHaveBeenCalled(); + }); + + it("should not allow non state users to edit forms by disabling buttons", async () => { + useApiMock(apiData); + renderWithHookForm(component); + + expect(screen.getByTestId("measure-wrapper-form")).toBeInTheDocument(); + const completeButton = screen.getByText("Complete Measure"); + fireEvent.click(completeButton); + expect(completeButton).toHaveAttribute("disabled"); + }); + + jest.setTimeout(33000); + it("should pass a11y tests", async () => { + useApiMock(apiData); + renderWithHookForm(component); + await act(async () => { + const results = await axe(screen.getByTestId("measure-wrapper-form")); + expect(results).toHaveNoViolations(); + }); + }); +}); + +const notReportingData = { + DidReport: "no", +}; + +const OPMData = { MeasurementSpecification: "Other", DidReport: "yes" }; + +const completedMeasureData = { + PerformanceMeasure: { + rates: { + singleCategory: [ + { + label: "Ages 3 to 11", + rate: "100.0", + numerator: "55", + denominator: "55", + }, + { + label: "Ages 12 to 17", + }, + { + label: "Ages 18 to 21", + }, + { + label: "Total", + isTotal: true, + rate: "100.0", + numerator: "55", + denominator: "55", + }, + ], + }, + }, + MeasurementSpecification: "NCQA/HEDIS", + DidReport: "yes", +}; diff --git a/services/ui-src/src/measures/2024/WCVCH/index.tsx b/services/ui-src/src/measures/2024/WCVCH/index.tsx new file mode 100644 index 0000000000..196a70cad2 --- /dev/null +++ b/services/ui-src/src/measures/2024/WCVCH/index.tsx @@ -0,0 +1,68 @@ +import { useEffect } from "react"; +import { useFormContext } from "react-hook-form"; +import * as CMQ from "measures/2024/shared/CommonQuestions"; +import * as PMD from "./data"; +import * as QMR from "components"; +import { validationFunctions } from "./validation"; +import { getPerfMeasureRateArray } from "measures/2024/shared/globalValidations"; +import { FormData } from "./types"; + +export const WCVCH = ({ + name, + year, + measureId, + setValidationFunctions, + isNotReportingData, + isPrimaryMeasureSpecSelected, + showOptionalMeasureStrat, + isOtherMeasureSpecSelected, +}: QMR.MeasureWrapperProps) => { + const { watch } = useFormContext(); + const data = watch(); + + useEffect(() => { + if (setValidationFunctions) { + setValidationFunctions(validationFunctions); + } + }, [setValidationFunctions]); + + const performanceMeasureArray = getPerfMeasureRateArray(data, PMD.data); + + return ( + <> + + + {!isNotReportingData && ( + <> + + + + + + {isPrimaryMeasureSpecSelected && ( + <> + + + + )} + {isOtherMeasureSpecSelected && } + + {showOptionalMeasureStrat && ( + + )} + + )} + + + ); +}; diff --git a/services/ui-src/src/measures/2024/WCVCH/types.ts b/services/ui-src/src/measures/2024/WCVCH/types.ts new file mode 100644 index 0000000000..0179dced3f --- /dev/null +++ b/services/ui-src/src/measures/2024/WCVCH/types.ts @@ -0,0 +1,3 @@ +import * as Types from "measures/2024/shared/CommonQuestions/types"; + +export type FormData = Types.DefaultFormData; diff --git a/services/ui-src/src/measures/2024/WCVCH/validation.ts b/services/ui-src/src/measures/2024/WCVCH/validation.ts new file mode 100644 index 0000000000..ab5e46e633 --- /dev/null +++ b/services/ui-src/src/measures/2024/WCVCH/validation.ts @@ -0,0 +1,75 @@ +import * as DC from "dataConstants"; +import * as GV from "measures/2024/shared/globalValidations"; +import * as PMD from "./data"; +import { FormData } from "./types"; +import { OMSData } from "measures/2024/shared/CommonQuestions/OptionalMeasureStrat/data"; + +const WCVCHValidation = (data: FormData) => { + const ageGroups = PMD.qualifiers; + const whyNotReporting = data[DC.WHY_ARE_YOU_NOT_REPORTING]; + const OPM = data[DC.OPM_RATES]; + const performanceMeasureArray = + GV.getPerfMeasureRateArray(data, PMD.data) ?? []; + const dateRange = data[DC.DATE_RANGE]; + const didCalculationsDeviate = data[DC.DID_CALCS_DEVIATE] === DC.YES; + const deviationReason = data[DC.DEVIATION_REASON]; + + let errorArray: any[] = []; + if (data[DC.DID_REPORT] === DC.NO) { + errorArray = [...GV.validateReasonForNotReporting(whyNotReporting)]; + return errorArray; + } + + errorArray = [ + ...errorArray, + ...GV.omsValidations({ + data, + qualifiers: PMD.qualifiers, + categories: PMD.categories, + locationDictionary: GV.omsLocationDictionary( + OMSData(), + PMD.qualifiers, + PMD.categories + ), + validationCallbacks: [ + GV.validateNumeratorLessThanDenominatorOMS(), + GV.validateRateNotZeroOMS(), + GV.validateOMSTotalNDR(), + GV.validateRateZeroOMS(), + ], + }), + ...GV.validateAtLeastOneDeviationFieldFilled( + didCalculationsDeviate, + deviationReason + ), + ...GV.validateAtLeastOneRateComplete( + performanceMeasureArray, + OPM, + ageGroups, + PMD.categories + ), + ...GV.validateAtLeastOneDataSource(data), + ...GV.validateAtLeastOneDefinitionOfPopulation(data), + ...GV.validateAtLeastOneDataSourceType(data), + ...GV.validateAtLeastOneDeliverySystem(data), + ...GV.validateFfsRadioButtonCompletion(data), + ...GV.validateNumeratorsLessThanDenominatorsPM( + performanceMeasureArray, + OPM, + ageGroups + ), + ...GV.validateRateNotZeroPM(performanceMeasureArray, OPM, ageGroups), + ...GV.validateRateZeroPM(performanceMeasureArray, OPM, ageGroups, data), + ...GV.validateRequiredRadioButtonForCombinedRates(data), + ...GV.validateDateRangeRadioButtonCompletion(data), + ...GV.validateBothDatesCompleted(dateRange), + ...GV.validateYearFormat(dateRange), + ...GV.validateHedisYear(data), + ...GV.validateOPMRates(OPM), + ...GV.validateTotalNDR(performanceMeasureArray), + ]; + + return errorArray; +}; + +export const validationFunctions = [WCVCHValidation]; diff --git a/services/ui-src/src/measures/2024/index.tsx b/services/ui-src/src/measures/2024/index.tsx new file mode 100644 index 0000000000..c8df210964 --- /dev/null +++ b/services/ui-src/src/measures/2024/index.tsx @@ -0,0 +1,318 @@ +import { lazy } from "react"; +import { Data, Qualifier } from "./shared/Qualifiers"; + +/* +When importing a measure it should be a named import and added to the measures object below so that it routes correctly +the key should be the measure id as a string (with '-XX' included) +*/ +const AABAD = lazy(() => + import("./AABAD").then((module) => ({ default: module.AABAD })) +); +const AABCH = lazy(() => + import("./AABCH").then((module) => ({ default: module.AABCH })) +); +const ADDCH = lazy(() => + import("./ADDCH").then((module) => ({ default: module.ADDCH })) +); +const AIFHH = lazy(() => + import("./AIFHH").then((module) => ({ default: module.AIFHH })) +); +const AMBCH = lazy(() => + import("./AMBCH").then((module) => ({ default: module.AMBCH })) +); +const AMBHH = lazy(() => + import("./AMBHH").then((module) => ({ default: module.AMBHH })) +); +const AMMAD = lazy(() => + import("./AMMAD").then((module) => ({ default: module.AMMAD })) +); +const AMRAD = lazy(() => + import("./AMRAD").then((module) => ({ default: module.AMRAD })) +); +const AMRCH = lazy(() => + import("./AMRCH").then((module) => ({ default: module.AMRCH })) +); +const APMCH = lazy(() => + import("./APMCH").then((module) => ({ default: module.APMCH })) +); +const APPCH = lazy(() => + import("./APPCH").then((module) => ({ default: module.APPCH })) +); +const BCSAD = lazy(() => + import("./BCSAD").then((module) => ({ default: module.BCSAD })) +); +const CBPAD = lazy(() => + import("./CBPAD").then((module) => ({ default: module.CBPAD })) +); +const CBPHH = lazy(() => + import("./CBPHH").then((module) => ({ default: module.CBPHH })) +); +const CCPAD = lazy(() => + import("./CCPAD").then((module) => ({ default: module.CCPAD })) +); +const CCPCH = lazy(() => + import("./CCPCH").then((module) => ({ default: module.CCPCH })) +); +const CCSAD = lazy(() => + import("./CCSAD").then((module) => ({ default: module.CCSAD })) +); +const CCWAD = lazy(() => + import("./CCWAD").then((module) => ({ default: module.CCWAD })) +); +const CCWCH = lazy(() => + import("./CCWCH").then((module) => ({ default: module.CCWCH })) +); +const CDFAD = lazy(() => + import("./CDFAD").then((module) => ({ default: module.CDFAD })) +); +const CDFCH = lazy(() => + import("./CDFCH").then((module) => ({ default: module.CDFCH })) +); +const CDFHH = lazy(() => + import("./CDFHH").then((module) => ({ default: module.CDFHH })) +); +const CHLAD = lazy(() => + import("./CHLAD").then((module) => ({ default: module.CHLAD })) +); +const CHLCH = lazy(() => + import("./CHLCH").then((module) => ({ default: module.CHLCH })) +); +const CISCH = lazy(() => + import("./CISCH").then((module) => ({ default: module.CISCH })) +); +const COBAD = lazy(() => + import("./COBAD").then((module) => ({ default: module.COBAD })) +); +const COLAD = lazy(() => + import("./COLAD").then((module) => ({ default: module.COLAD })) +); +const COLHH = lazy(() => + import("./COLHH").then((module) => ({ default: module.COLHH })) +); +const CPAAD = lazy(() => + import("./CPAAD").then((module) => ({ default: module.CPAAD })) +); +const CPCCH = lazy(() => + import("./CPCCH").then((module) => ({ default: module.CPCCH })) +); +const CPUAD = lazy(() => + import("./CPUAD").then((module) => ({ default: module.CPUAD })) +); +const DEVCH = lazy(() => + import("./DEVCH").then((module) => ({ default: module.DEVCH })) +); +const FUAAD = lazy(() => + import("./FUAAD").then((module) => ({ default: module.FUAAD })) +); +const FUACH = lazy(() => + import("./FUACH").then((module) => ({ default: module.FUACH })) +); +const FUAHH = lazy(() => + import("./FUAHH").then((module) => ({ default: module.FUAHH })) +); +const FUHAD = lazy(() => + import("./FUHAD").then((module) => ({ default: module.FUHAD })) +); +const FUHCH = lazy(() => + import("./FUHCH").then((module) => ({ default: module.FUHCH })) +); +const FUHHH = lazy(() => + import("./FUHHH").then((module) => ({ default: module.FUHHH })) +); +const FUMAD = lazy(() => + import("./FUMAD").then((module) => ({ default: module.FUMAD })) +); +const FUMCH = lazy(() => + import("./FUMCH").then((module) => ({ default: module.FUMCH })) +); +const FUMHH = lazy(() => + import("./FUMHH").then((module) => ({ default: module.FUMHH })) +); +const FVAAD = lazy(() => + import("./FVAAD").then((module) => ({ default: module.FVAAD })) +); +const HVLAD = lazy(() => + import("./HVLAD").then((module) => ({ default: module.HVLAD })) +); +const HBDAD = lazy(() => + import("./HBDAD").then((module) => ({ default: module.HBDAD })) +); +const HPCMIAD = lazy(() => + import("./HPCMIAD").then((module) => ({ default: module.HPCMIAD })) +); +const IETAD = lazy(() => + import("./IETAD").then((module) => ({ default: module.IETAD })) +); +const IETHH = lazy(() => + import("./IETHH").then((module) => ({ default: module.IETHH })) +); +const IMACH = lazy(() => + import("./IMACH").then((module) => ({ default: module.IMACH })) +); +const IUHH = lazy(() => + import("./IUHH").then((module) => ({ default: module.IUHH })) +); +const LBWCH = lazy(() => + import("./LBWCH").then((module) => ({ default: module.LBWCH })) +); +const LRCDCH = lazy(() => + import("./LRCDCH").then((module) => ({ default: module.LRCDCH })) +); +const MSCAD = lazy(() => + import("./MSCAD").then((module) => ({ default: module.MSCAD })) +); +const LSCCH = lazy(() => + import("./LSCCH").then((module) => ({ default: module.LSCCH })) +); +const NCIDDSAD = lazy(() => + import("./NCIDDSAD").then((module) => ({ default: module.NCIDDSAD })) +); +const OEVCH = lazy(() => + import("./OEVCH").then((module) => ({ default: module.OEVCH })) +); +const OHDAD = lazy(() => + import("./OHDAD").then((module) => ({ default: module.OHDAD })) +); +const OUDAD = lazy(() => + import("./OUDAD").then((module) => ({ default: module.OUDAD })) +); +const OUDHH = lazy(() => + import("./OUDHH").then((module) => ({ default: module.OUDHH })) +); +const PCRAD = lazy(() => + import("./PCRAD").then((module) => ({ default: module.PCRAD })) +); +const PCRHH = lazy(() => + import("./PCRHH").then((module) => ({ default: module.PCRHH })) +); +const PPCAD = lazy(() => + import("./PPCAD").then((module) => ({ default: module.PPCAD })) +); +const PPCCH = lazy(() => + import("./PPCCH").then((module) => ({ default: module.PPCCH })) +); +const PQI01AD = lazy(() => + import("./PQI01AD").then((module) => ({ default: module.PQI01AD })) +); +const PQI05AD = lazy(() => + import("./PQI05AD").then((module) => ({ default: module.PQI05AD })) +); +const PQI92HH = lazy(() => + import("./PQI92HH").then((module) => ({ default: module.PQI92HH })) +); +const PQI08AD = lazy(() => + import("./PQI08AD").then((module) => ({ default: module.PQI08AD })) +); +const PQI15AD = lazy(() => + import("./PQI15AD").then((module) => ({ default: module.PQI15AD })) +); +const SAAAD = lazy(() => + import("./SAAAD").then((module) => ({ default: module.SAAAD })) +); +const SFMCH = lazy(() => + import("./SFMCH").then((module) => ({ default: module.SFMCH })) +); +const SSDAD = lazy(() => + import("./SSDAD").then((module) => ({ default: module.SSDAD })) +); +const SSHH = lazy(() => + import("./SSHH").then((module) => ({ default: module.SSHH })) +); +const TFLCH = lazy(() => + import("./TFLCH").then((module) => ({ default: module.TFLCH })) +); +const W30CH = lazy(() => + import("./W30CH").then((module) => ({ default: module.W30CH })) +); +const WCCCH = lazy(() => + import("./WCCCH").then((module) => ({ default: module.WCCCH })) +); +const WCVCH = lazy(() => + import("./WCVCH").then((module) => ({ default: module.WCVCH })) +); + +const twentyTwentyFourMeasures = { + "AAB-AD": AABAD, + "AAB-CH": AABCH, + "ADD-CH": ADDCH, + "AIF-HH": AIFHH, + "AMB-CH": AMBCH, + "AMB-HH": AMBHH, + "AMM-AD": AMMAD, + "AMR-AD": AMRAD, + "AMR-CH": AMRCH, + "APM-CH": APMCH, + "APP-CH": APPCH, + "BCS-AD": BCSAD, + "CBP-AD": CBPAD, + "CBP-HH": CBPHH, + "CCP-AD": CCPAD, + "CCP-CH": CCPCH, + "CCS-AD": CCSAD, + "CCW-AD": CCWAD, + "CCW-CH": CCWCH, + "CDF-AD": CDFAD, + "CDF-CH": CDFCH, + "CDF-HH": CDFHH, + "CHL-AD": CHLAD, + "CHL-CH": CHLCH, + "CIS-CH": CISCH, + "COB-AD": COBAD, + "COL-AD": COLAD, + "COL-HH": COLHH, + "CPA-AD": CPAAD, + "CPC-CH": CPCCH, + "CPU-AD": CPUAD, + "DEV-CH": DEVCH, + "FUA-AD": FUAAD, + "FUA-CH": FUACH, + "FUA-HH": FUAHH, + "FUH-AD": FUHAD, + "FUH-CH": FUHCH, + "FUH-HH": FUHHH, + "FUM-AD": FUMAD, + "FUM-CH": FUMCH, + "FUM-HH": FUMHH, + "FVA-AD": FVAAD, + "HBD-AD": HBDAD, + "HPCMI-AD": HPCMIAD, + "HVL-AD": HVLAD, + "IET-AD": IETAD, + "IET-HH": IETHH, + "IMA-CH": IMACH, + "IU-HH": IUHH, + "LBW-CH": LBWCH, + "LRCD-CH": LRCDCH, + "MSC-AD": MSCAD, + "LSC-CH": LSCCH, + "NCIDDS-AD": NCIDDSAD, + "OEV-CH": OEVCH, + "OHD-AD": OHDAD, + "OUD-AD": OUDAD, + "OUD-HH": OUDHH, + "PCR-AD": PCRAD, + "PCR-HH": PCRHH, + "PPC-AD": PPCAD, + "PPC-CH": PPCCH, + "PQI01-AD": PQI01AD, + "PQI05-AD": PQI05AD, + "PQI08-AD": PQI08AD, + "PQI15-AD": PQI15AD, + "PQI92-HH": PQI92HH, + "SAA-AD": SAAAD, + "SFM-CH": SFMCH, + "SSD-AD": SSDAD, + "SS-1-HH": SSHH, + "SS-2-HH": SSHH, + "SS-3-HH": SSHH, + "SS-4-HH": SSHH, + "SS-5-HH": SSHH, + "TFL-CH": TFLCH, + "W30-CH": W30CH, + "WCC-CH": WCCCH, + "WCV-CH": WCVCH, + Qualifier, +}; + +export const QualifierData = Data; +export default twentyTwentyFourMeasures; diff --git a/services/ui-src/src/measures/2024/rateLabelText.test.ts b/services/ui-src/src/measures/2024/rateLabelText.test.ts new file mode 100644 index 0000000000..06ddf95985 --- /dev/null +++ b/services/ui-src/src/measures/2024/rateLabelText.test.ts @@ -0,0 +1,40 @@ +import { data } from "./rateLabelText"; + +describe("Rate Label Data", () => { + test("Categories should have globally-unique IDs", () => { + const categoryIdUsage = new Map< + string, + { measureName: string; categoryIndex: number } + >(); + for (const [measureName, measure] of Object.entries(data)) { + for (let i = 0; i < measure.categories.length; i += 1) { + const category = measure.categories[i]; + if (categoryIdUsage.has(category.id)) { + const existingIdUse = categoryIdUsage.get(category.id); + throw new Error( + `Measure ${existingIdUse?.measureName}, category #${existingIdUse?.categoryIndex} has the same ID as ${measureName}, category #${i}.` + ); + } else { + categoryIdUsage.set(category.id, { + measureName, + categoryIndex: i, + }); + } + } + } + }); + + test("Qualifiers should have unique IDs within each measure", () => { + for (const [measureName, measure] of Object.entries(data)) { + if ( + measure.qualifiers.some( + (qual, i, arr) => i !== arr.findIndex((q) => q.id === qual.id) + ) + ) { + throw new Error( + `Measure ${measureName} has multiple qualifiers with the same ID.` + ); + } + } + }); +}); diff --git a/services/ui-src/src/measures/2024/rateLabelText.ts b/services/ui-src/src/measures/2024/rateLabelText.ts new file mode 100644 index 0000000000..8536abc279 --- /dev/null +++ b/services/ui-src/src/measures/2024/rateLabelText.ts @@ -0,0 +1,1820 @@ +/** + * Attention + * Changing the labels will change how the measure data is shaped and should not be done unless that is the desired result. + * Changing the text property of these objects will change the text that is displayed to the user. + * Changing the id's will affect the save data in the database and also how the client pulls the data. NEVER CHANGE THEM. + * id's are randomly generated from https://shortunique.id/. If there's a qualifiers that is called Total, use the word "Total" as the id + */ + +export const data = { + "AAB-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "kINeRZ" + }, + { + "label": "Age 65 and Older", + "text": "Age 65 and Older", + "id": "EreZBY" + } + ], + "categories": [{"id":"SU6HXz", "label": "", "text":""}] + }, + "AAB-CH": { + "qualifiers": [ + { + "label": "Ages 3 months to 17 years", + "text": "Ages 3 months to 17 years", + "id": "xS5HMm" + }, + ], + "categories": [{"id":"ZCy3XP", "label": "", "text":""}] + }, + "ADD-CH": { + "qualifiers": [ + { + "label": "Initiation Phase", + "text": "Initiation Phase", + "id": "UYSXR5" + }, + { + "label": "Continuation and Maintenance (C&M) Phase", + "text": "Continuation and Maintenance (C&M) Phase", + "id": "jfj0f8" + } + ], + "categories": [{"id":"ugoYfe", "label": "", "text":""}] + }, + "AIF-HH": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "Quotyg", + "excludeFromOMS": true + }, + { + "label": "Ages 65 to 74", + "text": "Ages 65 to 74", + "id": "R5nGsO", + "excludeFromOMS": true + }, + { + "label": "Ages 75 to 84", + "text": "Ages 75 to 84", + "id": "hrrJSA", + "excludeFromOMS": true + }, + { + "label": "Age 85 and older", + "text": "Age 85 and older", + "id": "QqtlDW", + "excludeFromOMS": true + }, + { + "label": "Total (Age 18 and older)", + "text": "Total (Age 18 and older)", + "id": "Total" + } + ], + "categories": [{"id":"ZyxRR5", "label": "", "text":""}] + }, + "AMB-CH": { + "qualifiers": [ + { + "label": "< Age 1", + "text": "< Age 1", + "id": "AElhke", + "excludeFromOMS": true + }, + { + "label": "Ages 1 to 9", + "text": "Ages 1 to 9", + "id": "lvn4pe", + "excludeFromOMS": true + }, + { + "label": "Ages 10 to 19", + "text": "Ages 10 to 19", + "id": "9IODyt", + "excludeFromOMS": true + }, + { + "label": "Ages unknown", + "text": "Ages unknown", + "id": "mIA2wL", + "excludeFromOMS": true + }, + { + "label": "Total (Ages <1 to 19)", + "text": "Total (Ages <1 to 19)", + "id": "Total" + } + ], + "categories": [{"id":"8KgPDM", "label": "", "text":""}] + }, + "AMB-HH": { + "qualifiers": [ + { + "label": "Ages 0 to 17", + "text": "Ages 0 to 17", + "id": "dmwUgG", + "excludeFromOMS": true + }, + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "JmMRlc", + "excludeFromOMS": true + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "ISVyAj", + "excludeFromOMS": true + }, + { + "label": "Ages unknown", + "text": "Ages unknown", + "id": "f2dfi8", + "excludeFromOMS": true + }, + { + "label": "Total (All Ages)", + "text": "Total (All Ages)", + "id": "Total" + } + ], + "categories": [{"id":"9pQZSL", "label": "", "text":""}] + }, + "AMM-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "g91VU9" + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "Gjknmo" + } + ], + "categories": [ + { + "label": "Effective Acute Phase Treatment", + "text": "Effective Acute Phase Treatment", + "id": "DFukSh" + }, + { + "label": "Effective Continuation Phase Treatment", + "text": "Effective Continuation Phase Treatment", + "id": "JjQeAC" + } + ] + }, + "AMR-AD": { + "qualifiers": [ + { + "label": "Ages 19 to 50", + "text": "Ages 19 to 50", + "id": "uT3Ybg", + "excludeFromOMS": true + }, + { + "label": "Ages 51 to 64", + "text": "Ages 51 to 64", + "id": "pvkFnD", + "excludeFromOMS": true + }, + { + "label": "Total (Ages 19 to 64)", + "text": "Total (Ages 19 to 64)", + "id": "Total" + } + ], + "categories": [{"id":"HRsQ7F", "label": "", "text":""}] + }, + "AMR-CH": { + "qualifiers": [ + { + "label": "Ages 5 to 11", + "text": "Ages 5 to 11", + "id": "kijL2T", + "excludeFromOMS": true + }, + { + "label": "Ages 12 to 18", + "text": "Ages 12 to 18", + "id": "cteSKS", + "excludeFromOMS": true + }, + { + "label": "Total (Ages 5 to 18)", + "text": "Total (Ages 5 to 18)", + "id": "Total" + } + ], + "categories": [{"id":"tMt8gW", "label": "", "text":""}] + }, + "APM-CH": { + "qualifiers": [ + { + "label": "Ages 1 to 11", + "text": "Ages 1 to 11", + "id": "rJQSKZ", + "excludeFromOMS": true + }, + { + "label": "Ages 12 to 17", + "text": "Ages 12 to 17", + "id": "FS1yOb", + "excludeFromOMS": true + }, + { + "label": "Total (Ages 1 to 17)", + "text": "Total (Ages 1 to 17)", + "id": "Total" + } + ], + "categories": [ + { + "label": "Blood Glucose", + "text": "Blood Glucose", + "id": "rcmfbq" + }, + { + "label": "Cholesterol", + "text": "Cholesterol", + "id": "0oa3fh" + }, + { + "label": "Blood Glucose and Cholesterol", + "text": "Blood Glucose and Cholesterol", + "id": "s60uoF" + } + ] + }, + "APP-CH": { + "qualifiers": [ + { + "label": "Ages 1 to 11", + "text": "Ages 1 to 11", + "id": "8KS1is", + "excludeFromOMS": true + }, + { + "label": "Ages 12 to 17", + "text": "Ages 12 to 17", + "id": "fNNDrp", + "excludeFromOMS": true + }, + { + "label": "Total (Ages 1 to 17)", + "text": "Total (Ages 1 to 17)", + "id": "Total" + } + ], + "categories": [{"id":"JTlCwr", "label": "", "text":""}] + }, + "BCS-AD": { + "qualifiers": [ + { + "label": "Ages 50 to 64", + "text": "Ages 50 to 64", + "id": "bfvp78" + }, + { + "label": "Ages 65 to 74", + "text": "Ages 65 to 74", + "id": "vRqegW" + } + ], + "categories": [{"id":"xacc8a", "label": "", "text":""}] + }, + "CBP-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "lL2f0N" + }, + { + "label": "Ages 65 to 85", + "text": "Ages 65 to 85", + "id": "SynTm5" + } + ], + "categories": [{"id":"qyic1D", "label": "", "text":""}] + }, + "CBP-HH": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "KvPMQt", + "excludeFromOMS": true + }, + { + "label": "Ages 65 to 85", + "text": "Ages 65 to 85", + "id": "BgLk3U", + "excludeFromOMS": true + }, + { + "label": "Total (Ages 18 to 85)", + "text": "Total (Ages 18 to 85)", + "id": "Total" + } + ], + "categories": [{"id":"f2iZFk", "label": "", "text":""}] + }, + "CCP-AD": { + "qualifiers": [ + { + "label": "Three Days Postpartum Rate", + "text": "Three Days Postpartum Rate", + "id": "gDSWIR" + }, + { + "label": "Ninety Days Postpartum Rate", + "text": "Ninety Days Postpartum Rate", + "id": "iHteIp" + } + ], + "categories": [ + { + "label": "Most effective or moderately effective method of contraception", + "text": "Most effective or moderately effective method of contraception", + "id": "JmD71i" + }, + { + "label": "Long-acting reversible method of contraception (LARC)", + "text": "Long-acting reversible method of contraception (LARC)", + "id": "xS3fQk" + } + ] + }, + "CCP-CH": { + "qualifiers": [ + { + "label": "Three Days Postpartum Rate", + "text": "Three Days Postpartum Rate", + "id": "CeTZzq" + }, + { + "label": "Ninety Days Postpartum Rate", + "text": "Ninety Days Postpartum Rate", + "id": "XiBACI" + } + ], + "categories": [ + { + "label": "Most effective or moderately effective method of contraception", + "text": "Most effective or moderately effective method of contraception", + "id": "041gNX" + }, + { + "label": "Long-acting reversible method of contraception (LARC)", + "text": "Long-acting reversible method of contraception (LARC)", + "id": "r7b2vH" + } + ] + }, + "CCS-AD": { + "qualifiers": [ + { + "label": "Percentage of women ages 21 to 64 screened", + "text": "Percentage of women ages 21 to 64 screened", + "id": "VZUrlc" + } + ], + "categories": [{"id":"Ma0WNl", "label": "", "text":""}] + }, + "CCW-AD": { + "qualifiers": [ + { + "label": "Most effective or moderately effective method of contraception", + "text": "Most effective or moderately effective method of contraception", + "id": "FLFmHi" + }, + { + "label": "Long-acting reversible method of contraception (LARC)", + "text": "Long-acting reversible method of contraception (LARC)", + "id": "qken13" + } + ], + "categories": [{"id":"ntJIVl", "label": "", "text":""}] + }, + "CCW-CH": { + "qualifiers": [ + { + "label": "Most effective or moderately effective method of contraception", + "text": "Most effective or moderately effective method of contraception", + "id": "XmMjXU" + }, + { + "label": "Long-acting reversible method of contraception (LARC)", + "text": "Long-acting reversible method of contraception (LARC)", + "id": "tnsawX" + } + ], + "categories": [{"id":"p2e6nu", "label": "", "text":""}] + }, + "CDF-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "wjMBXk" + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "tJ0j8E" + } + ], + "categories": [{"id":"fUqwlh", "label": "", "text":""}] + }, + "CDF-CH": { + "qualifiers": [ + { + "label": "Ages 12 to 17", + "text": "Ages 12 to 17", + "id": "HgCHDt" + } + ], + "categories": [{"id":"hsjQhs", "label": "", "text":""}] + }, + "CDF-HH": { + "qualifiers": [ + { + "label": "Ages 12 to 17", + "text": "Ages 12 to 17", + "id": "UXIY6B", + "excludeFromOMS": true + }, + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "z3Sc1P", + "excludeFromOMS": true + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "AcmtW5", + "excludeFromOMS": true + }, + { + "label": "Total (Age 12 and older)", + "text": "Total (Age 12 and older)", + "id": "Total" + } + ], + "categories": [{"id":"yhQjRm", "label": "", "text":""}] + }, + "CHL-AD": { + "qualifiers": [ + { + "label": "Ages 21 to 24", + "text": "Ages 21 to 24", + "id": "eV26mN" + } + ], + "categories": [{"id":"cvc5jQ", "label": "", "text":""}] + }, + "CHL-CH": { + "qualifiers": [ + { + "label": "Ages 16 to 20", + "text": "Ages 16 to 20", + "id": "PbJazd" + } + ], + "categories": [{"id":"xMHGQi", "label": "", "text":""}] + }, + "CIS-CH": { + "qualifiers": [ + { + "label": "DTaP", + "text": "DTaP", + "id": "315LFo", + "excludeFromOMS": true + }, + { + "label": "IPV", + "text": "IPV", + "id": "g11sKr", + "excludeFromOMS": true + }, + { + "label": "MMR", + "text": "MMR", + "id": "RRDiUD" + }, + { + "label": "HiB", + "text": "HiB", + "id": "MEtwzK", + "excludeFromOMS": true + }, + { + "label": "Hep B", + "text": "Hep B", + "id": "legTtc", + "excludeFromOMS": true + }, + { + "label": "VZV", + "text": "VZV", + "id": "vo0QQI", + "excludeFromOMS": true + }, + { + "label": "PCV", + "text": "PCV", + "id": "KH78dm", + "excludeFromOMS": true + }, + { + "label": "Hep A", + "text": "Hep A", + "id": "QQdRrJ", + "excludeFromOMS": true + }, + { + "label": "RV", + "text": "RV", + "id": "UACFyG", + "excludeFromOMS": true + }, + { + "label": "Flu", + "text": "Flu", + "id": "VxUjMm", + "excludeFromOMS": true + }, + { + "label": "Combo 3", + "text": "Combo 3", + "id": "aI8KQ7" + }, + { + "label": "Combo 7", + "text": "Combo 7", + "id": "xOvucQ", + "excludeFromOMS": true + }, + { + "label": "Combo 10", + "text": "Combo 10", + "id": "UwzvFc" + } + ], + "categories": [{"id":"u7wDB2", "label": "", "text":""}] + }, + "COB-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "PuPnUR" + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "e37PqQ" + } + ], + "categories": [{"id":"3jFQwQ", "label": "", "text":""}] + }, + "COL-AD": { + "qualifiers": [ + { + "label": "Ages 46 to 49", + "text": "Ages 46 to 49", + "id": "jaap4W" + }, + { + "label": "Ages 50 to 64", + "text": "Ages 50 to 64", + "id": "cvwOYG" + }, + { + "label": "Ages 65 to 75", + "text": "Ages 65 to 75", + "id": "YHLMTu" + } + ], + "categories": [{"id":"jVTfQB", "label": "", "text":""}] + }, + "COL-HH": { + "qualifiers": [ + { + "label": "Ages 46 to 49", + "text": "Ages 46 to 49", + "id": "RHlNkr" + }, + { + "label": "Ages 50 to 64", + "text": "Ages 50 to 64", + "id": "u1n5QA" + }, + { + "label": "Ages 65 to 75", + "text": "Ages 65 to 75", + "id": "2ATODe" + } + ], + "categories": [{"id":"sYNAsL", "label": "", "text":""}] + }, + "CPU-AD": { + "qualifiers": [ + { + "label": "Care Plan with Core Elements Documented", + "text": "Care Plan with Core Elements Documented", + "id": "T6dADz", + }, + { + "label": "Care Plan with Supplemental Elements Documented", + "text": "Care Plan with Supplemental Elements Documented", + "id": "NBz553", + }, + ], + "categories": [{"id":"HLXNLW", "label": "", "text":""}] + }, + "DEV-CH": { + "qualifiers": [ + { + "label": "Children screened by 12 months of age", + "text": "Children screened by 12 months of age", + "id": "V9moUD", + "excludeFromOMS": true + }, + { + "label": "Children screened by 24 months of age", + "text": "Children screened by 24 months of age", + "id": "8syeJa", + "excludeFromOMS": true + }, + { + "label": "Children screened by 36 months of age", + "text": "Children screened by 36 months of age", + "id": "UjlL0h", + "excludeFromOMS": true + }, + { + "label": "Children Total", + "text": "Children Total", + "id": "Total" + } + ], + "categories": [{"id":"rnFOY6", "label": "", "text":""}] + }, + "FUA-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "YXDxKr" + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "DV7M5h" + } + ], + "categories": [ + { + "label": "Follow-up within 30 days of ED visit", + "text": "Follow-up within 30 days of ED visit", + "id": "LfkdK3" + }, + { + "label": "Follow-up within 7 days of ED visit", + "text": "Follow-up within 7 days of ED visit", + "id": "Ipp7sc" + } + ] + }, + "FUA-CH": { + "qualifiers": [ + { + "label": "Ages 13 to 17", + "text": "Ages 13 to 17", + "id": "ACAVuF" + } + ], + "categories": [ + { + "label": "Follow-up within 30 days of ED visit", + "text": "Follow-up within 30 days of ED visit", + "id": "EMKZhW" + }, + { + "label": "Follow-up within 7 days of ED visit", + "text": "Follow-up within 7 days of ED visit", + "id": "Nz1u3I" + } + ] + }, + "FUA-HH": { + "qualifiers": [ + { + "label": "Ages 13 to 17", + "text": "Ages 13 to 17", + "id": "V4pnKr", + "excludeFromOMS": true + }, + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "Njn9ob", + "excludeFromOMS": true + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "GM7MbC", + "excludeFromOMS": true + }, + { + "label": "Total (Age 13 and older)", + "text": "Total (Age 13 and older)", + "id": "Total" + } + ], + "categories": [ + { + "label": "Follow-up within 30 days of ED visit", + "text": "Follow-up within 30 days of ED visit", + "id": "eHvs7S" + }, + { + "label": "Follow-up within 7 days of ED visit", + "text": "Follow-up within 7 days of ED visit", + "id": "PyKyIU" + } + ] + }, + "FUH-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "FbBLHo" + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "MqFK9L" + } + ], + "categories": [ + { + "label": "Follow-Up within 30 days after discharge", + "text": "Follow-Up within 30 days after discharge", + "id": "8w4t99" + }, + { + "label": "Follow-Up within 7 days after discharge", + "text": "Follow-Up within 7 days after discharge", + "id": "c2CNBL" + } + ] + }, + "FUH-CH": { + "qualifiers": [ + { + "label": "Ages 6 to 17", + "text": "Ages 6 to 17", + "id": "x1i20X" + } + ], + "categories": [ + { + "label": "Follow-Up within 30 days after discharge", + "text": "Follow-Up within 30 days after discharge", + "id": "YI7qPh" + }, + { + "label": "Follow-Up within 7 days after discharge", + "text": "Follow-Up within 7 days after discharge", + "id": "bCOfkY" + } + ] + }, + "FUH-HH": { + "qualifiers": [ + { + "label": "Ages 6 to 17", + "text": "Ages 6 to 17", + "id": "SSE84V", + "excludeFromOMS": true + }, + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "dqF5XO", + "excludeFromOMS": true + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "OLGEc6", + "excludeFromOMS": true + }, + { + "label": "Total (Age 6 and older)", + "text": "Total (Age 6 and older)", + "id": "Total" + } + ], + "categories": [ + { + "label": "Follow-up within 30 days after discharge", + "text": "Follow-up within 30 days after discharge", + "id": "BDDUqy" + }, + { + "label": "Follow-up within 7 days after discharge", + "text": "Follow-up within 7 days after discharge", + "id": "5qOI4g" + } + ] + }, + "FUM-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "xO0lQK" + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "6p8dL9" + } + ], + "categories": [ + { + "label": "30-day follow-up after ED visit for mental illness", + "text": "30-day follow-up after ED visit for mental illness", + "id": "biXkZF" + }, + { + "label": "7-day follow-up after ED visit for mental illness", + "text": "7-day follow-up after ED visit for mental illness", + "id": "jQ2y5i" + } + ] + }, + "FUM-CH": { + "qualifiers": [ + { + "label": "Ages 6 to 17", + "text": "Ages 6 to 17", + "id": "v1LHRu" + } + ], + "categories": [ + { + "label": "30-day follow-up after ED visit for mental illness", + "text": "30-day follow-up after ED visit for mental illness", + "id": "mpaDiA" + }, + { + "label": "7-day follow-up after ED visit for mental illness", + "text": "7-day follow-up after ED visit for mental illness", + "id": "MFDMmO" + } + ] + }, + "FUM-HH": { + "qualifiers": [ + { + "label": "Ages 6 to 17", + "text": "Ages 6 to 17", + "id": "wZkCIH", + "excludeFromOMS": true + }, + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "HOpeUo", + "excludeFromOMS": true + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "CBkwgBC", + "excludeFromOMS": true + }, + { + "label": "Total (Age 6 and older)", + "text": "Total (Age 6 and older)", + "id": "Total" + } + ], + "categories": [ + { + "label": "30-day follow-up after ED visit for mental illness", + "text": "30-day follow-up after ED visit for mental illness", + "id": "bLYJVL" + }, + { + "label": "7-day follow-up after ED visit for mental illness", + "text": "7-day follow-up after ED visit for mental illness", + "id": "0PNnN6" + } + ] + }, + "FVA-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "BKbFhQ" + } + ], + "categories": [{"id":"5bDNK0", "label": "", "text":""}] + }, + "HBD-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "clYQr0" + }, + { + "label": "Ages 65 to 75", + "text": "Ages 65 to 75", + "id": "chkl7n" + } + ], + "categories": [ + { + "label": "HbA1c control (<8.0%)", + "text": "HbA1c control (<8.0%)", + "id":"F9V8xD", + "excludeFromOMS": true + }, + { + "label": "HbA1c poor control (>9.0%)", + "text": "HbA1c poor control (>9.0%)", + "id":"MELFVb" + } + ] + }, + "HPCMI-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "T93ccP" + }, + { + "label": "Ages 65 to 75", + "text": "Ages 65 to 75", + "id": "LmBzTX" + } + ], + "categories": [{"id":"rCN1NM", "label": "", "text":""}] + }, + "HVL-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "NipqyO" + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "0SLTQO" + } + ], + "categories": [{"id":"iAT7Xc", "label": "", "text":""}] + }, + "IET-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "Mj6NGv" + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "IrauwZ" + } + ], + "categories": [ + { + "label": "Initiation of SUD Treatment: Alcohol Use Disorder", + "text": "Initiation of SUD Treatment: Alcohol Use Disorder", + "id": "pgGQFr", + "excludeFromOMS": true + }, + { + "label": "Engagement of SUD Treatment: Alcohol Use Disorder", + "text": "Engagement of SUD Treatment: Alcohol Use Disorder", + "id": "eWGRSg", + "excludeFromOMS": true + }, + { + "label": "Initiation of SUD Treatment: Opioid Use Disorder", + "text": "Initiation of SUD Treatment: Opioid Use Disorder", + "id": "yrQOQf", + "excludeFromOMS": true + }, + { + "label": "Engagement of SUD Treatment: Opioid Use Disorder", + "text": "Engagement of SUD Treatment: Opioid Use Disorder", + "id": "zkA4Vm", + "excludeFromOMS": true + }, + { + "label": "Initiation of SUD Treatment: Other Substance Use Disorder", + "text": "Initiation of SUD Treatment: Other Substance Use Disorder", + "id": "ptnmQC", + "excludeFromOMS": true + }, + { + "label": "Engagement of SUD Treatment: Other Substance Use Disorder", + "text": "Engagement of SUD Treatment: Other Substance Use Disorder", + "id": "leK847", + "excludeFromOMS": true + }, + { + "label": "Initiation of SUD Treatment: Total", + "text": "Initiation of SUD Treatment: Total", + "id": "NoGIsL" + }, + { + "label": "Engagement of SUD Treatment: Total", + "text": "Engagement of SUD Treatment: Total", + "id": "nHTL1Y" + } + ] + }, + "IET-HH": { + "qualifiers": [ + { + "label": "Ages 13 to 17", + "text": "Ages 13 to 17", + "id": "pO7AO4", + "excludeFromOMS": true + }, + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "VnqwIP", + "excludeFromOMS": true + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "HJa7Gq", + "excludeFromOMS": true + }, + { + "label": "Total (age 13 and older)", + "text": "Total (age 13 and older)", + "id": "Total" + } + ], + "categories": [ + { + "label": "Initiation of SUD Treatment: Alcohol Use Disorder", + "text": "Initiation of SUD Treatment: Alcohol Use Disorder", + "id": "SzkEgT", + "excludeFromOMS": true + }, + { + "label": "Engagement of SUD Treatment: Alcohol Use Disorder", + "text": "Engagement of SUD Treatment: Alcohol Use Disorder", + "id": "fBjzVK", + "excludeFromOMS": true + }, + { + "label": "Initiation of SUD Treatment: Opioid Use Disorder", + "text": "Initiation of SUD Treatment: Opioid Use Disorder", + "id": "XcV5kx", + "excludeFromOMS": true + }, + { + "label": "Engagement of SUD Treatment: Opioid Use Disorder", + "text": "Engagement of SUD Treatment: Opioid Use Disorder", + "id": "TbEkhJ", + "excludeFromOMS": true + }, + { + "label": "Initiation of SUD Treatment: Other Substance Use Disorder", + "text": "Initiation of SUD Treatment: Other Substance Use Disorder", + "id": "DNIllr", + "excludeFromOMS": true + }, + { + "label": "Engagement of SUD Treatment: Other Substance Use Disorder", + "text": "Engagement of SUD Treatment: Other Substance Use Disorder", + "id": "J2Kfn5", + "excludeFromOMS": true + }, + { + "label": "Initiation of SUD Treatment: Total", + "text": "Initiation of SUD Treatment: Total", + "id": "QpzHSf" + }, + { + "label": "Engagement of SUD Treatment: Total", + "text": "Engagement of SUD Treatment: Total", + "id": "AVY2yg" + } + ] + }, + "IMA-CH": { + "qualifiers": [ + { + "label": "Meningococcal", + "text": "Meningococcal", + "id": "25diKI" + }, + { + "label": "Tdap", + "text": "Tdap", + "id": "gTSKzD" + }, + { + "label": "Human Papillomavirus (HPV)", + "text": "Human Papillomavirus (HPV)", + "id": "3r9tKr" + }, + { + "label": "Combination 1 (Meningococcal, Tdap)", + "text": "Combination 1 (Meningococcal, Tdap)", + "id": "61I5BC" + }, + { + "label": "Combination 2 (Meningococcal, Tdap, HPV)", + "text": "Combination 2 (Meningococcal, Tdap, HPV)", + "id": "agvITR" + } + ], + "categories": [{"id":"6Wts84", "label": "", "text":""}] + }, + "IU-HH": { + "qualifiers": [ + { + "label": "Ages 0 to 17", + "text": "Ages 0 to 17", + "id": "RTKRUB", + "excludeFromOMS": true + }, + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "HN9CBC", + "excludeFromOMS": true + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "OdxHfY", + "excludeFromOMS": true + }, + { + "label": "Ages unknown", + "text": "Ages unknown", + "id": "At7duO", + "excludeFromOMS": true + }, + { + "label": "Total", + "text": "Total", + "id": "Total" + } + ], + "categories": [ + { + "label": "Inpatient", + "text": "Inpatient", + "id": "895Rzk" + }, + { + "label": "Mental and Behavioral Disorders", + "text": "Mental and Behavioral Disorders", + "id": "VX9uaf", + "excludeFromOMS": true + }, + { + "label": "Surgery", + "text": "Surgery", + "id": "QzuJDR", + "excludeFromOMS": true + }, + { + "label": "Medicine", + "text": "Medicine", + "id": "ioz7ed", + "excludeFromOMS": true + } + ] + }, + + "LSC-CH": { + "qualifiers": [ + { + "label": "At least one lead capillary or venous blood test on or before the child's second birthday", + "text": "At least one lead capillary or venous blood test on or before the child's second birthday", + "id": "AtXKXx" + } + ], + "categories": [{"id":"HOiTVZ", "label": "", "text":""}] + }, + "MSC-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "wNVojQ" + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "SYlSVC" + } + ], + "categories": [ + { + "label": "Percentage of Current Smokers and Tobacco Users", + "text": "Percentage of Current Smokers and Tobacco Users", + "id": "fF0COe" + }, + { + "label": "Advising Smokers and Tobacco Users to Quit", + "text": "Advising Smokers and Tobacco Users to Quit", + "id": "S2ngC7" + }, + { + "label": "Discussing Cessation Medications", + "text": "Discussing Cessation Medications", + "id": "Hk5dWm" + }, + { + "label": "Discussing Cessation Strategies", + "text": "Discussing Cessation Strategies", + "id": "xiXN7R" + } + ] + }, + "OEV-CH": { + "qualifiers": [ + { + "label": "Age <1", + "text": "Age <1", + "id": "cJpLzk", + "excludeFromOMS": true + }, + { + "label": "Ages 1 to 2", + "text": "Ages 1 to 2", + "id": "lUpXnj", + "excludeFromOMS": true + }, + { + "label": "Ages 3 to 5", + "text": "Ages 3 to 5", + "id": "tfIdZe", + "excludeFromOMS": true + }, + { + "label": "Ages 6 to 7", + "text": "Ages 6 to 7", + "id": "HQtz8Q", + "excludeFromOMS": true + }, + { + "label": "Ages 8 to 9", + "text": "Ages 8 to 9", + "id": "lw6tF8", + "excludeFromOMS": true + }, + { + "label": "Ages 10 to 11", + "text": "Ages 10 to 11", + "id": "iS0hVY", + "excludeFromOMS": true + }, + { + "label": "Ages 12 to 14", + "text": "Ages 12 to 14", + "id": "OAxju0", + "excludeFromOMS": true + }, + { + "label": "Ages 15 to 18", + "text": "Ages 15 to 18", + "id": "0B13E5", + "excludeFromOMS": true + }, + { + "label": "Ages 19 to 20", + "text": "Ages 19 to 20", + "id": "2v1LvP", + "excludeFromOMS": true + }, + { + "label": "Total ages <1 to 20", + "text": "Total ages <1 to 20", + "id": "Total" + } + ], + "categories": [{"id":"oe5lKf", "label": "", "text":""}] + }, + "OHD-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "NyGIus" + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "23IWY1" + } + ], + "categories": [{"id":"aeOiMF", "label": "", "text":""}] + }, + "OUD-AD": { + "qualifiers": [ + { + "label": "Total Rate", + "text": "Total Rate", + "id": "R74AGC" + }, + { + "label": "Buprenorphine", + "text": "Buprenorphine", + "id": "ayxxN6", + "excludeFromOMS": true + }, + { + "label": "Oral naltrexone", + "text": "Oral naltrexone", + "id": "xaFLEL", + "excludeFromOMS": true + }, + { + "label": "Long-acting, injectable naltrexone", + "text": "Long-acting, injectable naltrexone", + "id": "Pumtn4", + "excludeFromOMS": true + }, + { + "label": "Methadone", + "text": "Methadone", + "id": "CgQFr3", + "excludeFromOMS": true + } + ], + "categories": [{"id":"t75kZB", "label": "", "text":""}] + }, + "OUD-HH": { + "qualifiers": [ + { + "label": "Total Rate", + "text": "Total Rate", + "id": "yiqyWE" + }, + { + "label": "Buprenorphine", + "text": "Buprenorphine", + "id": "QczL9f", + "excludeFromOMS": true + }, + { + "label": "Oral naltrexone", + "text": "Oral naltrexone", + "id": "bNSOWQ", + "excludeFromOMS": true + }, + { + "label": "Long-acting, injectable naltrexone", + "text": "Long-acting, injectable naltrexone", + "id": "LNXU8H", + "excludeFromOMS": true + }, + { + "label": "Methadone", + "text": "Methadone", + "id": "vsv8L7", + "excludeFromOMS": true + } + ], + "categories": [{"id":"lS0z8M", "label": "", "text":""}] + }, + "PCR-AD": { + "qualifiers": [ + { + "label": "Count of Index Hospital Stays", + "text": "Count of Index Hospital Stays", + "id": "Z31BMw" + }, + { + "label": "Count of Observed 30-Day Readmissions", + "text": "Count of Observed 30-Day Readmissions", + "id": "KdVD0I" + }, + { + "label": "Observed Readmission Rate", + "text": "Observed Readmission Rate", + "id": "GWePur" + }, + { + "label": "Count of Expected 30-Day Readmissions", + "text": "Count of Expected 30-Day Readmissions", + "id": "ciVWdY" + }, + { + "label": "Expected Readmission Rate", + "text": "Expected Readmission Rate", + "id": "qi3Vd7" + }, + { + "label": "O/E Ratio (Count of Observed 30-Day Readmissions/Count of Expected 30-Day Readmissions)", + "text": "O/E Ratio (Count of Observed 30-Day Readmissions/Count of Expected 30-Day Readmissions)", + "id": "SczxqV" + }, + { + "label": "Count of Beneficiaries in Medicaid Population", + "text": "Count of Beneficiaries in Medicaid Population", + "id": "Ei65yg" + }, + { + "label": "Number of Outliers", + "text": "Number of Outliers", + "id": "pBILL1" + }, + { + "label": "Outlier Rate (Number of Outliers/Count of Beneficiaries in Medicaid Population) x 1,000", + "text": "Outlier Rate (Number of Outliers/Count of Beneficiaries in Medicaid Population) x 1,000", + "id": "Nfe4Cn" + } + ], + "categories": [{"id":"zcwVcA", "label": "", "text":""}] + }, + "PCR-HH": { + "qualifiers": [ + { + "label": "Count of Index Hospital Stays", + "text": "Count of Index Hospital Stays", + "id": "QfSJYl" + }, + { + "label": "Count of Observed 30-Day Readmissions", + "text": "Count of Observed 30-Day Readmissions", + "id": "ObB95y" + }, + { + "label": "Observed Readmission Rate", + "text": "Observed Readmission Rate", + "id": "6GwY7k" + }, + { + "label": "Count of Expected 30-Day Readmissions", + "text": "Count of Expected 30-Day Readmissions", + "id": "xBK85r" + }, + { + "label": "Expected Readmission Rate", + "text": "Expected Readmission Rate", + "id": "f1XeFZ" + }, + { + "label": "O/E Ratio (Count of Observed 30-Day Readmissions/Count of Expected 30-Day Readmissions)", + "text": "O/E Ratio (Count of Observed 30-Day Readmissions/Count of Expected 30-Day Readmissions)", + "id": "PVXXII" + }, + { + "label": "Count of Enrollees in Health Home Population", + "text": "Count of Enrollees in Health Home Population", + "id": "BkJVO0" + }, + { + "label": "Number of Outliers", + "text": "Number of Outliers", + "id": "vf2fO0" + }, + { + "label": "Outlier Rate (Number of Outliers/Count of Enrollees in Health Home Population) x 1,000", + "text": "Outlier Rate (Number of Outliers/Count of Enrollees in Health Home Population) x 1,000", + "id": "08LeiP" + } + ], + "categories": [{"id":"YGJwmu", "label": "", "text":""}] + }, + "PPC-AD": { + "qualifiers": [ + { + "label": "Postpartum visit between 7 and 84 days", + "text": "Postpartum visit between 7 and 84 days", + "id": "LrWMAT" + } + ], + "categories": [{"id":"SyrrI1", "label": "", "text":""}] + }, + "PPC-CH": { + "qualifiers": [ + { + "label": "Prenatal care visit in the first trimester, on or before the enrollment start date or within 42 days of enrollment in Medicaid/CHIP.", + "text": "Prenatal care visit in the first trimester, on or before the enrollment start date or within 42 days of enrollment in Medicaid/CHIP.", + "id": "kCBB0a" + } + ], + "categories": [{"id":"fcjCsg", "label": "", "text":""}] + }, + "PQI01-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "8N7tKQ" + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "g01NRO" + } + ], + "categories": [{"id":"Gp9GRU", "label": "", "text":""}] + }, + "PQI05-AD": { + "qualifiers": [ + { + "label": "Ages 40 to 64", + "text": "Ages 40 to 64", + "id": "ocpMf5" + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "usIZvV" + } + ], + "categories": [{"id":"CzBbvv", "label": "", "text":""}] + }, + "PQI08-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "bi8vv6" + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "rRyKdf" + } + ], + "categories": [{"id":"eYsLWE", "label": "", "text":""}] + }, + "PQI15-AD": { + "qualifiers": [ + { + "label": "Ages 18 to 39", + "text": "Ages 18 to 39", + "id": "i57RxI" + } + ], + "categories": [{"id":"Z4aIZZ", "label": "", "text":""}] + }, + "PQI92-HH": { + "qualifiers": [ + { + "label": "Ages 18 to 64", + "text": "Ages 18 to 64", + "id": "cxWfJ4", + "excludeFromOMS": true + }, + { + "label": "Age 65 and older", + "text": "Age 65 and older", + "id": "ttjYve", + "excludeFromOMS": true + }, + { + "label": "Total (Age 18 and older)", + "text": "Total (Age 18 and older)", + "id": "Total" + } + ], + "categories": [{"id":"mIBZOk", "label": "", "text":""}] + }, + "SAA-AD": { + "qualifiers": [ + { + "label": "Beneficiaries Age 18 and Older", + "text": "Beneficiaries Age 18 and Older", + "id": "5Zy1WJ" + } + ], + "categories": [{"id":"bdI1f1", "label": "", "text":""}] + }, + "SFM-CH": { + "qualifiers": [ + { + "label": "Rate 1 - At Least One Sealant", + "text": "Rate 1 - At Least One Sealant", + "id": "5Jb7fw" + }, + { + "label": "Rate 2 - All Four Molars Sealed", + "text": "Rate 2 - All Four Molars Sealed", + "id": "VC1O0n" + } + ], + "categories": [{"id":"zooXXr", "label": "", "text":""}] + }, + "SSD-AD": { + "qualifiers": [ + { + "label": "Percentage of Beneficiaries Ages 18 to 64", + "text": "Percentage of Beneficiaries Ages 18 to 64", + "id": "QAiTJA" + } + ], + "categories": [{"id":"IJmKJZ", "label": "", "text":""}] + }, + "TFL-CH": { + "qualifiers": [ + { + "label": "Ages 1 to 2", + "text": "Ages 1 to 2", + "id": "TGl5f1", + "excludeFromOMS": true + }, + { + "label": "Ages 3 to 5", + "text": "Ages 3 to 5", + "id": "AccBCl", + "excludeFromOMS": true + }, + { + "label": "Ages 6 to 7", + "text": "Ages 6 to 7", + "id": "zcbPe0", + "excludeFromOMS": true + }, + { + "label": "Ages 8 to 9", + "text": "Ages 8 to 9", + "id": "OdDTxr", + "excludeFromOMS": true + }, + { + "label": "Ages 10 to 11", + "text": "Ages 10 to 11", + "id": "Cjw7GS", + "excludeFromOMS": true + }, + { + "label": "Ages 12 to 14", + "text": "Ages 12 to 14", + "id": "muOeEP", + "excludeFromOMS": true + }, + { + "label": "Ages 15 to 18", + "text": "Ages 15 to 18", + "id": "fbpAPY", + "excludeFromOMS": true + }, + { + "label": "Ages 19 to 20", + "text": "Ages 19 to 20", + "id": "a6okZM", + "excludeFromOMS": true + }, + { + "label": "Total Ages 1 through 20", + "text": "Total Ages 1 through 20", + "id": "Total" + } + ], + "categories": [ + { + "label": "Dental or oral health services", + "text": "Dental or oral health services", + "id": "1LW8Lr" + }, + { + "label": "Dental services", + "text": "Dental services", + "id": "1gVqJh", + "excludeFromOMS": true + }, + { + "label": "Oral health services", + "text": "Oral health services", + "id": "0NPmzO", + "excludeFromOMS": true + } + ] + }, + "W30-CH": { + "qualifiers": [ + { + "label": "Rate 1 - Six or more well-child visits in the first 15 months ", + "text": "Rate 1 - Six or more well-child visits in the first 15 months ", + "id": "VCTS6B" + }, + { + "label": "Rate 2 - Two or more well-child visits for ages 15 months to 30 months", + "text": "Rate 2 - Two or more well-child visits for ages 15 months to 30 months", + "id": "pNdn6w" + } + ], + "categories": [{"id":"HOiTVR", "label": "", "text":""}] + }, + "WCC-CH": { + "qualifiers": [ + { + "label": "Ages 3 to 11", + "text": "Ages 3 to 11", + "id": "iWwR8Z", + "excludeFromOMS": true + }, + { + "label": "Ages 12 to 17", + "text": "Ages 12 to 17", + "id": "BFwD7g", + "excludeFromOMS": true + }, + { + "label": "Total (Ages 3 to 17)", + "text": "Total (Ages 3 to 17)", + "id": "Total" + } + ], + "categories": [ + { + "label": "Body mass index (BMI) percentile documentation", + "text": "Body mass index (BMI) percentile documentation", + "id": "4TXd3h" + }, + { + "label": "Counseling for Nutrition", + "text": "Counseling for Nutrition", + "id": "cKH5gj" + }, + { + "label": "Counseling for Physical Activity", + "text": "Counseling for Physical Activity", + "id": "1POxYx" + } + ] + }, + "WCV-CH": { + "qualifiers": [ + { + "label": "Ages 3 to 11", + "text": "Ages 3 to 11", + "id": "AtXKXf", + "excludeFromOMS": true + }, + { + "label": "Ages 12 to 17", + "text": "Ages 12 to 17", + "id": "iqbQvH", + "excludeFromOMS": true + }, + { + "label": "Ages 18 to 21", + "text": "Ages 18 to 21", + "id": "pJmWDB", + "excludeFromOMS": true + }, + { + "label": "Total (Ages 3 to 21)", + "text": "Total (Ages 3 to 21)", + "id": "Total" + } + ], + "categories": [{"id":"YOFx9h", "label": "", "text":""}] + } +} + +export const getCatQualLabels = (measure: keyof typeof data) => { + const qualifiers = data[measure].qualifiers; + const categories = data[measure].categories; + + return { + qualifiers, + categories, + }; + }; \ No newline at end of file diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/AdditionalNotes/index.test.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/AdditionalNotes/index.test.tsx new file mode 100644 index 0000000000..c70a2b4bc1 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/AdditionalNotes/index.test.tsx @@ -0,0 +1,59 @@ +import fireEvent from "@testing-library/user-event"; +import { AdditionalNotes } from "."; +import { Reporting } from "../Reporting"; +import { screen } from "@testing-library/react"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; + +describe("Test AdditionalNotes component", () => { + beforeEach(() => { + renderWithHookForm([ + , + , + ]); + }); + + it("component renders", () => { + expect( + screen.getByText("Additional Notes/Comments on the measure (optional)") + ).toBeInTheDocument(); + expect( + screen.getByText( + "If you need additional space to include comments or supplemental information, please attach further documentation below." + ) + ).toBeInTheDocument(); + }); + + it("accepts input", async () => { + const textArea = await screen.findByLabelText( + "Please add any additional notes or comments on the measure not otherwise captured above (text in this field is included in publicly-reported state-specific comments):" + ); + fireEvent.type(textArea, "This is the test text"); + expect(textArea).toHaveDisplayValue("This is the test text"); + }); + + it("input is deleted when measure reporting radio option is changed", async () => { + const reportingNo = await screen.findByLabelText( + "No, I am not reporting My Test Measure (MTM) for FFY 2024 quality measure reporting." + ); + + const textArea = await screen.findByLabelText( + "Please add any additional notes or comments on the measure not otherwise captured above (text in this field is included in publicly-reported state-specific comments):" + ); + + fireEvent.click(reportingNo); + fireEvent.type(textArea, "This is the test text"); + expect(textArea).toHaveDisplayValue("This is the test text"); + + // change reporting radio button option from no to yes + const reportingYes = await screen.findByLabelText( + "Yes, I am reporting My Test Measure (MTM) for FFY 2024 quality measure reporting." + ); + + fireEvent.click(reportingYes); + expect(textArea).toHaveDisplayValue(""); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/AdditionalNotes/index.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/AdditionalNotes/index.tsx new file mode 100644 index 0000000000..2ff4bc2554 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/AdditionalNotes/index.tsx @@ -0,0 +1,46 @@ +import * as QMR from "components"; +import * as CUI from "@chakra-ui/react"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import { Upload } from "components/Upload"; +import * as Types from "../types"; +import * as DC from "dataConstants"; +import { useFormContext } from "react-hook-form"; +import { useEffect } from "react"; + +export const AdditionalNotes = () => { + const register = useCustomRegister(); + const { getValues, resetField } = useFormContext(); + const didReport = getValues()["DidReport"]; + + useEffect(() => { + resetField("AdditionalNotes-AdditionalNotes"); + }, [didReport, resetField]); + + return ( + + + Please add any additional notes or comments on the measure not + otherwise captured above ( + + text in this field is included in publicly-reported state-specific + comments + + ): + + } + {...register(DC.ADDITIONAL_NOTES)} + /> + + + + + ); +}; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/CombinedRates/index.test.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/CombinedRates/index.test.tsx new file mode 100644 index 0000000000..e3de3c1bcb --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/CombinedRates/index.test.tsx @@ -0,0 +1,178 @@ +import fireEvent from "@testing-library/user-event"; +import { CombinedRates } from "."; +import { screen } from "@testing-library/react"; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; + +describe("Test CombinedRates component", () => { + beforeEach(() => { + renderWithHookForm(); + }); + + it("component renders", () => { + expect( + screen.getByText( + "Did you combine rates from multiple reporting units (e.g. health plans, delivery systems, programs) to create a State-Level rate?" + ) + ).toBeInTheDocument(); + expect( + screen.getByText( + "Did you combine rates from multiple reporting units (e.g. health plans, delivery systems, programs) to create a State-Level rate?" + ) + ).toBeInTheDocument(); + }); + + it("renders suboptions when Yes is clicked", async () => { + const textArea = await screen.findByLabelText( + "Yes, we combined rates from multiple reporting units to create a State-Level rate." + ); + fireEvent.click(textArea); + expect( + screen.getByText( + "The rates are not weighted based on the size of the measure-eligible population. All reporting units are given equal weights when calculating a State-Level rate." + ) + ).toBeInTheDocument(); + expect( + screen.getByText( + "The rates are weighted based on the size of the measure-eligible population for each reporting unit." + ) + ).toBeInTheDocument(); + expect( + screen.getByText( + "The rates are weighted based on another weighting factor." + ) + ).toBeInTheDocument(); + }); + + it("renders a text area when Yes is clicked and the last option is clicked", async () => { + fireEvent.click( + await screen.findByLabelText( + "Yes, we combined rates from multiple reporting units to create a State-Level rate." + ) + ); + fireEvent.click( + await screen.getByText( + "The rates are weighted based on another weighting factor." + ) + ); + + expect( + screen.getByText("Describe the other weighting factor:") + ).toBeInTheDocument(); + + const textArea = await screen.findByLabelText( + "Describe the other weighting factor:" + ); + + fireEvent.type(textArea, "This is the test text"); + expect(textArea).toHaveDisplayValue("This is the test text"); + }); + + it("does not render suboptions when No is clicked", async () => { + const textArea = await screen.findByLabelText( + "No, we did not combine rates from multiple reporting units to create a State-Level rate." + ); + fireEvent.click(textArea); + expect( + screen.queryByText( + "The rates are not weighted based on the size of the measure-eligible population. All reporting units are given equal weights when calculating a State-Level rate." + ) + ).toBeNull(); + expect( + screen.queryByText( + "The rates are weighted based on the size of the measure-eligible population for each reporting unit." + ) + ).toBeNull(); + expect( + screen.queryByText( + "The rates are weighted based on another weighting factor." + ) + ).toBeNull(); + }); +}); + +describe("Test CombinedRates component for Health Homes", () => { + beforeEach(() => { + renderWithHookForm(); + }); + + it("component renders with different text", () => { + expect( + screen.getByText( + "Did you combine rates from multiple reporting units (e.g. Health Home Providers) to create a Health Home Program-Level rate?" + ) + ).toBeInTheDocument(); + expect( + screen.getByText( + "Did you combine rates from multiple reporting units (e.g. Health Home Providers) to create a Health Home Program-Level rate?" + ) + ).toBeInTheDocument(); + }); + + it("renders suboptions when Yes is clicked", async () => { + const textArea = await screen.findByLabelText( + "Yes, we combined rates from multiple reporting units to create a Health Home Program-Level rate." + ); + fireEvent.click(textArea); + expect( + screen.getByText( + "The rates are not weighted based on the size of the measure-eligible population. All reporting units are given equal weights when calculating a Program-Level rate." + ) + ).toBeInTheDocument(); + expect( + screen.getByText( + "The rates are weighted based on the size of the measure-eligible population for each reporting unit." + ) + ).toBeInTheDocument(); + expect( + screen.getByText( + "The rates are weighted based on another weighting factor." + ) + ).toBeInTheDocument(); + }); + + it("renders a text area when Yes is clicked and the last option is clicked for Health Homes", async () => { + fireEvent.click( + await screen.findByLabelText( + "Yes, we combined rates from multiple reporting units to create a Health Home Program-Level rate." + ) + ); + fireEvent.click( + await screen.getByText( + "The rates are weighted based on another weighting factor." + ) + ); + + expect( + screen.getByText("Describe the other weighting factor:") + ).toBeInTheDocument(); + + const textArea = await screen.findByLabelText( + "Describe the other weighting factor:" + ); + + fireEvent.type(textArea, "This is the test text"); + expect(textArea).toHaveDisplayValue("This is the test text"); + }); + + it("does not render suboptions when No is clicked", async () => { + const textArea = await screen.findByLabelText( + "No, we did not combine rates from multiple reporting units to create a Program-Level rate for Health Home measures." + ); + fireEvent.click(textArea); + expect( + screen.queryByText( + "The rates are not weighted based on the size of the measure-eligible population. All reporting units are given equal weights when calculating a Program-Level rate." + ) + ).toBeNull(); + expect( + screen.queryByText( + "The rates are weighted based on the size of the measure-eligible population for each reporting unit." + ) + ).toBeNull(); + expect( + screen.queryByText( + "The rates are weighted based on another weighting factor." + ) + ).toBeNull(); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/CombinedRates/index.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/CombinedRates/index.tsx new file mode 100644 index 0000000000..c99c4e6663 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/CombinedRates/index.tsx @@ -0,0 +1,96 @@ +import * as CUI from "@chakra-ui/react"; +import * as QMR from "components"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import * as Types from "../types"; +import * as DC from "dataConstants"; + +interface Props { + healthHomeMeasure?: boolean; +} + +export const CombinedRates = ({ healthHomeMeasure }: Props) => { + const register = useCustomRegister(); + + return ( + + + {!healthHomeMeasure ? ( + <> + Did you combine rates from multiple reporting units (e.g. health + plans, delivery systems, programs) to create a State-Level rate? + + ) : ( + <> + Did you combine rates from multiple reporting units (e.g. Health + Home Providers) to create a Health Home Program-Level rate? + + )} + + + For additional information refer to the{" "} + + State-Level Rate Brief + + . + + , + ], + }, + ]} + />, + ], + }, + { + displayValue: !healthHomeMeasure + ? "No, we did not combine rates from multiple reporting units to create a State-Level rate." + : "No, we did not combine rates from multiple reporting units to create a Program-Level rate for Health Home measures.", + value: DC.NO, + }, + ]} + renderHelperTextAbove + {...register(DC.COMBINED_RATES)} + /> + + ); +}; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSource/__snapshots__/index.test.tsx.snap b/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSource/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000..800e2d0150 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSource/__snapshots__/index.test.tsx.snap @@ -0,0 +1,258 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test the global DataSource component (Custom Structure) Component renders with correct content 1`] = ` +
+
+

+ Data Source +

+
+
+
+ +
+
+ + +
+ + +
+ + +
+
+
+
+
+
+
+
+
+`; + +exports[`Test the global DataSource component (Default) Component renders with correct content 1`] = ` +
+
+

+ Data Source +

+
+
+
+ +
+
+ + +
+ + +
+
+
+
+
+
+
+
+
+`; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSource/data.ts b/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSource/data.ts new file mode 100644 index 0000000000..a4eefef9b4 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSource/data.ts @@ -0,0 +1,43 @@ +import * as DC from "dataConstants"; + +export interface OptionNode { + value: string; + subOptions?: { + label?: string; + options: OptionNode[]; + }[]; + description?: boolean; +} + +export interface DataSourceData { + options: OptionNode[]; + optionsLabel: string; +} + +export const defaultData: DataSourceData = { + optionsLabel: + "If reporting entities (e.g., health plans) used different data sources, please select all applicable data sources used below.", + options: [ + { + value: DC.ADMINISTRATIVE_DATA, + subOptions: [ + { + label: "What is the Administrative Data Source?", + options: [ + { + value: DC.MEDICAID_MANAGEMENT_INFO_SYSTEM, + }, + { + value: DC.ADMINISTRATIVE_DATA_OTHER, + description: true, + }, + ], + }, + ], + }, + { + value: DC.OTHER_DATA_SOURCE, + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSource/index.test.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSource/index.test.tsx new file mode 100644 index 0000000000..25fbd64d41 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSource/index.test.tsx @@ -0,0 +1,47 @@ +import { DataSource } from "./index"; +import { DataSourceData as DS } from "utils/testUtils/testFormData"; +import { DataDrivenTypes } from "measures/2024/shared/CommonQuestions/types"; +import { testSnapshot } from "utils/testUtils/testSnapshot"; + +describe("Test the global DataSource component", () => { + it("(Default) Component renders with correct content", () => { + const component = ; + testSnapshot({ component, defaultValues: DS.default }); + }); + + it("(Custom Structure) Component renders with correct content", () => { + const component = ; + testSnapshot({ component, defaultValues: DS.custom }); + }); +}); + +const data: DataDrivenTypes.DataSource = { + optionsLabel: "How do we feel about a label with this text? Does it work?", + options: [ + { + value: "Option 1", + subOptions: [ + { + label: "What is the Option 1 Data Source?", + options: [ + { + value: "I'm not telling", + }, + { + value: "You'll have to kill me first", + description: true, + }, + ], + }, + ], + }, + { + value: "A Better Option", + description: true, + }, + { + value: "Something Even Crazier Than The First 2", + description: false, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSource/index.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSource/index.tsx new file mode 100644 index 0000000000..f33bbaf771 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSource/index.tsx @@ -0,0 +1,148 @@ +import * as QMR from "components"; +import * as CUI from "@chakra-ui/react"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import * as Types from "../types"; +import { DataSourceData, defaultData, OptionNode } from "./data"; +import { useFormContext, useWatch } from "react-hook-form"; +import * as DC from "dataConstants"; +import { cleanString } from "utils/cleanString"; + +interface DataSourceProps { + data?: DataSourceData; +} + +interface DataSourceCheckboxBuilderProps { + data?: OptionNode[]; + label?: string; + parentName?: string; +} + +type DSCBFunc = ({ + data, +}: DataSourceCheckboxBuilderProps) => QMR.CheckboxOption[]; + +type DSCBChildFunc = ({ + data, +}: DataSourceCheckboxBuilderProps) => React.ReactElement[]; + +/** + * Build child checkboxes for data source options + */ +const buildDataSourceCheckboxOptionChildren: DSCBChildFunc = ({ + data, + label, + parentName, +}) => { + const elements: React.ReactElement[] = []; + if (data?.length) { + elements.push( + + ); + } + return elements; +}; + +/** + * Build Data Source checkbox options, and possible child checkbox children + */ +const buildDataSourceOptions: DSCBFunc = ({ data = [], parentName }) => { + const checkBoxOptions: QMR.CheckboxOption[] = []; + for (const node of data) { + const cleanedNodeValue = cleanString(node.value); + const adjustedParentName = parentName + ? `${parentName}-${cleanedNodeValue}` + : cleanedNodeValue; + let children: any = []; + node.subOptions?.forEach((subOption: any, i) => { + children = [ + ...children, + ...buildDataSourceCheckboxOptionChildren({ + data: subOption.options, + label: subOption.label, + parentName: `${adjustedParentName}${i}`, + }), + ]; + }); + + if (node.description) { + let label = ( + <> + Describe the data source ( + + text in this field is included in publicly-reported state-specific + comments + + ): + + ); + children.push( + + ); + } + + checkBoxOptions.push({ + value: cleanedNodeValue, + displayValue: node.value, + children, + }); + } + + return checkBoxOptions; +}; + +/** + * Fully built DataSource component + */ +export const DataSource = ({ data = defaultData }: DataSourceProps) => { + const register = useCustomRegister(); + const { getValues } = useFormContext(); + const watchDataSource = useWatch({ + name: DC.DATA_SOURCE, + defaultValue: getValues().DataSource, + }) as string[] | undefined; + + const showExplanation = watchDataSource && watchDataSource.length >= 2; + + return ( + +
+ +
+ {showExplanation && ( + + + For each data source selected above, describe which reporting + entities used each data source (e.g., health plans, FFS). If the + data source differed across health plans or delivery systems, + identify the number of plans or delivery systems that used each data + source. + + + + )} +
+ ); +}; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSourceCahps/data.ts b/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSourceCahps/data.ts new file mode 100644 index 0000000000..e860916874 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSourceCahps/data.ts @@ -0,0 +1,26 @@ +export interface OptionNode { + value: string; + subOptions?: { + label?: string; + options: OptionNode[]; + }; + description?: boolean; +} + +export interface DataSourceData { + options: OptionNode[]; + optionsLabel: string; +} + +export const defaultData: DataSourceData = { + optionsLabel: "Which version of the CAHPS survey was used for reporting?", + options: [ + { + value: "CAHPS 5.1H", + }, + { + value: "Other Data Source", + description: true, + }, + ], +}; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSourceCahps/index.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSourceCahps/index.tsx new file mode 100644 index 0000000000..636df5bfc6 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/DataSourceCahps/index.tsx @@ -0,0 +1,55 @@ +import * as QMR from "components"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import * as Types from "../types"; +import { DataSourceData, defaultData } from "./data"; +import * as DC from "dataConstants"; + +interface DataSourceProps { + data?: DataSourceData; +} + +/** + * Fully built DataSource component + */ +export const DataSourceRadio = ({ data = defaultData }: DataSourceProps) => { + const register = useCustomRegister(); + + return ( + + + Describe the data source ( + + text in this field is included in publicly-reported + state-specific comments + + ): + + } + key="dataSourceOtherTextArea" + formLabelProps={{ + fontWeight: "normal", + fontSize: "normal", + }} + />, + ], + }, + ]} + /> + + ); +}; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/DateRange/index.test.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/DateRange/index.test.tsx new file mode 100644 index 0000000000..d9da961de4 --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/DateRange/index.test.tsx @@ -0,0 +1,104 @@ +import { DateRange } from "."; +import { renderWithHookForm } from "utils/testUtils/reactHookFormRenderer"; +import { screen } from "@testing-library/react"; +import fireEvent from "@testing-library/user-event"; + +describe("DateRange component, adult", () => { + beforeEach(() => { + renderWithHookForm(); + }); + + it("renders properly", async () => { + expect( + screen.getByText( + "Did your state adhere to Core Set specifications in defining the measurement period for calculating this measure?" + ) + ).toBeInTheDocument(); + }); + + it("renders Adult Measurement Period Table link properly when No is clicked", async () => { + const textArea = await screen.findByLabelText( + "No, our state used a different measurement period." + ); + fireEvent.click(textArea); + + expect( + screen.getByRole("link", { name: "Measurement Period Table" }) + ).toHaveAttribute( + "href", + "https://www.medicaid.gov/medicaid/quality-of-care/performance-measurement/adult-and-child-health-care-quality-measures/adult-core-set-reporting-resources/index.html" + ); + + expect( + screen.getByText( + "For all measures, states should report start and end dates to calculate the denominator. For some measures, the specifications require a “look-back period” before or after the measurement period to determine eligibility or utilization. The measurement period entered in the Start and End Date fields should not include the “look-back period.”" + ) + ).toBeInTheDocument(); + + expect(screen.getByText("Start Date")).toBeInTheDocument(); + expect(screen.getByText("End Date")).toBeInTheDocument(); + }); + + it("does not render suboptions when Yes is clicked", async () => { + const textArea = await screen.findByLabelText( + "Yes, our state adhered to Core Set specifications in defining the measurement period for calculating this measure." + ); + fireEvent.click(textArea); + + expect( + screen.queryByText( + "For all measures, states should report start and end dates to calculate the denominator. For some measures, the specifications require a “look-back period” before or after the measurement period to determine eligibility or utilization. The measurement period entered in the Start and End Date fields should not include the “look-back period.”" + ) + ).toBeNull(); + }); +}); + +describe("DateRange component, child", () => { + beforeEach(() => { + renderWithHookForm(); + }); + + it("renders Child Measurement Period Table link properly when No is clicked", async () => { + const textArea = await screen.findByLabelText( + "No, our state used a different measurement period." + ); + fireEvent.click(textArea); + + expect( + screen.getByText( + "For all measures, states should report start and end dates to calculate the denominator. For some measures, the specifications require a “look-back period” before or after the measurement period to determine eligibility or utilization. The measurement period entered in the Start and End Date fields should not include the “look-back period.”" + ) + ).toBeInTheDocument(); + + expect( + screen.getByRole("link", { name: "Measurement Period Table" }) + ).toHaveAttribute( + "href", + "https://www.medicaid.gov/medicaid/quality-of-care/performance-measurement/adult-and-child-health-care-quality-measures/child-core-set-reporting-resources/index.html" + ); + }); +}); + +describe("DateRange component, Health Home", () => { + it("renders Health Home Measurement Period Table link properly when No is clicked", async () => { + renderWithHookForm(); + + const textArea = await screen.findByLabelText( + "No, our state used a different measurement period." + ); + fireEvent.click(textArea); + + expect( + screen.getByText( + "For all measures, states should report start and end dates to calculate the denominator. For some measures, the specifications require a “look-back period” before or after the measurement period to determine eligibility or utilization. The measurement period entered in the Start and End Date fields should not include the “look-back period.”" + ) + ).toBeInTheDocument(); + + expect( + screen.getByRole("link", { name: "Measurement Period Table" }) + ).toHaveAttribute( + "href", + "https://www.medicaid.gov/state-resource-center/medicaid-state-technical-assistance/health-home-information-resource-center/quality-reporting/index.html" + ); + }); +}); diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/DateRange/index.tsx b/services/ui-src/src/measures/2024/shared/CommonQuestions/DateRange/index.tsx new file mode 100644 index 0000000000..b5939decfd --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/DateRange/index.tsx @@ -0,0 +1,73 @@ +import * as QMR from "components"; +import * as CUI from "@chakra-ui/react"; +import { useCustomRegister } from "hooks/useCustomRegister"; +import * as Types from "../types"; +import * as DC from "dataConstants"; + +const measurementPeriodTableLinks = { + adult: + "https://www.medicaid.gov/medicaid/quality-of-care/performance-measurement/adult-and-child-health-care-quality-measures/adult-core-set-reporting-resources/index.html", + child: + "https://www.medicaid.gov/medicaid/quality-of-care/performance-measurement/adult-and-child-health-care-quality-measures/child-core-set-reporting-resources/index.html", + health: + "https://www.medicaid.gov/state-resource-center/medicaid-state-technical-assistance/health-home-information-resource-center/quality-reporting/index.html", +}; + +interface Props { + type: "adult" | "child" | "health"; +} + +const subTextElement = (link: string) => { + return ( + + Information for each measure is available in the{" "} + + Measurement Period Table + {" "} + resource. + + ); +}; + +export const DateRange = ({ type }: Props) => { + const register = useCustomRegister(); + const link = measurementPeriodTableLinks[type]; + + return ( + + + + + For all measures, states should report start and end dates + to calculate the denominator. For some measures, the + specifications require a “look-back period” before or after + the measurement period to determine eligibility or + utilization. The measurement period entered in the Start and + End Date fields should not include the “look-back period.” + + + + + , + ], + }, + ]} + /> + + ); +}; diff --git a/services/ui-src/src/measures/2024/shared/CommonQuestions/DefinitionsOfPopulation/__snapshots__/index.test.tsx.snap b/services/ui-src/src/measures/2024/shared/CommonQuestions/DefinitionsOfPopulation/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000..23c0d14e1c --- /dev/null +++ b/services/ui-src/src/measures/2024/shared/CommonQuestions/DefinitionsOfPopulation/__snapshots__/index.test.tsx.snap @@ -0,0 +1,3452 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Test DefinitionOfPopulation componnent (ACS Hybrid) Component renders with correct content 1`] = ` +
+
+

+ Definition of Population Included in the Measure +

+
+

+ Definition of denominator +

+
+

+ Please select all populations that are included. For example, if your data include both non-dual Medicaid beneficiaries and Medicare and Medicaid Dual Eligibles, select both: +

+
    +
  • + Denominator includes Medicaid population +
  • +
  • + Denominator includes Medicare and Medicaid Dually-Eligible population +
  • +
+
+
+
+ + +
+ + +
+ + +
+ + +
+
+
+
+
+
+
+
+ +