diff --git a/.github/ISSUE_TEMPLATE/cypress.yml b/.github/ISSUE_TEMPLATE/cypress.yml new file mode 100644 index 0000000000..5ee6bb522e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/cypress.yml @@ -0,0 +1,65 @@ +name: User provided automated tests +description: Submit Gherkin scenarios to generate .feature files for our test suite. +title: '[Test Case]: REPLACE WITH YOUR TEST CASE TITLE' +labels: gherkin, automated test +body: + - type: markdown + attributes: + value: | + ## 📝 Gherkin Scenario Submission + + Thank you for contributing! Please provide your Gherkin scenario in the format below. + + ### Gherkin/Cucumber Test Scenario + - ✍️ **Write or paste your Gherkin scenario in the first block**: + - Ensure the scenario follows the structure of **Given**, **When**, **Then**. + - Ensure the scenario contains a **Feature** and **Scenario** instruction. + - You can have multiple **Scenarios** in a single **Feature**. + - Pay attention to the **indentation** and **keywords**, as misconfigured files will cause issues when executed. + - Pay attention to the data you're using in your scenario. Make sure it don't pose a security risk or exposes any **REAL** [personal identifiable information](https://www2.gov.bc.ca/gov/content/governments/services-for-government/information-management-technology/privacy/personal-information). + - If you're unsure how to format your scenario, [click here for a guide](https://github.com/bcgov/nr-forest-client/tree/main/cypress#readme). + - 🧩 **Additional instructions**: + - Use the second block to provide any **non-default** steps or custom behavior that doesn’t fit into our predefined instructions. You can find a list of ready-made instructions in [our documentation](https://github.com/bcgov/nr-forest-client/tree/main/cypress#readme). + + ### Example + + ```gherkin + Feature: Guess the word + + # The first example has two steps + Scenario: Maker starts a game + When the Maker starts a game + Then the Maker waits for a Breaker to join + + # The second example has three steps + Scenario: Breaker joins a game + Given the Maker has started a game with the word "silky" + When the Breaker joins the Maker's game + Then the Breaker must guess a word with 5 characters + ``` + - type: textarea + id: gherkin_scenario + attributes: + label: Test Scenario + description: 'Please write or paste your Gherkin scenario here.' + render: gherkin + placeholder: | + Feature: [Feature Name] + Scenario: [Scenario Name] + Given [some initial context] + When [some event occurs] + Then [some outcome should occur] + validations: + required: true + + - type: textarea + id: additional_instructions + attributes: + label: Additional Instructions + description: 'Please provide any additional instructions or custom behavior that doesn’t fit into our predefined instructions.' + render: shell + placeholder: | + - [Instruction 1] - [Description] + - [I can choose "rock" as the word] - User can provide a custom word to guess. + validations: + required: false diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml index 3c8ff7f8ce..3d3ae35ac5 100644 --- a/.github/workflows/analysis.yml +++ b/.github/workflows/analysis.yml @@ -31,6 +31,6 @@ jobs: results: name: Analysis Results needs: [tests-java, tests-frontend, repo-reports] - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - run: echo "Workflow completed successfully!" diff --git a/.github/workflows/issue-gherkin.yml b/.github/workflows/issue-gherkin.yml new file mode 100644 index 0000000000..cd0bc6a1cf --- /dev/null +++ b/.github/workflows/issue-gherkin.yml @@ -0,0 +1,49 @@ +name: Create Gherkin Feature from Issue +on: + issues: + types: + - opened + +jobs: + create-feature: + if: contains(github.event.issue.labels.*.name, 'gherkin') + runs-on: ubuntu-24.04 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + fetch-depth: 0 + + - name: Extract feature content + id: feature + uses: bcgov-nr/action-gherkin-issue-processor@v0.0.2 + with: + issue: ${{ github.event.issue.number }} + default_title: "[Test Case]: REPLACE WITH YOUR TEST CASE TITLE" + update_title: true + + - name: Create feature file + run: echo "${{ steps.feature.outputs.feature }}" > cypress/cypress/e2e/upt_${{ github.event.issue.number }}.feature + + - name: Commit & Push changes + uses: Andro999b/push@v1.3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + branch: test/upt_${{ github.event.issue.number }} + force: true + message: | + test(upt #${{ github.event.issue.number }}): ${{ steps.feature.outputs.title }} + + Closes #${{ github.event.issue.number }} + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v5 + with: + branch: test/upt_${{ github.event.issue.number }} + title: "test(upt #${{ github.event.issue.number }}): ${{ steps.feature.outputs.title }}" + body: | + ${{ github.event.issue.body }} + + Closes #${{ github.event.issue.number }} diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml index 1f49639636..216e09d53a 100644 --- a/.github/workflows/merge.yml +++ b/.github/workflows/merge.yml @@ -17,7 +17,7 @@ jobs: name: Set Variables outputs: pr: ${{ steps.pr.outputs.pr }} - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 timeout-minutes: 1 steps: # Get PR number for squash merges to main @@ -28,7 +28,7 @@ jobs: images-test: name: Promote images to TEST needs: [vars] - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: component: [backend, database, frontend, legacy, processor] @@ -47,16 +47,17 @@ jobs: ZONE: test URL: forestclient-tst.nrs.gov.bc.ca environment: test - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Deploys - uses: bcgov-nr/action-deployer-openshift@v3.0.0 + uses: bcgov-nr/action-deployer-openshift@v3.0.1 with: file: common/openshift.init.yml oc_namespace: ${{ secrets.OC_NAMESPACE }} oc_server: ${{ secrets.OC_SERVER }} oc_token: ${{ secrets.OC_TOKEN }} + oc_version: "4.13" overwrite: true parameters: -p ZONE=${{ env.ZONE }} -p NAME=${{ github.event.repository.name }} @@ -108,17 +109,18 @@ jobs: URL: forestclient-tst.nrs.gov.bc.ca ZONE: test environment: test - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Deploy Database Backup - uses: bcgov-nr/action-deployer-openshift@v3.0.0 + uses: bcgov-nr/action-deployer-openshift@v3.0.1 with: file: database/openshift.backup.yml oc_namespace: ${{ secrets.OC_NAMESPACE }} oc_server: ${{ secrets.OC_SERVER }} oc_token: ${{ secrets.OC_TOKEN }} + oc_version: "4.13" overwrite: true parameters: -p ZONE=${{ env.ZONE }} -p NAME=${{ github.event.repository.name }} @@ -133,24 +135,26 @@ jobs: oc create job --from=cronjob/${{ github.event.repository.name }}-${{ env.ZONE }}-database-backup ${{ github.event.repository.name }}-${{ env.ZONE }}-database-backup-$(date +%Y%m%d%H%M%S) - name: Deploy Database - uses: bcgov-nr/action-deployer-openshift@v3.0.0 + uses: bcgov-nr/action-deployer-openshift@v3.0.1 with: file: database/openshift.deploy.yml oc_namespace: ${{ secrets.OC_NAMESPACE }} oc_server: ${{ secrets.OC_SERVER }} oc_token: ${{ secrets.OC_TOKEN }} + oc_version: "4.13" overwrite: false parameters: -p ZONE=${{ env.ZONE }} -p NAME=${{ github.event.repository.name }} -p PROMOTE=${{ github.repository }}/database:${{ env.ZONE }} - name: Deploy Legacy - uses: bcgov-nr/action-deployer-openshift@v3.0.0 + uses: bcgov-nr/action-deployer-openshift@v3.0.1 with: file: legacy/openshift.deploy.yml oc_namespace: ${{ secrets.OC_NAMESPACE }} oc_server: ${{ secrets.OC_SERVER }} oc_token: ${{ secrets.OC_TOKEN }} + oc_version: "4.13" overwrite: true verification_path: health parameters: @@ -159,12 +163,13 @@ jobs: -p ENVIRONMENT=${{ secrets.OC_NAMESPACE }} - name: Deploy Processor - uses: bcgov-nr/action-deployer-openshift@v3.0.0 + uses: bcgov-nr/action-deployer-openshift@v3.0.1 with: file: processor/openshift.deploy.yml oc_namespace: ${{ secrets.OC_NAMESPACE }} oc_server: ${{ secrets.OC_SERVER }} oc_token: ${{ secrets.OC_TOKEN }} + oc_version: "4.13" overwrite: true verification_path: health parameters: @@ -173,23 +178,25 @@ jobs: -p BCREGISTRY_URI='https://bcregistry-prod.apigee.net' - name: Deploy Backend ConfigMap - uses: bcgov-nr/action-deployer-openshift@v3.0.0 + uses: bcgov-nr/action-deployer-openshift@v3.0.1 with: file: backend/openshift.configmap.test.yml oc_namespace: ${{ secrets.OC_NAMESPACE }} oc_server: ${{ secrets.OC_SERVER }} oc_token: ${{ secrets.OC_TOKEN }} + oc_version: "4.13" overwrite: true parameters: -p ZONE=${{ env.ZONE }} - name: Deploy Backend - uses: bcgov-nr/action-deployer-openshift@v3.0.0 + uses: bcgov-nr/action-deployer-openshift@v3.0.1 with: file: backend/openshift.deploy.yml oc_namespace: ${{ secrets.OC_NAMESPACE }} oc_server: ${{ secrets.OC_SERVER }} oc_token: ${{ secrets.OC_TOKEN }} + oc_version: "4.13" overwrite: true verification_path: health parameters: @@ -202,33 +209,36 @@ jobs: -p FRONTEND_URL=${{ env.URL }} - name: Dev data replacement - uses: bcgov-nr/action-deployer-openshift@v3.0.0 + uses: bcgov-nr/action-deployer-openshift@v3.0.1 with: file: database/openshift.dev.yml oc_namespace: ${{ secrets.OC_NAMESPACE }} oc_server: ${{ secrets.OC_SERVER }} oc_token: ${{ secrets.OC_TOKEN }} + oc_version: "4.13" overwrite: true parameters: -p ZONE=${{ env.ZONE }} -p NAME=${{ github.event.repository.name }} - name: Deploy Frontend ConfigMap - uses: bcgov-nr/action-deployer-openshift@v3.0.0 + uses: bcgov-nr/action-deployer-openshift@v3.0.1 with: file: frontend/openshift.configmap.test.yml oc_namespace: ${{ secrets.OC_NAMESPACE }} oc_server: ${{ secrets.OC_SERVER }} oc_token: ${{ secrets.OC_TOKEN }} + oc_version: "4.13" overwrite: true parameters: -p ZONE=${{ env.ZONE }} - name: Deploy Frontend - uses: bcgov-nr/action-deployer-openshift@v3.0.0 + uses: bcgov-nr/action-deployer-openshift@v3.0.1 with: file: frontend/openshift.deploy.yml oc_namespace: ${{ secrets.OC_NAMESPACE }} oc_server: ${{ secrets.OC_SERVER }} oc_token: ${{ secrets.OC_TOKEN }} + oc_version: "4.13" overwrite: true parameters: -p ZONE=${{ env.ZONE }} -p NAME=${{ github.event.repository.name }} @@ -251,7 +261,7 @@ jobs: images-prod: name: Promote images to PROD needs: [test-deploy] - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: component: [backend, frontend, legacy, database, processor] @@ -270,16 +280,17 @@ jobs: URL: forestclient.nrs.gov.bc.ca ZONE: prod environment: prod - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Deploys - uses: bcgov-nr/action-deployer-openshift@v3.0.0 + uses: bcgov-nr/action-deployer-openshift@v3.0.1 with: file: common/openshift.init.yml oc_namespace: ${{ secrets.OC_NAMESPACE }} oc_server: ${{ secrets.OC_SERVER }} oc_token: ${{ secrets.OC_TOKEN }} + oc_version: "4.13" overwrite: true parameters: -p ZONE=${{ env.ZONE }} -p NAME=${{ github.event.repository.name }} @@ -309,21 +320,27 @@ jobs: ZONE: prod URL: forestclient.nrs.gov.bc.ca environment: prod - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Deploy Database Backup - uses: bcgov-nr/action-deployer-openshift@v3.0.0 + uses: bcgov-nr/action-deployer-openshift@v3.0.1 with: file: database/openshift.backup.yml oc_namespace: ${{ secrets.OC_NAMESPACE }} oc_server: ${{ secrets.OC_SERVER }} oc_token: ${{ secrets.OC_TOKEN }} + oc_version: "4.13" overwrite: true parameters: -p ZONE=${{ env.ZONE }} -p NAME=${{ github.event.repository.name }} -p PROMOTE=${{ github.repository }}/database:${{ env.PREV }} + + - name: Install CLI tools from OpenShift Mirror + uses: redhat-actions/openshift-tools-installer@v1 + with: + oc: "4.13" - name: Backup database before update continue-on-error: true @@ -335,24 +352,26 @@ jobs: oc create job --from=cronjob/${{ github.event.repository.name }}-${{ env.ZONE }}-database-backup ${{ github.event.repository.name }}-${{ env.ZONE }}-database-backup-$(date +%Y%m%d%H%M%S) - name: Deploy Database - uses: bcgov-nr/action-deployer-openshift@v3.0.0 + uses: bcgov-nr/action-deployer-openshift@v3.0.1 with: file: database/openshift.deploy.yml oc_namespace: ${{ secrets.OC_NAMESPACE }} oc_server: ${{ secrets.OC_SERVER }} oc_token: ${{ secrets.OC_TOKEN }} + oc_version: "4.13" overwrite: false parameters: -p ZONE=${{ env.ZONE }} -p NAME=${{ github.event.repository.name }} -p PROMOTE=${{ github.repository }}/database:${{ env.PREV }} - name: Deploy Legacy - uses: bcgov-nr/action-deployer-openshift@v3.0.0 + uses: bcgov-nr/action-deployer-openshift@v3.0.1 with: file: legacy/openshift.deploy.yml oc_namespace: ${{ secrets.OC_NAMESPACE }} oc_server: ${{ secrets.OC_SERVER }} oc_token: ${{ secrets.OC_TOKEN }} + oc_version: "4.13" overwrite: true verification_path: health parameters: @@ -361,12 +380,13 @@ jobs: -p ENVIRONMENT=${{ secrets.OC_NAMESPACE }} - name: Deploy Processor - uses: bcgov-nr/action-deployer-openshift@v3.0.0 + uses: bcgov-nr/action-deployer-openshift@v3.0.1 with: file: processor/openshift.deploy.yml oc_namespace: ${{ secrets.OC_NAMESPACE }} oc_server: ${{ secrets.OC_SERVER }} oc_token: ${{ secrets.OC_TOKEN }} + oc_version: "4.13" overwrite: true verification_path: health parameters: @@ -375,23 +395,25 @@ jobs: -p BCREGISTRY_URI='https://bcregistry-prod.apigee.net' - name: Deploy Backend ConfigMap - uses: bcgov-nr/action-deployer-openshift@v3.0.0 + uses: bcgov-nr/action-deployer-openshift@v3.0.1 with: file: backend/openshift.configmap.prod.yml oc_namespace: ${{ secrets.OC_NAMESPACE }} oc_server: ${{ secrets.OC_SERVER }} oc_token: ${{ secrets.OC_TOKEN }} + oc_version: "4.13" overwrite: true parameters: -p ZONE=${{ env.ZONE }} - name: Deploy Backend - uses: bcgov-nr/action-deployer-openshift@v3.0.0 + uses: bcgov-nr/action-deployer-openshift@v3.0.1 with: file: backend/openshift.deploy.yml oc_namespace: ${{ secrets.OC_NAMESPACE }} oc_server: ${{ secrets.OC_SERVER }} oc_token: ${{ secrets.OC_TOKEN }} + oc_version: "4.13" overwrite: true verification_path: health parameters: @@ -404,23 +426,25 @@ jobs: -p FRONTEND_URL=${{ env.URL }} - name: Deploy Frontend ConfigMap - uses: bcgov-nr/action-deployer-openshift@v3.0.0 + uses: bcgov-nr/action-deployer-openshift@v3.0.1 with: file: frontend/openshift.configmap.prod.yml oc_namespace: ${{ secrets.OC_NAMESPACE }} oc_server: ${{ secrets.OC_SERVER }} oc_token: ${{ secrets.OC_TOKEN }} + oc_version: "4.13" overwrite: true parameters: -p ZONE=${{ env.ZONE }} - name: Deploy Frontend - uses: bcgov-nr/action-deployer-openshift@v3.0.0 + uses: bcgov-nr/action-deployer-openshift@v3.0.1 with: file: frontend/openshift.deploy.yml oc_namespace: ${{ secrets.OC_NAMESPACE }} oc_server: ${{ secrets.OC_SERVER }} oc_token: ${{ secrets.OC_TOKEN }} + oc_version: "4.13" overwrite: true parameters: -p ZONE=${{ env.ZONE }} -p NAME=${{ github.event.repository.name }} diff --git a/.github/workflows/pr-close.yml b/.github/workflows/pr-close.yml index e27ce31d5a..e7b162f406 100644 --- a/.github/workflows/pr-close.yml +++ b/.github/workflows/pr-close.yml @@ -24,8 +24,13 @@ jobs: name: Cleanup tools environment needs: [cleanup] environment: tools - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: + - name: Install CLI tools from OpenShift Mirror + uses: redhat-actions/openshift-tools-installer@v1 + with: + oc: "4.13" + - name: Remove the PR database continue-on-error: true run: | diff --git a/.github/workflows/pr-open.yml b/.github/workflows/pr-open.yml index 8a5801ee7f..a6a4d11feb 100644 --- a/.github/workflows/pr-open.yml +++ b/.github/workflows/pr-open.yml @@ -12,7 +12,7 @@ concurrency: jobs: vars: name: Variables - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 outputs: semver: ${{ steps.semver.outputs.tag }} url: ${{ steps.calculate.outputs.url }} @@ -43,7 +43,7 @@ jobs: builds: name: Builds - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 needs: [vars] permissions: packages: write @@ -65,7 +65,7 @@ jobs: build-legacydb: name: Builds (legacydb) - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 needs: [vars] permissions: packages: write @@ -82,30 +82,87 @@ jobs: build_args: | APP_VERSION=${{ needs.vars.outputs.semver }}-${{ github.event.number }} + pre-tools: + name: Pre Deploy Tools + needs: [build-legacydb, vars] + environment: dev + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Install CLI tools from OpenShift Mirror + uses: redhat-actions/openshift-tools-installer@v1 + with: + oc: "4.13" + - name: Scale down legacy + continue-on-error: true + run: | + oc login --token=${{ secrets.OC_TOKEN }} --server=${{ secrets.OC_SERVER }} + oc project ${{ secrets.OC_NAMESPACE }} # Safeguard! + oc scale dc/nr-forest-client-${{ github.event.number }}-legacy --replicas=0 -n ${{ secrets.OC_NAMESPACE }} + undesired_replicas=0 + + while true; do + available_replicas=$(oc get dc/nr-forest-client-${{ github.event.number }}-legacy -n ${{ secrets.OC_NAMESPACE }} -o jsonpath='{.status.availableReplicas}') + + if [[ "$available_replicas" -ge "$undesired_replicas" ]]; then + echo "DeploymentConfig ${{ secrets.OC_NAMESPACE }}-${{ github.event.number }}-legacy is now available with $available_replicas replicas." + break + fi + + echo "Waiting... ($available_replicas pods available)" + sleep 5 + done + deploy-tools: name: Deploy Tools - needs: [build-legacydb, vars] + needs: [pre-tools, build-legacydb, vars] environment: tools env: DOMAIN: apps.silver.devops.gov.bc.ca PREFIX: ${{ needs.vars.outputs.url }} - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Initializing Deployment - uses: bcgov-nr/action-deployer-openshift@v3.0.0 + uses: bcgov-nr/action-deployer-openshift@v3.0.1 with: file: legacydb/openshift.deploy.yml oc_namespace: ${{ secrets.OC_NAMESPACE }} oc_server: ${{ secrets.OC_SERVER }} oc_token: ${{ secrets.OC_TOKEN }} + oc_version: "4.13" overwrite: false parameters: -p ZONE=tools -p ORACLEDB_USER_W=THE -p ORACLEDB_PASSWORD_W=${{ secrets.ORACLEDB_PASSWORD_W }} -p TAG=latest + - name: Install CLI tools from OpenShift Mirror + uses: redhat-actions/openshift-tools-installer@v1 + with: + oc: "4.13" + - name: Remove the PR database + continue-on-error: true + run: | + oc login --token=${{ secrets.OC_TOKEN }} --server=${{ secrets.OC_SERVER }} + oc project ${{ secrets.OC_NAMESPACE }} # Safeguard! + # This removes a new pluggable database, user and service for the PR + for i in {1..5}; do + POD_NAME=$(oc get pods -l app=nr-forest-client-tools -l deployment=nr-forest-client-tools-legacydb -o jsonpath='{.items[0].metadata.name}' 2>/dev/null) + if [ -n "$POD_NAME" ]; then + echo "Pod found: $POD_NAME" + oc exec $POD_NAME -- /opt/oracle/removeDatabase "THE" "PR_${{ github.event.number }}" + break + else + echo "Pod not found, retrying in 10 seconds... ($i/5)" + sleep 10 + fi + done + + if [ -z "$POD_NAME" ]; then + echo "Failed to find the pod after 5 attempts." + fi - name: Create the PR database continue-on-error: true @@ -160,7 +217,7 @@ jobs: oc login --token=${{ secrets.OC_TOKEN }} --server=${{ secrets.OC_SERVER }} oc project ${{ secrets.OC_NAMESPACE }} # Safeguard! oc create job --from=cronjob/nr-forest-client-tools-migratedb migrate-pr${{ github.event.number }}-${{ github.run_attempt }}-$(date +%s) --dry-run=client -o yaml | sed "s/value: main/value: ${ESCAPED_BRANCH_NAME}/" | sed "s/value: \"0\"/value: \"${{ github.event.number }}\"/" | oc apply -f - - + deploy: name: Deploy Application needs: [deploy-tools, builds, vars] @@ -168,17 +225,18 @@ jobs: env: DOMAIN: apps.silver.devops.gov.bc.ca PREFIX: ${{ needs.vars.outputs.url }} - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Initializing Deployment - uses: bcgov-nr/action-deployer-openshift@v3.0.0 + uses: bcgov-nr/action-deployer-openshift@v3.0.1 with: file: common/openshift.init.yml oc_namespace: ${{ secrets.OC_NAMESPACE }} oc_server: ${{ secrets.OC_SERVER }} oc_token: ${{ secrets.OC_TOKEN }} + oc_version: "4.13" overwrite: true parameters: -p ZONE=${{ github.event.number }} @@ -201,17 +259,21 @@ jobs: -p CHES_MAIL_COPY=${{ secrets.CHES_MAIL_COPY }} - name: Deploy Database Backup - uses: bcgov-nr/action-deployer-openshift@v3.0.0 + uses: bcgov-nr/action-deployer-openshift@v3.0.1 with: file: database/openshift.backup.yml oc_namespace: ${{ secrets.OC_NAMESPACE }} oc_server: ${{ secrets.OC_SERVER }} oc_token: ${{ secrets.OC_TOKEN }} + oc_version: "4.13" overwrite: true parameters: -p ZONE=${{ github.event.number }} -p PROMOTE=${{ github.repository }}/database:${{ github.event.number }} - + - name: Install CLI tools from OpenShift Mirror + uses: redhat-actions/openshift-tools-installer@v1 + with: + oc: "4.13" - name: Backup database before update continue-on-error: true run: | @@ -223,24 +285,26 @@ jobs: ${{ github.event.repository.name }}-${{ github.event.number }}-database-backup-$(date +%Y%m%d%H%M%S) - name: Deploy Database - uses: bcgov-nr/action-deployer-openshift@v3.0.0 + uses: bcgov-nr/action-deployer-openshift@v3.0.1 with: file: database/openshift.deploy.yml oc_namespace: ${{ secrets.OC_NAMESPACE }} oc_server: ${{ secrets.OC_SERVER }} oc_token: ${{ secrets.OC_TOKEN }} + oc_version: "4.13" overwrite: false parameters: -p ZONE=${{ github.event.number }} -p PROMOTE=${{ github.repository }}/database:${{ github.event.number }} - name: Deploy Legacy - uses: bcgov-nr/action-deployer-openshift@v3.0.0 + uses: bcgov-nr/action-deployer-openshift@v3.0.1 with: file: legacy/openshift.deploy.yml oc_namespace: ${{ secrets.OC_NAMESPACE }} oc_server: ${{ secrets.OC_SERVER }} oc_token: ${{ secrets.OC_TOKEN }} + oc_version: "4.13" overwrite: true verification_path: health parameters: @@ -250,12 +314,13 @@ jobs: -p ORACLEDB_PORT=1521 - name: Deploy Processor - uses: bcgov-nr/action-deployer-openshift@v3.0.0 + uses: bcgov-nr/action-deployer-openshift@v3.0.1 with: file: processor/openshift.deploy.yml oc_namespace: ${{ secrets.OC_NAMESPACE }} oc_server: ${{ secrets.OC_SERVER }} oc_token: ${{ secrets.OC_TOKEN }} + oc_version: "4.13" overwrite: true verification_path: health parameters: @@ -264,23 +329,25 @@ jobs: -p BCREGISTRY_URI='https://bcregistry-prod.apigee.net' - name: Deploy Backend ConfigMap - uses: bcgov-nr/action-deployer-openshift@v3.0.0 + uses: bcgov-nr/action-deployer-openshift@v3.0.1 with: file: backend/openshift.configmap.dev.yml oc_namespace: ${{ secrets.OC_NAMESPACE }} oc_server: ${{ secrets.OC_SERVER }} oc_token: ${{ secrets.OC_TOKEN }} + oc_version: "4.13" overwrite: true parameters: -p ZONE=${{ github.event.number }} - name: Deploy Backend - uses: bcgov-nr/action-deployer-openshift@v3.0.0 + uses: bcgov-nr/action-deployer-openshift@v3.0.1 with: file: backend/openshift.deploy.yml oc_namespace: ${{ secrets.OC_NAMESPACE }} oc_server: ${{ secrets.OC_SERVER }} oc_token: ${{ secrets.OC_TOKEN }} + oc_version: "4.13" overwrite: true verification_path: health parameters: @@ -293,33 +360,36 @@ jobs: -p FRONTEND_URL=${{ needs.vars.outputs.url }} - name: Dev data replacement - uses: bcgov-nr/action-deployer-openshift@v3.0.0 + uses: bcgov-nr/action-deployer-openshift@v3.0.1 with: file: database/openshift.dev.yml oc_namespace: ${{ secrets.OC_NAMESPACE }} oc_server: ${{ secrets.OC_SERVER }} oc_token: ${{ secrets.OC_TOKEN }} + oc_version: "4.13" overwrite: true parameters: -p ZONE=${{ github.event.number }} - name: Deploy Frontend ConfigMap - uses: bcgov-nr/action-deployer-openshift@v3.0.0 + uses: bcgov-nr/action-deployer-openshift@v3.0.1 with: file: frontend/openshift.configmap.dev.yml oc_namespace: ${{ secrets.OC_NAMESPACE }} oc_server: ${{ secrets.OC_SERVER }} oc_token: ${{ secrets.OC_TOKEN }} + oc_version: "4.13" overwrite: true parameters: -p ZONE=${{ github.event.number }} - name: Deploy Frontend - uses: bcgov-nr/action-deployer-openshift@v3.0.0 + uses: bcgov-nr/action-deployer-openshift@v3.0.1 with: file: frontend/openshift.deploy.yml oc_namespace: ${{ secrets.OC_NAMESPACE }} oc_server: ${{ secrets.OC_SERVER }} oc_token: ${{ secrets.OC_TOKEN }} + oc_version: "4.13" overwrite: true parameters: -p ZONE=${{ github.event.number }} @@ -337,9 +407,9 @@ jobs: cypress-run: name: "User flow test" - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 needs: [deploy, vars] - environment: dev + environment: tools env: URL: ${{ needs.vars.outputs.url }} steps: @@ -358,6 +428,16 @@ jobs: env: CYPRESS_baseUrl: https://${{ env.URL }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CYPRESS_editor_password: ${{ secrets.UAT_EDITOR_PASSWORD }} + CYPRESS_editor_username: ${{ secrets.UAT_EDITOR_USERNAME }} + CYPRESS_admin_password: ${{ secrets.UAT_ADMIN_PASSWORD }} + CYPRESS_admin_username: ${{ secrets.UAT_ADMIN_USERNAME }} + CYPRESS_viewer_password: ${{ secrets.UAT_VIEWER_PASSWORD }} + CYPRESS_viewer_username: ${{ secrets.UAT_VIEWER_USERNAME }} + CYPRESS_bceid_password: ${{ secrets.UAT_BCEID_PASSWORD }} + CYPRESS_bceid_username: ${{ secrets.UAT_BCEID_USERNAME }} + CYPRESS_bcsc_password: ${{ secrets.UAT_BCSC_PASSWORD }} + CYPRESS_bcsc_username: ${{ secrets.UAT_BCSC_USERNAME }} - name: Publish Cypress Results uses: mikepenz/action-junit-report@v4 @@ -370,18 +450,177 @@ jobs: detailed_summary: true job_name: User Journeys + - name: Check for Cypress Screenshots and Videos + run: | + if [ -d "cypress/cypress/screenshots" ] && [ "$(ls -A cypress/cypress/screenshots)" ]; then + echo "Screenshots folder is not empty, uploading artifacts." + echo "screenshots=true" >> $GITHUB_OUTPUT + + else + echo "Screenshots folder is empty or does not exist." + echo "screenshots=false" >> $GITHUB_OUTPUT + fi + + if [ -d "cypress/cypress/videos" ] && [ "$(ls -A cypress/cypress/videos)" ]; then + echo "Videos folder is not empty, uploading artifacts." + echo "videos=true" >> $GITHUB_OUTPUT + + else + echo "Videos folder is empty or does not exist." + echo "videos=false" >> $GITHUB_OUTPUT + fi + id: check_artifacts + - uses: actions/upload-artifact@v4 - name: Upload Cypress Screenshots with error - if: failure() + name: Upload Cypress Screenshots + if: always() with: name: cypress-screenshots path: cypress/cypress/screenshots retention-days: 7 - uses: actions/upload-artifact@v4 - name: Upload Cypress Videos with error - if: failure() + name: Upload Cypress Videos + if: always() with: name: cypress-videos path: cypress/cypress/videos retention-days: 7 + + scale-down-after: + name: Scale down legacy + needs: [cypress-run] + environment: dev + if: always() + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Install CLI tools from OpenShift Mirror + uses: redhat-actions/openshift-tools-installer@v1 + with: + oc: "4.13" + - name: Stop the Legacy Service + continue-on-error: true + run: | + oc login --token=${{ secrets.OC_TOKEN }} --server=${{ secrets.OC_SERVER }} + oc project ${{ secrets.OC_NAMESPACE }} # Safeguard! + oc scale dc/nr-forest-client-${{ github.event.number }}-legacy --replicas=0 + undesired_replicas=0 + while true; do + available_replicas=$(oc get dc/nr-forest-client-${{ github.event.number }}-legacy -n ${{ secrets.OC_NAMESPACE }} -o jsonpath='{.status.availableReplicas}') + + if [[ "$available_replicas" -ge "$undesired_replicas" ]]; then + echo "DeploymentConfig ${{ secrets.OC_NAMESPACE }}-${{ github.event.number }}-legacy is now available with $available_replicas replicas." + break + fi + + echo "Waiting... ($available_replicas pods available)" + sleep 5 + done + + recreate-database: + name: Recreate database + needs: [scale-down-after] + environment: tools + if: always() + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Install CLI tools from OpenShift Mirror + uses: redhat-actions/openshift-tools-installer@v1 + with: + oc: "4.13" + - name: Remove the PR database + continue-on-error: true + run: | + oc login --token=${{ secrets.OC_TOKEN }} --server=${{ secrets.OC_SERVER }} + oc project ${{ secrets.OC_NAMESPACE }} # Safeguard! + # This removes a new pluggable database, user and service for the PR + for i in {1..5}; do + POD_NAME=$(oc get pods -l app=nr-forest-client-tools -l deployment=nr-forest-client-tools-legacydb -o jsonpath='{.items[0].metadata.name}' 2>/dev/null) + if [ -n "$POD_NAME" ]; then + echo "Pod found: $POD_NAME" + oc exec $POD_NAME -- /opt/oracle/removeDatabase "THE" "PR_${{ github.event.number }}" + break + else + echo "Pod not found, retrying in 10 seconds... ($i/5)" + sleep 10 + fi + done + + if [ -z "$POD_NAME" ]; then + echo "Failed to find the pod after 5 attempts." + fi + + - name: Create the PR database + continue-on-error: true + run: | + oc login --token=${{ secrets.OC_TOKEN }} --server=${{ secrets.OC_SERVER }} + oc project ${{ secrets.OC_NAMESPACE }} # Safeguard! + # This creates a new pluggable database for the PR + for i in {1..5}; do + POD_NAME=$(oc get pods -l app=nr-forest-client-tools -l deployment=nr-forest-client-tools-legacydb -o jsonpath='{.items[0].metadata.name}' 2>/dev/null) + if [ -n "$POD_NAME" ]; then + echo "Pod found: $POD_NAME" + oc exec $POD_NAME -- /opt/oracle/createDatabase PR_${{ github.event.number }} + break + else + echo "Pod not found, retrying in 10 seconds... ($i/5)" + sleep 10 + fi + done + + if [ -z "$POD_NAME" ]; then + echo "Failed to find the pod after 5 attempts." + fi + + - name: Create the PR user + continue-on-error: true + run: | + oc login --token=${{ secrets.OC_TOKEN }} --server=${{ secrets.OC_SERVER }} + oc project ${{ secrets.OC_NAMESPACE }} # Safeguard! + # This creates a new pluggable database for the PR + for i in {1..5}; do + POD_NAME=$(oc get pods -l app=nr-forest-client-tools -l deployment=nr-forest-client-tools-legacydb -o jsonpath='{.items[0].metadata.name}' 2>/dev/null) + if [ -n "$POD_NAME" ]; then + echo "Pod found: $POD_NAME" + oc exec $POD_NAME -- /opt/oracle/createAppUser "THE" "${{ secrets.ORACLEDB_PASSWORD_W }}_${{ github.event.number }}" "PR_${{ github.event.number }}" + break + else + echo "Pod not found, retrying in 10 seconds... ($i/5)" + sleep 10 + fi + done + + if [ -z "$POD_NAME" ]; then + echo "Failed to find the pod after 5 attempts." + fi + + - name: Migrate the PR database + continue-on-error: true + run: | + BRANCH_NAME="${{ github.head_ref }}" + # Escape slashes and other special characters + ESCAPED_BRANCH_NAME=$(echo "$BRANCH_NAME" | sed 's/[\/&]/\\&/g') + oc login --token=${{ secrets.OC_TOKEN }} --server=${{ secrets.OC_SERVER }} + oc project ${{ secrets.OC_NAMESPACE }} # Safeguard! + oc create job --from=cronjob/nr-forest-client-tools-migratedb migrate-pr${{ github.event.number }}-${{ github.run_attempt }}-$(date +%s) --dry-run=client -o yaml | sed "s/value: main/value: ${ESCAPED_BRANCH_NAME}/" | sed "s/value: \"0\"/value: \"${{ github.event.number }}\"/" | oc apply -f - + + scale-up-legacy: + name: Scale up legacy + needs: [recreate-database] + environment: dev + if: always() + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Install CLI tools from OpenShift Mirror + uses: redhat-actions/openshift-tools-installer@v1 + with: + oc: "4.13" + - name: Start the Legacy Service + continue-on-error: true + run: | + oc login --token=${{ secrets.OC_TOKEN }} --server=${{ secrets.OC_SERVER }} + oc project ${{ secrets.OC_NAMESPACE }} # Safeguard! + oc scale dc/nr-forest-client-${{ github.event.number }}-legacy --replicas=1 diff --git a/.github/workflows/pr-validate.yml b/.github/workflows/pr-validate.yml index edfcf09307..e898426329 100644 --- a/.github/workflows/pr-validate.yml +++ b/.github/workflows/pr-validate.yml @@ -11,7 +11,7 @@ concurrency: jobs: vars: name: Variables - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 outputs: url: ${{ steps.calculate.outputs.url }} steps: @@ -29,7 +29,7 @@ jobs: changelog: name: Pull Request Validation - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: contents: read pull-requests: write @@ -73,6 +73,6 @@ jobs: results: name: Validate Results needs: [changelog, validate] - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - run: echo "Workflow completed successfully!" diff --git a/.github/workflows/reusable-doc-gen.yml b/.github/workflows/reusable-doc-gen.yml index 639ab59257..49cdbcf0dc 100644 --- a/.github/workflows/reusable-doc-gen.yml +++ b/.github/workflows/reusable-doc-gen.yml @@ -9,7 +9,7 @@ jobs: outputs: user: ${{ steps.data.outputs.user }} pass: ${{ steps.data.outputs.pass }} - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 timeout-minutes: 1 steps: - name: Generate random username and password @@ -23,7 +23,7 @@ jobs: schemaspy: name: Generate Documentation - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 needs: [vars] services: postgres: diff --git a/.github/workflows/reusable-tests-be.yml b/.github/workflows/reusable-tests-be.yml index 6a7a1e8b32..237edd720a 100644 --- a/.github/workflows/reusable-tests-be.yml +++ b/.github/workflows/reusable-tests-be.yml @@ -7,7 +7,7 @@ jobs: tests-java: name: Backend Tests if: github.event_name != 'pull_request' || !github.event.pull_request.draft - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: bcgov-nr/action-test-and-analyse-java@v1.0.2 name: Backend Coverage diff --git a/.github/workflows/reusable-tests-fe.yml b/.github/workflows/reusable-tests-fe.yml index b2572dac1b..5737c6a31c 100644 --- a/.github/workflows/reusable-tests-fe.yml +++ b/.github/workflows/reusable-tests-fe.yml @@ -7,7 +7,7 @@ jobs: tests-frontend: name: Frontend Unit Tests if: github.event_name != 'pull_request' || !github.event.pull_request.draft - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: bcgov-nr/action-test-and-analyse@v1.2.1 env: diff --git a/.github/workflows/reusable-tests-repo.yml b/.github/workflows/reusable-tests-repo.yml index d48c92c474..48f695c28b 100644 --- a/.github/workflows/reusable-tests-repo.yml +++ b/.github/workflows/reusable-tests-repo.yml @@ -7,7 +7,7 @@ jobs: trivy: name: Repository Report if: github.event_name != 'pull_request' || !github.event.pull_request.draft - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Run Trivy vulnerability scanner in repo mode @@ -27,7 +27,7 @@ jobs: codeql: name: Semantic Code Analysis - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 permissions: actions: read contents: read diff --git a/.github/workflows/scheduled.yml b/.github/workflows/scheduled.yml index 1e313ee262..3c573288e9 100644 --- a/.github/workflows/scheduled.yml +++ b/.github/workflows/scheduled.yml @@ -14,7 +14,7 @@ jobs: uses: ./.github/workflows/reusable-doc-gen.yml zap_scan: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 name: Penetration Tests env: DOMAIN: apps.silver.devops.gov.bc.ca diff --git a/backend/openshift.configmap.dev.yml b/backend/openshift.configmap.dev.yml index d31b88ca14..87173564b0 100644 --- a/backend/openshift.configmap.dev.yml +++ b/backend/openshift.configmap.dev.yml @@ -28,3 +28,9 @@ objects: info: app: component: ${COMPONENT} + ca: + bc: + gov: + nrs: + idirMaxSubmissions: 65535 + otherMaxSubmissions: 65535 diff --git a/backend/src/main/java/ca/bc/gov/app/dto/client/LegalTypeEnum.java b/backend/src/main/java/ca/bc/gov/app/dto/client/LegalTypeEnum.java index ce5cb36873..50e449dd9c 100644 --- a/backend/src/main/java/ca/bc/gov/app/dto/client/LegalTypeEnum.java +++ b/backend/src/main/java/ca/bc/gov/app/dto/client/LegalTypeEnum.java @@ -1,6 +1,7 @@ package ca.bc.gov.app.dto.client; import com.fasterxml.jackson.annotation.JsonCreator; +import ca.bc.gov.app.exception.UnsupportedLegalTypeException; import java.util.HashMap; import java.util.Map; diff --git a/backend/src/main/java/ca/bc/gov/app/exception/UnsuportedClientTypeException.java b/backend/src/main/java/ca/bc/gov/app/exception/UnsupportedClientTypeException.java similarity index 76% rename from backend/src/main/java/ca/bc/gov/app/exception/UnsuportedClientTypeException.java rename to backend/src/main/java/ca/bc/gov/app/exception/UnsupportedClientTypeException.java index 8cce606115..2a98477343 100644 --- a/backend/src/main/java/ca/bc/gov/app/exception/UnsuportedClientTypeException.java +++ b/backend/src/main/java/ca/bc/gov/app/exception/UnsupportedClientTypeException.java @@ -6,9 +6,9 @@ import org.springframework.web.server.ResponseStatusException; @ResponseStatus(HttpStatus.NOT_ACCEPTABLE) -public class UnsuportedClientTypeException extends ResponseStatusException { +public class UnsupportedClientTypeException extends ResponseStatusException { - public UnsuportedClientTypeException(String clientType) { + public UnsupportedClientTypeException(String clientType) { super(HttpStatus.NOT_ACCEPTABLE, String.format("Client type %s is not supported at the moment", ClientTypeCodeEnum.as(clientType))); diff --git a/backend/src/main/java/ca/bc/gov/app/exception/UnsupportedLegalTypeException.java b/backend/src/main/java/ca/bc/gov/app/exception/UnsupportedLegalTypeException.java new file mode 100644 index 0000000000..16f26ad3d7 --- /dev/null +++ b/backend/src/main/java/ca/bc/gov/app/exception/UnsupportedLegalTypeException.java @@ -0,0 +1,13 @@ +package ca.bc.gov.app.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.server.ResponseStatusException; + +@ResponseStatus(HttpStatus.NOT_ACCEPTABLE) +public class UnsupportedLegalTypeException extends ResponseStatusException { + public UnsupportedLegalTypeException(String legalType) { + super(HttpStatus.NOT_ACCEPTABLE, "Unsupported Legal Type: " + legalType); + } +} + diff --git a/backend/src/main/java/ca/bc/gov/app/service/client/ClientService.java b/backend/src/main/java/ca/bc/gov/app/service/client/ClientService.java index 417b9543fe..c8cd00f010 100644 --- a/backend/src/main/java/ca/bc/gov/app/service/client/ClientService.java +++ b/backend/src/main/java/ca/bc/gov/app/service/client/ClientService.java @@ -21,7 +21,8 @@ import ca.bc.gov.app.exception.InvalidAccessTokenException; import ca.bc.gov.app.exception.NoClientDataFound; import ca.bc.gov.app.exception.UnableToProcessRequestException; -import ca.bc.gov.app.exception.UnsuportedClientTypeException; +import ca.bc.gov.app.exception.UnsupportedClientTypeException; +import ca.bc.gov.app.exception.UnsupportedLegalTypeException; import ca.bc.gov.app.repository.client.ClientTypeCodeRepository; import ca.bc.gov.app.repository.client.ContactTypeCodeRepository; import ca.bc.gov.app.repository.client.CountryCodeRepository; @@ -220,87 +221,82 @@ public Mono getClientDetails( String businessId, String provider ) { - log.info("Loading details for {}", clientNumber); - return - bcRegistryService - .requestDocumentData(clientNumber) - .next() - .doOnNext(document -> - log.info("Searching on Oracle legacy db for {} {}", - document.business().identifier(), - document.business().getResolvedLegalName() - ) - ) - .flatMap(document -> - legacyService - .searchLegacy( - document.business().identifier(), - document.business().getResolvedLegalName(), - userId, - businessId - ) - .next() - .filter(isMatchWith(document)) - .doOnNext(legacy -> - log.info("Found legacy entry for {} {}", - document.business().identifier(), - document.business().getResolvedLegalName() - ) - ) - .flatMap(legacy -> Mono - .error( - new ClientAlreadyExistException( - legacy.clientNumber(), - document.business().identifier(), - document.business().getResolvedLegalName()) - ) - ) - .defaultIfEmpty(document) - .doOnNext(value -> - log.info("No entry found on legacy for {} {}", - document.business().identifier(), - document.business().getResolvedLegalName() - ) - ) - ) - .map(BcRegistryDocumentDto.class::cast) - - .flatMap(client -> { - // FSADT1-1388: Allow IDIR users to search for any client type + log.info("Loading details for {}", clientNumber); + return bcRegistryService + .requestDocumentData(clientNumber) + .next() + .doOnNext(document -> + log.info("Searching on Oracle legacy db for {} {}", + document.business().identifier(), + document.business().getResolvedLegalName() + ) + ) + .flatMap(document -> legacyService + .searchLegacy( + document.business().identifier(), + document.business().getResolvedLegalName(), + userId, + businessId + ) + .next() + .filter(isMatchWith(document)) + .doOnNext(legacy -> + log.info("Found legacy entry for {} {}", + document.business().identifier(), + document.business().getResolvedLegalName() + ) + ) + .flatMap(legacy -> Mono.error( + new ClientAlreadyExistException( + legacy.clientNumber(), + document.business().identifier(), + document.business().getResolvedLegalName()) + )) + .defaultIfEmpty(document) + .doOnNext(value -> + log.info("No entry found on legacy for {} {}", + document.business().identifier(), + document.business().getResolvedLegalName() + ) + ) + ) + .map(BcRegistryDocumentDto.class::cast) + + .flatMap(client -> { + // Check for unsupported legal type + LegalTypeEnum legalType = LegalTypeEnum.fromValue(client.business().legalType()); + if (legalType == null) { + return Mono.error( + new UnsupportedLegalTypeException(client.business().legalType()) + ); + } + + // FSADT1-1388: Allow IDIR users to search for any client type if (provider.equalsIgnoreCase("idir")) { - return Mono.just(client); + return Mono.just(client); } if (ApplicationConstant.AVAILABLE_CLIENT_TYPES.contains( - ClientValidationUtils.getClientType( - LegalTypeEnum.valueOf(client.business().legalType()) - ) - .toString() - ) - ) { - return Mono.just(client); + ClientValidationUtils.getClientType(legalType).toString() + )) { + return Mono.just(client); } - return Mono.error( - new UnsuportedClientTypeException(ClientValidationUtils.getClientType( - LegalTypeEnum.valueOf(client.business().legalType()) - ) - .toString() - )); - }) - - //if document type is SP and party contains only one entry that is not a person, fail - .filter(document -> - // FSADT1-1388: Allow IDIR users to search for any client type - provider.equalsIgnoreCase("idir") || - !("SP".equalsIgnoreCase(document.business().legalType()) - && document.parties().size() == 1 - && !document.parties().get(0).isPerson() - ) - ) - .flatMap(buildDetails()) - .switchIfEmpty(Mono.error(new UnableToProcessRequestException( - "Unable to process request. This sole proprietor is not owner by a person" - ))); + + return Mono.error(new UnsupportedClientTypeException( + ClientValidationUtils.getClientType(legalType).toString() + )); + }) + + // If document type is SP and party contains only one entry that is not a person, fail + .filter(document -> provider.equalsIgnoreCase("idir") || + !("SP".equalsIgnoreCase(document.business().legalType()) && + document.parties().size() == 1 && + !document.parties().get(0).isPerson()) + ) + .flatMap(buildDetails()) + .switchIfEmpty(Mono.error(new UnableToProcessRequestException( + "Unable to process request. This sole proprietor is not owned by a person" + ))); } /** diff --git a/cypress/README.md b/cypress/README.md index 4e87d28fa0..873d598048 100644 --- a/cypress/README.md +++ b/cypress/README.md @@ -11,24 +11,28 @@ The below sections are intended to explain how the framework works, along with a [Cucumber](https://cucumber.io/) is a popular tool used for behavior-driven development (BDD) in software testing. It allows you to write executable specifications in a natural language format called Gherkin. Gherkin is a plain-text language that is easy to understand and can be used by non-technical stakeholders as well. In this tutorial, we will guide you through the basics of writing a Cucumber/Gherkin file for end-to-end tests. To learn more about Cucumber, Gherking and BDD, you can enroll on a [free course](https://school.cucumber.io/courses/bdd-overview-for-business-analysts-and-product-owners). -It is always good to read about Gherkin language format before start but in summary, Gherkin is a writing format that leverages plain english to allow users to describe the expected behavior in simple steps that is simple to understand by non-technical persons, but also makes sense to be executed in that specific order. Think of it like a set of steps in a cake recipe. Each test is written in a file with the `.feature` extension and is saved inside [cypress/e2e](cypress/e2e) folder on this repo, you can check some of the existing files for reference. +It is always good to read about Gherkin language format before start but in summary, Gherkin is a writing format that leverages plain english to allow users to describe the expected behavior in simple steps that is simple to understand by non-technical persons, but also makes sense to be executed in that specific order. Think of it like a set of steps in a cake recipe. - Avoid using names with spaces or special characters. Replace spaces with `_` and special characters for it's non-special character counterpart +To make things easier for non-technical team members, we developed a strategy to leverage the use of [github issues](https://github.com/bcgov/nr-forest-client/issues), something that is quite similar to Jira Tickets. Click on `New issue` and a list of possible issue types will appear, select the `User provided automated test-case` one, and a form will appear. Follow the instructions on it on how to fill up the form with the appropriate data. This will then be automatically converted to a `feature file` that will be used for test. -Here is how to get started: + Pay attention to the format of the test description. Gherkin feature files are sensitive to spacing and the keywords are really picky when it comes to casing (uppercase x lowercase). -### Step 1: Creating a Feature File +Another thing that the development team did to facilitate the usage of Gherkin is the ready-to-use collection of instructions that can be used to speed up the writing of test cases. Check the [existing step instructions](#existing-step-instructions) topic for a list of steps already implemented. -Create a new file with the `.feature` extension inside the [cypress/e2e](cypress/e2e) folder. This file will contain your Gherkin scenarios. Start by writing a short description of the feature you are testing, preceded by the Feature keyword. For example: +Without further ado, let's get started: + +### Creating a Feature + +Every test group is called a `Feature` on Gherkin. This is the first keyword here and it will have a meaningful name. This will contain your scenarios and it can include a short description of the feature you are testing, preceded by the Feature keyword. For example: ```gherkin Feature: User Registration - As a new user - I want to register on the website - So that I can access exclusive content + As a new user I want to register on the website so that I can access exclusive content ``` -### Step 2: Writing Scenarios + Be aware that the description should be a level deeper than the Feature itself. You can use tab or two spaces + +### Creating Scenarios Scenarios represent specific test cases. Each scenario consists of a series of steps. Steps can be one of the following: **Given**, **When**, **Then**, **And**, or **But**. Here's an example scenario for our user registration feature: @@ -40,14 +44,101 @@ Scenario: Successful user registration Then I should see a success message ``` -Congratulations! You have successfully written a basic Cucumber/Gherkin file for end-to-end tests. You can continue adding more scenarios and step definitions to cover different test cases in the application. + Be aware that each step should be a level deeper than the Scenario itself. You can use tab or two spaces + Also, keep in mind that this should be at least one level deeper than the feature above it. + +Here's the final product of the above feature and scenario combined. + +```gherkin +Feature: User Registration + As a new user I want to register on the website so that I can access exclusive content + + Scenario: Successful user registration + Given I am on the registration page + When I fill in the registration form with valid information + And I click the "Register" button + Then I should see a success message +``` + +Congratulations! You have successfully written a basic Cucumber/Gherkin end-to-end tests. You can continue adding more scenarios and step definitions to cover different test cases in the application. Remember, the power of Cucumber lies in its ability to bridge the communication gap between technical and non-technical team members, enabling collaboration and providing a common language for defining software behavior. +### Existing step instructions + +The development team created a set of pre-defined step definitions that can be used to leverage the use of Gherkin tests and speed up the adoption of it with non-technical team members. The below steps should be used `as-is`, without changing the case of any letter, except for `variables`. + +A variable is a piece of information that will be used to pass down a information. Every variable should be wrapped in double quotes (`"`). We will have two distinc group of variables described below, and they will be defined as `input` and `field name`. **Input** variables are the actual data that you want to select or insert in the form, such as the first name `James` or the `Individual` type of user. **Field name** is a type of variable used to identify a field in the form based on it's label name. Some examples are `First name` and `Client type`. They should have the exact name and casing as the form, otherwise the test won't be able to find the input to fill the data in. + + The below list of instructions can be used in any of the steps, such as `Given`, `When`, `Then`, `And` or `But`. They are case sensitive and should be used as they are. + +Also, to speed up the process and avoid credentials being leaked everywhere, we have a special instruction that will be used to login using some specific user types. This instruction uses the `annotation` and it should be used at the `scenario` level. For more information, please refer to the [credentials](#credentials) topic below. + +Here's a list of instructions that are already implemented and can be used: + +| Step | Variables | Description | Example | +| --------------- | ------------------ | ------------------------------- | ------------------ | +| I visit {input} | `input` as the URL | Navigate to a specific URL path | I visit "/landing" | +| I can read {input} | `input` as the text on the screen | Look up for a specific text on the screen | I can read "Create new client" | +| I cannot see {input} | `input` as the content of something that should not be on the screen | Look up for a specific text or component that should not be on the screen | I cannot see "Error" | +| I wait for the text {input} to appear | `input` as the text to be waited to appear on the screen | Wait for a specific text to appear on the screen | I wait for the text "Success" to appear | +| I click on the {field name} button | `field name` as the text/name of the button to be clicked | Finds and click a button | I click on the "Next" button | +| I click on next | - | Click on the next button. It is a variation of the button click, with a limited scope | I click on next | +| I submit | - | Submit the form at the end. Is a variation of the button click, but with a more limited scope | I submit | +| I type {input} into the {field name} form input | `input` as the data to be inserted and `field name` as the field name, based on a label | Insert data into a input text field | I type "James" into the "First name" form input | +| I clear the {field name} form input | `field name` as the field name, based on a label | Clear the content of a input text field | I clear the "First name" form input | +| I type {input} into the {field name} form input for the {field name} | `input` as the data to be inserted and `field name` as the field name, based on a label and the last `field name` is a reference for the group where this information should be inserted. | Insert data into a input text field that belongs to a specific group | I type "James" into the "First name" form input for the "Primary contact" | +| I replace the {field name} with {input} form input | `field name` as the field name, based on a label and `input` as the data to be inserted | Replace the content of a input text field | I replace the "First name" with "John" form input | +| I replace the {field name} with {input} form input for the {field name} | `field name` as the field name, based on a label and `input` as the data to be inserted and the last `field name` is a reference for the group where this information should be inserted. | Replace the content of a input text field | I replace the "First name" with "John" form input | +| I type {input} into the {field name} form input area | `input` as the data to be inserted and `field name` as the field name, based on a label | Insert data into a input area | I type "All good" into the "Notes" form input area | +| I clear the {field name} form input area | `field name` as the field name, based on a label | Clear the content of a input area | I clear the "First name" form input area | +| I type {input} into the {field name} form input area for the {field name} | `input` as the data to be inserted and `field name` as the field name, based on a label and the last `field name` is a reference for the group where this information should be inserted. | Insert data into a input area | I type "All good" into the "Notes" form input area for the "Primary contact" | +| I select {input} from the {field name} form input | `input` as the data to be selected and `field name` as the field name, based on a label | Select a option from a dropdown | I select "Individual" from the "Client type" form input | +| I select {input} from the {field name} form input for the {field name} | `input` as the data to be selected and `field name` as the field name, based on a label and the last `field name` is a reference for the group where this information should be inserted. | Select a option from a dropdown | I select "Billing" from the "Client type" form input for the "Primary contact" | +| I select {input} from the {field name} multiselect | `input` as the data to be selected and `field name` as the field name, based on a label | Select a option from a multi select dropdown | I select "Individual" from the "Contact type" multiselect | +| I select {input} from the {field name} multiselect for the {field name} | `input` as the data to be selected and `field name` as the field name, based on a label and the last `field name` is a reference for the group where this information should be inserted. | Select a option from a multi select dropdown | I select "Office" from the "Location name" multiselect for the "Primary contact| +| I type {input} and select {input} from the {field name} form autocomplete | `input` as the data to be inserted, being the first one the text to be typed and the second one the text to be selected and `field name` as the field name, based on a label | Type into the autocomplete and selects one of the possible results | I type "James" and select "James Bond" from the "Client name" form autocomplete | +| I type {input} and select {input} from the {field name} form autocomplete for the {field name} | `input` as the data to be inserted, being the first one the text to be typed and the second one the text to be selected and `field name` as the field name, based on a label and the last `field name` is a reference for the group where this information should be inserted. | Type into the autocomplete and selects one of the possible results | I type "2975 Jutl" and select "2975 Jutland Rd" from the "Street address or PO box" form autocomplete for the "Primary location | +| I add a new location called {input} | `input` as the name of the location to be added | Add a new location to the list | I add a new location called "New York Office" | +| I addd a new contact called {input} | `input` as the name of the contact to be added | Add a new contact to the list | I add a new contact called "Johnathan Wick" | +| I should see the {input} message {input} on the {field name} | `input` as the type of message, being **error** or **warning**, `input` as the text of the message (can be partial) and `field name` as the location/group where the notification should appear, such as the **top**, **Primary contact** or **Office** | Check if a specific message is displayed on the screen | I should see the "error" message "Invalid email" on the "top" | +| The field {field name} should have the {input} message {input} | `field name` as the field name, based on a label, `input` as the type of message, being **error** or **warning**, `input` as the text of the message (can be partial) | Check if a specific message is displayed on a specific field | The field "Email" should have the "error" message "Invalid email" | +| I fill the form as follows | - | This is a special instruction that will be followed by a table with the data to be inserted in the form. More on the [data tables](#data-tables) topic below | I fill the form as follows | +| I fill the {input} address with the following | `input` as the location name value, or `Primary location` for the primary location | This is a special instruction that will be followed by a table with the data to be inserted in the form. More on the [data tables](#data-tables) topic below | I fill the "Primary location" address with the following | +| I fill the {input} information with the following | `input` as the location or contact name value, or `Primary contact` for the primary contact | This is a special instruction that will be followed by a table with the data to be inserted in the form. More on the [data tables](#data-tables) topic below | I fill the "Johnathan Wick" information with the following | +| I mark {input} on the {field name} {input} input | `input` as the value to be marked, `field name` as the field name, based on a label and the last `input` as the type of the input, being **radio** or **checkbox** | Mark a specific input | I mark "Yes" on the "Primary contact" "radio" input | +| The {field name} component is using the font {input} for the {field name} | `field name` as the field name, based on a label and `input` as the font name and the last `field name` is a reference for the group where this information should be inserted. | Check if a specific font is being used on a component | The "Business information" component is using the font "Arial" | +| The {input} has weight {input} inside the {field name} | `input` as the text to be checked, `input` as the font weight and `field name` as the location/group where the text should be | Check if a specific font weight is being used on a text | The "Business information" has weight "bold" inside the "form" | +| The {input} size is {input} inside the {field name} | `input` as the text to be checked, `input` as the font size and `field name` as the location/group where the text should be | Check if a specific font size is being used on a text | The "Business information" size is "16px" inside the "form" | ## For Developers -The developer will implement one `.ts` file for each `.feature` file, using the same name. Ex: `sample.feature` will have a corresponding `sample.ts` file. +Each test is written in a file with the `.feature` extension and is saved inside [cypress/e2e](cypress/e2e) folder on this repo, you can check some of the existing files for reference. The developer will implement one `.ts` file for each `.feature` file, using the same name. Ex: `sample.feature` will have a corresponding `sample.ts` file. This `.ts` file is only required if the `.feature` file has any instruction that is too specific to be implemented as a common step verbate. + + Avoid using names with spaces or special characters. Replace spaces with `_` and special characters for it's non-special character counterpart + +### Creating a Feature File + +Create a new file with the `.feature` extension inside the [cypress/e2e](cypress/e2e) folder. This file will contain your Gherkin scenarios. Start by writing a short description of the feature you are testing, preceded by the Feature keyword. For example: + +```gherkin +Feature: User Registration + As a new user + I want to register on the website + So that I can access exclusive content +``` + +### Writing Scenarios + +Scenarios represent specific test cases. Each scenario consists of a series of steps. Steps can be one of the following: **Given**, **When**, **Then**, **And**, or **But**. Here's an example scenario for our user registration feature: + +```gherkin +Scenario: Successful user registration + Given I am on the registration page + When I fill in the registration form with valid information + And I click the "Register" button + Then I should see a success message +``` ### Writing Step Definitions @@ -81,4 +172,57 @@ You can use the deployed application for the test, or the local environment. Ide After the tests are executed, it will generate a few artefacts as proof of execution, such as screenshots and videos. This is good for reference, in case of an error, or to validate a scenario with the rest of the team. -When a feature file exists without the corresponding implementation, it will make the automated test fail. **DON'T PANIC**. This is the expected behavior if you're adding a new scenario. One of the developers will be notified when new things are created so they can deal with the implementation of that scenario. \ No newline at end of file +When a feature file has a instruction without the corresponding implementation step, it will make the automated test fail. **DON'T PANIC**. This is a expected behavior if you're adding a new instruction. One of the developers will be notified when new things are created so they can deal with the implementation of that specific step. + +## Data tables + +Data tables makes the form filling process way easier and faster. It allows you to insert multiple data at once, without the need to write each step individually. The data table should be inserted after the `I fill the form as follows` instruction, and it should have 3 columns, the first one being the field name, the second one the data to be inserted and the third one the kind of field. + +The kind of field is a special instruction that will be used to identify the type of field that is being filled. The possible values are `text`, `select`, `multiselect`, `autocomplete` and `textbox`. + +Here's an example of a data table: + +```gherkin +Scenario: Submit individuals + When I click on the "Create client" button + And I can read "Create client" + Then I select "Individual" from the "Client type" form input + And I fill the form as follows + | Field name | Value | Type | + | First name | James | text | + | Last name | Baxter | text | + | Year | 1990 | text | + | Month | 01 | text | + | Day | 01 | text | + | ID type | Canadian driver's licence | select | + | ID number | 4417845 | text | +``` + +Pay attention to the way the data table is structured, as it is very sensitive to spacing and the number of columns. The `Type` column should be written in lowercase, the `Field name` should be the exact name of the field in the form and the `Value` should be the data to be inserted or selected. The table itself is composed by the `|` character to separate the columns and the rows, it is required to have a `|` at the beginning and at the end of each row. Also pay attention to the spacing required to make the table work, as this follows the Gherkin language format. + +One important case here to note is that for the `address` and `contact` information groups, the `contact` or `address` should be created/enabled first. If you declare a `contact` information before the `contact` group is created, the test will fail. The same applies for the `address` group. + +For `address` and `contact` we have a special instruction that can reduce the amount of steps required to start the new group. + +## Credentials + +To avoid credentials being leaked everywhere, we have a special instruction that will be used to login using some specific user types. This instruction uses the `annotation` and it should be used at the `scenario` level. The annotation should be written in the following format: + +```gherkin +@loginAsEditor +Scenario: Submit individuals + When I click on the "Create client" button + And I can read "Create client" +``` + +The `@loginAsEditor` is a special instruction that will be used to login as a specific user type. The `@` symbol is required to be used before the instruction, and it should be placed at the beginning of the scenario. The instruction itself should be written in camelCase, with the first letter of each word in uppercase. The instruction should be written right above the `Scenario` keyword. The possible values for it can be found down below. Keep in mind that it will only login and it will stop right after the login is done, on the expected page. + +Here's a list of possible instructions that can be used: + +| Instruction | Description | +| ---------------- | --------------------------------------| +| @loginAsEditor | Login as a staff editor user type | +| @loginAsViewer | Login as a staff viewer user type | +| @loginAsAdmin | Login as a staff admin user type | +| @loginAsBCeID | Login as a BCeID user type | +| @loginAsBCSC | Login as a BC Services Card user type | diff --git a/cypress/cypress.config.ts b/cypress/cypress.config.ts index f7d093e203..48cd453a58 100644 --- a/cypress/cypress.config.ts +++ b/cypress/cypress.config.ts @@ -48,7 +48,17 @@ async function setupNodeEvents( export default defineConfig({ e2e: { + reporter: require.resolve("@badeball/cypress-cucumber-preprocessor/pretty-reporter"), specPattern: "**/*.feature", setupNodeEvents, + defaultCommandTimeout: 10000, + pageLoadTimeout: 60000, + }, + includeShadowDom: true, + viewportHeight: 1080, + viewportWidth: 1920, + retries: { + runMode: 3, + openMode: 0, }, }); \ No newline at end of file diff --git a/cypress/cypress/e2e/bceid.feature b/cypress/cypress/e2e/bceid.feature new file mode 100644 index 0000000000..ffcb28915d --- /dev/null +++ b/cypress/cypress/e2e/bceid.feature @@ -0,0 +1,67 @@ +Feature: BceID User Tests + + Deals with BCeID Business user scenarios + + @loginAsBCeID + Scenario: BceID User Login + Given I am a "BceID" user + When I can read "New client application" + + @loginAsBCeID + Scenario: BceID Unregistered User + Given I am a "BceID" user + When I can read "New client application" + And I select "DMH - 100 Mile House Natural Resource District" from the "District" form input + Then I mark "I have an unregistered sole proprietorship" on the "Type of business" "radio" input + And I fill the form as follows + | Field name | Value | Type | + | Year | 1990 | text | + | Month | 01 | text | + | Day | 01 | text | + Then I click on next + And I type "2975 Jutland Rd" and select "2975 Jutland Rd" from the "Street address or PO box" form autocomplete + Then I click on next + And I type "2255522552" into the "Phone number" form input + Then I select "Billing" from the "Primary role" form input + And I add a new contact called "John Wick" + And I fill the "John Wick" information with the following + | Field name | Value | Type | + | Email address | jwick@thecontinental.ca | text | + | Phone number | 2501234568 | text | + | Primary role | Director | select | + And I click on next + Then I submit + And I can read "Application submitted!" + + @loginAsBCeID + Scenario: BceID Registered User + Given I am a "BceID" user + When I can read "New client application" + And I select "DMH - 100 Mile House Natural Resource District" from the "District" form input + Then I mark "I have a BC registered business (corporation, sole proprietorship, society, etc.)" on the "Type of business" "radio" input + And I type "veitch f" and select "VEITCH FOREST" from the "BC registered business name" form autocomplete + And I fill the form as follows + | Field name | Value | Type | + | Year | 1990 | text | + | Month | 01 | text | + | Day | 01 | text | + Then I click on next + And I click on next + And I type "2255522552" into the "Phone number" form input + Then I select "Billing" from the "Primary role" form input + And I fill the "GARY VEITCH" information with the following + | Field name | Value | Type | + | Email address | garyveitch@mail.ca | text | + | Phone number | 7787787778 | text | + | Primary role | Director | select | + And I click on next + Then I submit + And I can read "Application submitted!" + + @loginAsBCeID + Scenario: BceID Unregistered User already registered + Given I am a "BceID" user + When I can read "New client application" + And I select "DMH - 100 Mile House Natural Resource District" from the "District" form input + Then I mark "I have an unregistered sole proprietorship" on the "Type of business" "radio" input + And I should see the "error" message "Looks like “01-DEV, LOAD” has a client number. Select the 'Receive email and logout' button below to have it sent to you at maria.martinez@gov.bc.ca" on the "Business information" \ No newline at end of file diff --git a/cypress/cypress/e2e/bceid.ts b/cypress/cypress/e2e/bceid.ts new file mode 100644 index 0000000000..52d07c0fae --- /dev/null +++ b/cypress/cypress/e2e/bceid.ts @@ -0,0 +1,2 @@ +import { Then, Given, Step } from "@badeball/cypress-cucumber-preprocessor"; +//This file is left intentionally empty diff --git a/cypress/cypress/e2e/landing.feature b/cypress/cypress/e2e/landing.feature new file mode 100644 index 0000000000..cfd00316c6 --- /dev/null +++ b/cypress/cypress/e2e/landing.feature @@ -0,0 +1,27 @@ +Feature: Check application login + + This validate the possibility of login with any kind of provider + + Scenario: Try to log with BCeID + Given I visit "/landing" + Then I can read "Client Management System" + Then I cannot see "Log in with BCeID" + Then I can read "Log in with IDIR" + Then I visit "/landing?fd_to=&ref=external" + Then I can read "Log in with" + + Scenario: Try to log with BCSC + Given I visit "/landing" + Then I can read "Client Management System" + Then I cannot see "Log in with BC Services Card" + Then I can read "Log in with IDIR" + Then I visit "/landing?fd_to=&ref=individual" + Then I can read "Continue with" + Then I can read "BC Services Card app" + Then I can read "Test with username and password" + + Scenario: Try to log with IDIR + Given I visit "/landing" + Then I can read "Client Management System" + And I click on the "Log in with IDIR" button + Then I can read "Log in with IDIR" diff --git a/cypress/cypress/e2e/landing.ts b/cypress/cypress/e2e/landing.ts new file mode 100644 index 0000000000..3d99c031ec --- /dev/null +++ b/cypress/cypress/e2e/landing.ts @@ -0,0 +1,2 @@ +import { Then, Given } from "@badeball/cypress-cucumber-preprocessor"; +//This file is left intentionally empty \ No newline at end of file diff --git a/cypress/cypress/e2e/logouts.feature b/cypress/cypress/e2e/logouts.feature new file mode 100644 index 0000000000..79fdd40258 --- /dev/null +++ b/cypress/cypress/e2e/logouts.feature @@ -0,0 +1,9 @@ +Feature: BceID User Tests + + @loginAsBCeID + Scenario: BceID User Logout + Given I am a "BceID" user + When I can read "New client application" + Then I click on the "Logout" button + And I can read "Are you sure you want to logout? Your data will not be saved." + Then I click on the "Logout" button diff --git a/cypress/cypress/e2e/sample.feature b/cypress/cypress/e2e/sample.feature index 92760d2139..1f441cf0b8 100644 --- a/cypress/cypress/e2e/sample.feature +++ b/cypress/cypress/e2e/sample.feature @@ -3,5 +3,5 @@ Feature: Form screen loads correctly This is just a simple template file to show how to write and format your test Scenario: Screen loads - Given I am on the form page - Then I can see the title + Given I visit "/" + Then I can read "Client Management System" diff --git a/cypress/cypress/e2e/sample.ts b/cypress/cypress/e2e/sample.ts index b86902accb..bf60a1fc16 100644 --- a/cypress/cypress/e2e/sample.ts +++ b/cypress/cypress/e2e/sample.ts @@ -1,9 +1,2 @@ -import { Then, Given } from "@badeball/cypress-cucumber-preprocessor"; - -Given("I am on the form page", () => { - cy.visit('/'); -}); - -Then("I can see the title", () => { - cy.contains('Client Management System') -}); +import { Then, Given, Step } from "@badeball/cypress-cucumber-preprocessor"; +//This file is left intentionally empty \ No newline at end of file diff --git a/cypress/cypress/e2e/staffCreate.feature b/cypress/cypress/e2e/staffCreate.feature new file mode 100644 index 0000000000..2df58e6bc5 --- /dev/null +++ b/cypress/cypress/e2e/staffCreate.feature @@ -0,0 +1,95 @@ +Feature: Staff Screens + + This feature file is to test staff screen use cases + + @loginAsEditor + Scenario: Submit individuals + When I click on the "Create client" button + And I can read "Create client" + Then I select "Individual" from the "Client type" form input + And I fill the form as follows + | Field name | Value | Type | + | First name | James | text | + | Last name | Baxter | text | + | Year | 1990 | text | + | Month | 01 | text | + | Day | 01 | text | + | ID type | Canadian driver's licence | select | + | ID number | 4417845 | text | + Then I click on next + And I fill the "Primary location" address with the following + | Field name | Value | Type | + | Location name | Office | text | + | Email address | jamesbaxter@mail.ca | text | + | Primary phone number | 2501231568 | text | + | Secondary phone number | 2501233568 | text | + | Fax | 2501239568 | text | + | Street address or PO box | 1520 Blanshard St | autocomplete | + Then I click on next + And I fill the "Primary contact" information with the following + | Field name | Value | Type | + | Email address | baxter.james@mail.ca | text | + | Primary phone number | 2501237567 | text | + | Secondary phone number | 2501232567 | text | + | Fax | 2501444567 | text | + | Contact type | Billing | select | + | Location name | Office | multiselect | + Then I click on next + Then I submit + And I wait for the text "has been created!" to appear + + @loginAsEditor + Scenario: Editor can submit registered + When I click on the "Create client" button + And I can read "Create client" + Then I select "BC registered business" from the "Client type" form input + And I type "star dot star" and select "STAR DOT STAR VENTURES" from the "Client name" form autocomplete + Then I wait for the text "This information is from BC Registries" to appear + Then I click on next + And I fill the "Primary location" address with the following + | Field name | Value | Type | + | Street address or PO box | 1515 Blanshard | autocomplete | + | Email address | mail2@mail.ca | text | + | Primary phone number | 7780000001 | text | + | Secondary phone number | 7780000002 | text | + | Notes | This is a test | textbox | + And I add a new location called "Home" + And I fill the "Home" address with the following + | Field name | Value | Type | + | Street address or PO box | 1515 Blanshard | autocomplete | + Then I click on next + And I fill the "Primary contact" information with the following + | Field name | Value | Type | + | Email address | mail3@mail.ca | text | + | Primary phone number | 7780000003 | text | + | Contact type | Billing | select | + | Location name | Home | multiselect | + | Location name | Mailing address | multiselect | + And I fill the "MARCEL ST. AMANT" information with the following + | Field name | Value | Type | + | Email address | mail3@mail.ca | text | + | Primary phone number | 7780000004 | text | + | Contact type | Billing | select | + | Location name | Home | multiselect | + | Last name | ST AMANT | textreplace | + And I click on next + Then I submit + And I wait for the text "has been created!" to appear + + @loginAsEditor + Scenario: Already exists and has fuzzy match + When I click on the "Create client" button + And I can read "Create client" + Then I select "Individual" from the "Client type" form input + And I fill the form as follows + | Field name | Value | Type | + | First name | James | text | + | Last name | Baxter | text | + | Year | 1959 | text | + | Month | 05 | text | + | Day | 18 | text | + | ID type | Canadian driver's licence | select | + | ID number | 1234567 | text | + Then I click on next + And I should see the "warning" message "was found with similar name and birthdate" on the "top" + And The field "First name" should have the "warning" message "There's already a client with this name" \ No newline at end of file diff --git a/cypress/cypress/e2e/staffCreate.ts b/cypress/cypress/e2e/staffCreate.ts new file mode 100644 index 0000000000..0bd01d6e8f --- /dev/null +++ b/cypress/cypress/e2e/staffCreate.ts @@ -0,0 +1,2 @@ +import { Given, Then, When } from "@badeball/cypress-cucumber-preprocessor"; +//This file is left intentionally empty diff --git a/cypress/cypress/support/commands.ts b/cypress/cypress/support/commands.ts index 072e8c751d..92d6aa769f 100644 --- a/cypress/cypress/support/commands.ts +++ b/cypress/cypress/support/commands.ts @@ -1,71 +1,42 @@ /* eslint-disable no-undef */ /// - -const generateRandomHex = (length: number): string => { - const characters = '0123456789abcdef' - let result = '' - for (let i = 0; i < length; i++) { - const randomIndex = Math.floor(Math.random() * characters.length) - result += characters.charAt(randomIndex) - } - return result -} - -Cypress.Commands.add('addCookie', (name: string, value: string) => { - cy.setCookie(name, value, { - domain: 'localhost', - path: '/', - httpOnly: true, - secure: true, - expiry: Date.now() + 86400000 - }) -}) - -Cypress.Commands.add('expireCookie', (name: string) => { - cy.setCookie(name, '', { - domain: 'localhost', - path: '/', - httpOnly: true, - secure: true, - expiry: Date.now() - 86400000 * 2 - }) -}) - -Cypress.Commands.add('addToSessionStorage', (key: string, value: any) => { - cy.window().then((win) => { - win.sessionStorage.setItem(key, JSON.stringify(value)) - }) -}) - -Cypress.Commands.add('expireSessionStorage', (key: string) => { - cy.window().then((win) => { - win.sessionStorage.removeItem(key) - }) -}) - -Cypress.Commands.add('addToLocalStorage', (key: string, value: any) => { - cy.window().then((win) => { - win.localStorage.setItem(key, JSON.stringify(value)) - }) -}) - -Cypress.Commands.add('expireLocalStorage', (key: string) => { - cy.window().then((win) => { - win.localStorage.removeItem(key) - }) -}) - -Cypress.Commands.add('login', (email: string, name: string) => { - cy.get('.landing-button').should('be.visible') - cy.addToSessionStorage('user', { - name, - provider: 'idir', - userId: generateRandomHex(32), - email, - firstName: 'UAT', - lastName: 'Test' - }) - cy.reload() - cy.wait(1000) -}) \ No newline at end of file +Cypress.Commands.add("logout", () => { + cy.get("[data-id=logout-btn]").should("be.visible"); +}); + +Cypress.Commands.add("checkAutoCompleteErrorMessage", (field: string, message: string) => { + cy.get(field) + .should('have.attr', 'aria-invalid', 'true') + .should('have.attr', 'invalid-text', message); + + cy.get(field) + .shadow() + .find('svg').should('exist'); + + cy.get(field) + .shadow() + .find('div.cds--form__helper-text > slot#helper-text') + .invoke('text') + .should('contains', message); +}); + +Cypress.Commands.add("checkAccordionItemState", (additionalSelector: string, open: boolean) => { + cy.get(`cds-accordion-item${additionalSelector}`).should( + `${open ? "" : "not."}have.attr`, + "open", + ); +}); + +Cypress.Commands.add('waitForPageLoad', (element: string) => { + cy.get(element).should('be.visible').then(() => { + cy.log('Page loaded'); + }); +}); + +Cypress.Commands.add('logAndScreenshot', (message: string) => { + cy.log(message).then(() => { + console.log(message); + cy.screenshot(`log-${Date.now()}`); // Takes a screenshot with a timestamp + }); +}); diff --git a/cypress/cypress/support/cypress.d.ts b/cypress/cypress/support/cypress.d.ts index e1f89e1433..e6e3bb9e32 100644 --- a/cypress/cypress/support/cypress.d.ts +++ b/cypress/cypress/support/cypress.d.ts @@ -1,11 +1,9 @@ declare namespace Cypress { interface Chainable { - login(email: string, name: string): Chainable; - addCookie(name: string, value: string): Chainable; - addToLocalStorage(key: string, value: any): Chainable; - expireLocalStorage(key: string): Chainable; - addToSessionStorage(key: string, value: any): Chainable; - expireSessionStorage(key: string): Chainable; - expireCookie(name: string): Chainable; + logout(): Chainable; + checkAutoCompleteErrorMessage(field: string, message: string): Chainable; + checkAccordionItemState(additionalSelector: string, open: boolean): Chainable; + waitForPageLoad(element: string): Chainable; + logAndScreenshot(message: string): Chainable; } } \ No newline at end of file diff --git a/cypress/cypress/support/e2e.ts b/cypress/cypress/support/e2e.ts index 43c03b759d..de527d4507 100644 --- a/cypress/cypress/support/e2e.ts +++ b/cypress/cypress/support/e2e.ts @@ -1 +1,14 @@ import './commands' + +Cypress.on('window:before:load', (win) => { + // Listen to browser console logs and pass them to the Cypress console + const originalConsoleLog = win.console.log; + win.console.log = (...args) => { + originalConsoleLog(...args); + // Pass logs to Cypress terminal + Cypress.log({ + name: 'console.log', + message: [...args], + }); + }; +}); \ No newline at end of file diff --git a/cypress/cypress/support/step_definitions/area_input_steps.ts b/cypress/cypress/support/step_definitions/area_input_steps.ts new file mode 100644 index 0000000000..368f597a83 --- /dev/null +++ b/cypress/cypress/support/step_definitions/area_input_steps.ts @@ -0,0 +1,44 @@ +import { Then, Step, BeforeStep } from "@badeball/cypress-cucumber-preprocessor"; + +let idir = true; + +BeforeStep({ tags: "@loginAsBCeID or @loginAsBCSC" }, function () { + idir = false; +}); +BeforeStep({ tags: "@loginAsEditor or @loginAsViewer or @loginAsAdmin" }, function () { + idir = true; +}); + +/* Area Input Steps */ + +Then('I type {string} into the {string} form input area', (text: string, input: string) => { + cy.contains('div.cds-text-input-label span', input).then(($label) => { + cy.wrap($label.parent().parent().parent()) + .find('textarea[id*="input"]') + .type(text); + }); +}); + +Then('I clear the {string} form input area', (input: string) => { + cy.contains('div.cds-text-input-label span', input).then(($label) => { + cy.wrap($label.parent().parent().parent()) + .find('textarea[id*="input"]') + .clear(); + }); +}); + +Then( + 'I type {string} into the {string} form input area for the {string}', + (value: string, fieldLabel: string, sectionTitle: string) => { + + if (sectionTitle === 'Primary location' || sectionTitle === 'Primary contact') { + cy.get('div.frame-01:first').within(() => { + Step(this, `I type "${value}" into the "${fieldLabel}" form input area`); + }); + } else { + cy.get(`[data-text="${sectionTitle}"]`).within(() => { + Step(this, `I type "${value}" into the "${fieldLabel}" form input area`); + }); + } + +}); diff --git a/cypress/cypress/support/step_definitions/autocomplete_steps.ts b/cypress/cypress/support/step_definitions/autocomplete_steps.ts new file mode 100644 index 0000000000..948480ba1b --- /dev/null +++ b/cypress/cypress/support/step_definitions/autocomplete_steps.ts @@ -0,0 +1,62 @@ +import { Then, Step, BeforeStep } from "@badeball/cypress-cucumber-preprocessor"; + +let idir = true; + +BeforeStep({ tags: "@loginAsBCeID or @loginAsBCSC" }, function () { + idir = false; +}); +BeforeStep({ tags: "@loginAsEditor or @loginAsViewer or @loginAsAdmin" }, function () { + idir = true; +}); + +/* Autocomplete Input Steps */ + +Then('I type {string} and select {string} from the {string} form autocomplete', (search: string, value: string, input: string) => { + + if(input === 'Client name' || input === 'BC registered business name') { + cy.intercept('GET', `**/api/clients/**`).as('autocomplete'); + cy.intercept('GET', `**/api/opendata/**`).as('autocomplete'); + } else if(input === 'Street address or PO box') { + cy.intercept('GET', `**/api/address**`).as('autocomplete'); + } + + cy.contains('label', input).then(($label) => { + const parentShadow = $label[0].getRootNode(); + cy.wrap(parentShadow) + .find('input') + .type(search, { delay: 150 }) + .then(() => { + cy.wait('@autocomplete'); + cy.wrap(parentShadow) + .parent() + .find(`cds-combo-box-item[data-value="${value}"], cds-combo-box-item[data-value^="${value}"]`) + .first() + .then(($item) => { + if ($item.length) { + cy.wrap($item).click(); + } else { + throw new Error(`Item with value "${value}" not found.`); + } + }) + .then(() => { + cy.wait('@autocomplete'); + }); + }); + }); +}); + +Then( + 'I type {string} and select {string} from the {string} form autocomplete for the {string}', + (search: string, value: string, fieldLabel: string, sectionTitle: string) => { + + if (sectionTitle === 'Primary location' || sectionTitle === 'Primary contact') { + cy.get('div.frame-01:first').within(() => { + Step(this, `I type "${search}" and select "${value}" from the "${fieldLabel}" form autocomplete`); + }); + } else { + cy.get(`[data-text="${sectionTitle}"]`).within(() => { + Step(this, `I type "${search}" and select "${value}" from the "${fieldLabel}" form autocomplete`); + }); + } + +}); \ No newline at end of file diff --git a/cypress/cypress/support/step_definitions/button_steps.ts b/cypress/cypress/support/step_definitions/button_steps.ts new file mode 100644 index 0000000000..f8c9b4731f --- /dev/null +++ b/cypress/cypress/support/step_definitions/button_steps.ts @@ -0,0 +1,74 @@ +import { When, BeforeStep } from "@badeball/cypress-cucumber-preprocessor"; + +let idir = true; + +BeforeStep({ tags: "@loginAsBCeID or @loginAsBCSC" }, function () { + idir = false; +}); +BeforeStep({ tags: "@loginAsEditor or @loginAsViewer or @loginAsAdmin" }, function () { + idir = true; +}); + +/* Button Step */ + +When('I click on the {string} button', (name: string) => { + buttonClick(name, ['input', 'button', 'cds-button', 'cds-modal-footer-button', 'cds-side-nav-link']); +}); + +When('I click on next', () => { + + if (!idir) { + cy.get('cds-button[data-text="Next"]').click().then(() => {cy.wait(15);}); + } else { + cy.intercept('POST', `**/api/clients/matches`).as('matches'); + cy.get('cds-button[data-text="Next"]').click().then(() => {cy.wait('@matches',{ timeout: 10 * 1000 });}); + } + +}); + +When('I submit', () => { + if(idir){ + cy.intercept('POST', `**/api/clients/submissions/staff`).as('submit'); + } else { + cy.intercept('POST', `**/api/clients/submissions`).as('submit'); + } + cy.get('cds-button[data-text="Submit"]').scrollIntoView().click().then(() => {cy.wait('@submit',{ timeout: 60 * 1000 });}); +}); + + +const buttonClick = ( + name: string, + kinds: string[], + waitForIntercept: string = null, + waitForTime : number = 15, + selector: string = 'body' +) => { + if (kinds.length === 0) { + throw new Error(`Button with label "${name}" not found.`); + } + + // Build a selector string that matches any of the button kinds + const kindSelector = kinds.join(','); + + cy.get(selector) + .find(kindSelector) + .filter(':visible') // Only consider visible buttons + .filter((index, element) => { + // Check for the button label in various places + return Cypress.$(element).attr('data-text')?.includes(name) || + Cypress.$(element).html().includes(name) || + Cypress.$(element).text().includes(name) || + Cypress.$(element).val()?.toString().includes(name); + }) + .first() // Get the first matching, visible button + .should('be.visible') // Ensure it's visible before clicking + .click() // Click the button + .then(() => { + // Handle waiting for intercept or time after clicking + if (waitForIntercept) { + cy.wait(`@${waitForIntercept}`, { timeout: waitForTime * 1000 }); + } else if (waitForTime) { + cy.wait(waitForTime); + } + }); +} diff --git a/cypress/cypress/support/step_definitions/commons.ts b/cypress/cypress/support/step_definitions/commons.ts new file mode 100644 index 0000000000..831de3d2d9 --- /dev/null +++ b/cypress/cypress/support/step_definitions/commons.ts @@ -0,0 +1,74 @@ +import { Then, Step, BeforeStep } from "@badeball/cypress-cucumber-preprocessor"; + +let idir = true; + +BeforeStep({ tags: "@loginAsBCeID or @loginAsBCSC" }, function () { + idir = false; +}); +BeforeStep({ tags: "@loginAsEditor or @loginAsViewer or @loginAsAdmin" }, function () { + idir = true; +}); + +/* Extra Actions */ + +Then('I add a new location called {string}', (location: string) => { + Step(this,'I click on the "Add another location" button'); + cy.get(`[data-text="Additional location"]`).should('be.visible'); + Step(this,`I type "${location}" into the "Location name" form input for the "Additional location"`); + cy.get(`[data-text="${location}"]`).should('be.visible'); +}); + +Then('I add a new contact called {string}', (contactName: string) => { + Step(this,'I click on the "Add another contact" button'); + + cy.get('[data-text="Additional contact"]').should('be.visible'); + const [firstName, ...lastName] = contactName.split(' '); + Step(this,`I type "${firstName}" into the "First name" form input for the "Additional contact"`); + cy.get(`[data-text="${firstName} "]`).should('be.visible'); + Step(this,`I type "${lastName.join(' ')}" into the "Last name" form input for the "${firstName} "`); + +}); + +/* Error messages */ + +Then('I should see the {string} message {string} on the {string}', (kind: string, message: string, location: string) => { + checkForActionableNotification(message, location, kind); +}); + +Then( + 'The field {string} should have the {string} message {string}', + (field: string, kind: string, message: string) => { + + cy.contains('label', field).then(($label) => { + console.log('Shadow root: ', $label[0].getRootNode()); + if(kind === 'error') { + cy.wrap($label[0].getRootNode()) + .find('#invalid-text') + .invoke('text') + .should('contains', message); + } else { + cy.wrap($label[0].getRootNode()) + .find('.cds--form-requirement') + .invoke('text') + .should('contains', message); + } + }); + +}); + +/* This block is dedicated to the actual code */ + +const checkForActionableNotification = (message: string, location: string, kind: string) => { + let errorLookupTag = ''; + if(location.toLowerCase().includes("top")){ + errorLookupTag = `cds-actionable-notification[id="fuzzy-match-notification-global"][kind="${kind}"] div span.body-compact-01`; + } else if(idir) { + errorLookupTag = `cds-inline-notification[data-text="${location}"][kind="${kind}"] div span.body-compact-01`; + } else { + errorLookupTag = `cds-inline-notification[data-text="${location}"][kind="${kind}"]`; + } + + cy.get(errorLookupTag) + .should('exist') + .should('contain.text', message); +} diff --git a/cypress/cypress/support/step_definitions/general_steps.ts b/cypress/cypress/support/step_definitions/general_steps.ts new file mode 100644 index 0000000000..d6d374db8e --- /dev/null +++ b/cypress/cypress/support/step_definitions/general_steps.ts @@ -0,0 +1,35 @@ +import { Given, Then, BeforeStep } from "@badeball/cypress-cucumber-preprocessor"; +BeforeStep(() => { cy.wait(10); }); + +Given('I visit {string}', (url: string) => { + cy.visit(url).then(() => { + cy.window().then((win) => { + return new Cypress.Promise((resolve) => { + if (win.document.readyState === 'complete') { + resolve(); + } else { + win.addEventListener('load', resolve); + } + }); + }); + }); +}); + +Then('I can read {string}', (title: string) => { + cy.contains(title).should('be.visible'); +}); + +Then('I cannot see {string}', (button: string) => { + cy.contains(button).should('not.exist'); +}); + +Then('I wait for the text {string} to appear', (text: string) => { + cy.contains(text).should('be.visible'); +}); + +Then('I wait for the text {string} to appear after {string}', (text: string,waitFor: string) => { + cy.wait(`@${waitFor}`,{ timeout: 10 * 1000 }); + cy.contains(text).should('be.visible'); +}); + +Then('I am a {string} user', (userType: string) => {}); diff --git a/cypress/cypress/support/step_definitions/loginsHooks.ts b/cypress/cypress/support/step_definitions/loginsHooks.ts new file mode 100644 index 0000000000..d51151be8d --- /dev/null +++ b/cypress/cypress/support/step_definitions/loginsHooks.ts @@ -0,0 +1,71 @@ +import { Before, Step } from '@badeball/cypress-cucumber-preprocessor'; + +const doLogin = (kind: string, afterLoginLocation: string, extraLandingParam: string = null) => { + + const username = Cypress.env(`${kind}_username`); + const password = Cypress.env(`${kind}_password`); + + if(!username || !password) { + throw new Error(`Username or password for ${kind} not found.`); + } + + cy.session( + `${kind}-${username}`, + () => { + const landingPage = extraLandingParam ? `/landing?${extraLandingParam}` : '/landing'; + // Visit the landing page + Step(this, `I visit "${landingPage}"`); + + // Click on the login button + if(kind !== 'bceid' && kind !== 'bcsc') { + cy.waitForPageLoad('img'); + Step(this, 'I click on the "Log in with IDIR" button'); + } else if(kind === 'bceid') { + cy.waitForPageLoad('span#bceidLogo'); + } + + // Log into the application, not using a step here to prevent password spillage + cy.get("#user").type(username, { log: false }); + cy.get("#password").type(password, { log: false }); + Step(this, 'I click on the "Continue" button'); + + // Validate the login for session purposes + cy.url().should('include', afterLoginLocation); + cy.getCookies().then((cookies) => { + cookies.forEach((cookie) => cy.setCookie(cookie.name, cookie.value)); + }); + }, + { + validate: () => { + cy.request(afterLoginLocation).its('status').should('eq', 200); + cy.visit(afterLoginLocation); + }, + }); + cy.visit(afterLoginLocation); + +} + +Before({ tags: '@loginAsEditor' }, () => { + doLogin('editor','/submissions'); + cy.waitForPageLoad('cds-header'); +}); + +Before({ tags: '@loginAsAdmin' }, () => { + doLogin('admin','/submissions'); + cy.waitForPageLoad('cds-header'); +}); + +Before({ tags: '@loginAsViewer' }, () => { + doLogin('viewer','/submissions'); + cy.waitForPageLoad('cds-header'); +}); + +Before({ tags: '@loginAsBCeID' }, () => { + doLogin('bceid','/new-client','ref=external'); + cy.waitForPageLoad('cds-header'); +}); + +Before({ tags: '@loginAsBCSC' }, () => { + doLogin('bcsc','/new-client-bcsc','ref=individual'); + cy.waitForPageLoad('cds-header'); +}); \ No newline at end of file diff --git a/cypress/cypress/support/step_definitions/radio_check_steps.ts b/cypress/cypress/support/step_definitions/radio_check_steps.ts new file mode 100644 index 0000000000..70a65a02c7 --- /dev/null +++ b/cypress/cypress/support/step_definitions/radio_check_steps.ts @@ -0,0 +1,18 @@ +import { Then, BeforeStep } from "@badeball/cypress-cucumber-preprocessor"; + +let idir = true; + +BeforeStep({ tags: "@loginAsBCeID or @loginAsBCSC" }, function () { + idir = false; +}); +BeforeStep({ tags: "@loginAsEditor or @loginAsViewer or @loginAsAdmin" }, function () { + idir = true; +}); + +/* Radio and Check Input Steps */ + +Then('I mark {string} on the {string} {string} input', (value: string, input: string, kind: string) => { + cy.get(`cds-radio-button-group[legend-text="${input}"]`) + .find(`cds-radio-button[label-text="${value}"]`) + .click(); +}); diff --git a/cypress/cypress/support/step_definitions/select_input_steps.ts b/cypress/cypress/support/step_definitions/select_input_steps.ts new file mode 100644 index 0000000000..0af4f0b5f8 --- /dev/null +++ b/cypress/cypress/support/step_definitions/select_input_steps.ts @@ -0,0 +1,60 @@ +import { Then, Step, BeforeStep } from "@badeball/cypress-cucumber-preprocessor"; + +let idir = true; + +BeforeStep({ tags: "@loginAsBCeID or @loginAsBCSC" }, function () { + idir = false; +}); +BeforeStep({ tags: "@loginAsEditor or @loginAsViewer or @loginAsAdmin" }, function () { + idir = true; +}); + +/* Select Input Steps */ + +Then('I select {string} from the {string} form input', (value: string, input: string) => { + cy.contains('label', input).then(($label) => { + const parentShadow = $label[0].getRootNode(); + cy.wrap(parentShadow).find("[part='trigger-button']").click(); + cy.wrap(parentShadow).parent().find(`cds-combo-box-item[data-value="${value}"]`).click(); + }); +}); + +Then( + 'I select {string} from the {string} form input for the {string}', + (value: string, fieldLabel: string, sectionTitle: string) => { + + if (sectionTitle === 'Primary location' || sectionTitle === 'Primary contact') { + cy.get('div.frame-01:first').within(() => { + Step(this, `I select "${value}" from the "${fieldLabel}" form input`); + }); + } else { + cy.get(`[data-text="${sectionTitle}"]`).within(() => { + Step(this, `I select "${value}" from the "${fieldLabel}" form input`); + }); + } + +}); + +Then('I select {string} from the {string} multiselect', (value: string, input: string) => { + cy.contains('label', input).then(($label) => { + const parentShadow = $label[0].getRootNode(); + cy.wrap(parentShadow).find("[part='trigger-button']").click(); + cy.wrap(parentShadow).parent().find(`cds-multi-select-item[data-value="${value}"]`).click(); + }); +}); + +Then( + 'I select {string} from the {string} multiselect for the {string}', + (value: string, fieldLabel: string, sectionTitle: string) => { + + if (sectionTitle === 'Primary location' || sectionTitle === 'Primary contact') { + cy.get('div.frame-01:first').within(() => { + Step(this, `I select "${value}" from the "${fieldLabel}" multiselect`); + }); + } else { + cy.get(`[data-text="${sectionTitle}"]`).within(() => { + Step(this, `I select "${value}" from the "${fieldLabel}" multiselect`); + }); + } + +}); diff --git a/cypress/cypress/support/step_definitions/tables_steps.ts b/cypress/cypress/support/step_definitions/tables_steps.ts new file mode 100644 index 0000000000..8ed5fec90f --- /dev/null +++ b/cypress/cypress/support/step_definitions/tables_steps.ts @@ -0,0 +1,63 @@ +import { Then, DataTable, Step, BeforeStep } from "@badeball/cypress-cucumber-preprocessor"; + +let idir = true; + +BeforeStep({ tags: "@loginAsBCeID or @loginAsBCSC" }, function () { + idir = false; +}); +BeforeStep({ tags: "@loginAsEditor or @loginAsViewer or @loginAsAdmin" }, function () { + idir = true; +}); + +/* Data tables */ + +Then('I fill the form as follows', (table: DataTable) => { + table.rows().forEach(row => { + const [fieldName, value, kind] = row; + if (kind === 'text') { + Step(this, `I type "${value}" into the "${fieldName.trim()}" form input`); + }else if (kind === 'textreplace') { + Step(this, `I replace the "${fieldName.trim()}" with "${value}" form input`); + } else if (kind === 'select') { + Step(this, `I select "${value}" from the "${fieldName.trim()}" form input`); + } else if (kind === 'autocomplete') { + Step(this, `I type "${value}" and select "${value}" from the "${fieldName.trim()}" form autocomplete`); + } else if (kind === 'multiselect') { + Step(this, `I select "${value}" from the "${fieldName.trim()}" multiselect`); + } + }); +}); + +Then('I fill the {string} address with the following', (location: string, table: DataTable) => { + table.rows().forEach(row => { + const [fieldName, value, kind] = row; + if (kind === 'text') { + Step(this, `I type "${value}" into the "${fieldName.trim()}" form input for the "${location}"`); + } else if (kind === 'textreplace') { + Step(this, `I replace the "${fieldName.trim()}" with "${value}" form input for the "${location}"`); + } else if (kind === 'select') { + Step(this, `I select "${value}" from the "${fieldName.trim()}" form input for the "${location}"`); + } else if (kind === 'autocomplete') { + Step(this, `I type "${value}" and select "${value}" from the "${fieldName.trim()}" form autocomplete for the "${location}"`); + } else if (kind === 'multiselect') { + Step(this, `I select "${value}" from the "${fieldName.trim()}" multiselect`); + } + }); +}); + +Then('I fill the {string} information with the following', (contactName: string, table: DataTable) => { + table.rows().forEach(row => { + const [fieldName, value, kind] = row; + if (kind === 'text') { + Step(this, `I type "${value}" into the "${fieldName.trim()}" form input for the "${contactName.trim()}"`); + } else if (kind === 'textreplace') { + Step(this, `I replace the "${fieldName.trim()}" with "${value}" form input for the "${contactName}"`); + } else if (kind === 'select') { + Step(this, `I select "${value}" from the "${fieldName.trim()}" form input for the "${contactName.trim()}"`); + } else if (kind === 'autocomplete') { + Step(this, `I type "${value}" and select "${value}" from the "${fieldName.trim()}" form autocomplete for the "${contactName.trim()}"`); + } else if (kind === 'multiselect') { + Step(this, `I select "${value}" from the "${fieldName.trim()}" multiselect for the "${contactName.trim()}"`); + } + }); +}); \ No newline at end of file diff --git a/cypress/cypress/support/step_definitions/text_input_steps.ts b/cypress/cypress/support/step_definitions/text_input_steps.ts new file mode 100644 index 0000000000..50d38e6a0a --- /dev/null +++ b/cypress/cypress/support/step_definitions/text_input_steps.ts @@ -0,0 +1,77 @@ +import { Then, Step, BeforeStep } from "@badeball/cypress-cucumber-preprocessor"; + +let idir = true; + +BeforeStep({ tags: "@loginAsBCeID or @loginAsBCSC" }, function () { + idir = false; +}); +BeforeStep({ tags: "@loginAsEditor or @loginAsViewer or @loginAsAdmin" }, function () { + idir = true; +}); + +/* Text Input Steps */ + +Then('I type {string} into the {string} form input', (text: string, input: string) => { + cy.contains('label', input).then(($label) => { + cy.wrap($label.parent().parent()) + .find('input[id*="input"]') + .then(($input) => { + cy.wrap($input) + .type(text) + .should('have.value', text) + .focus(); + }); + }); +}); + +Then('I clear the {string} form input', (input: string) => { + cy.contains('label', input).then(($label) => { + cy.wrap($label.parent().parent()) + .find('input[id*="input"]') + .clear(); + }); +}); + +Then( + 'I type {string} into the {string} form input for the {string}', + (value: string, fieldLabel: string, sectionTitle: string) => { + + if (sectionTitle === 'Primary location' || sectionTitle === 'Primary contact') { + cy.get('div.frame-01:first').within(() => { + Step(this, `I type "${value}" into the "${fieldLabel}" form input`); + }); + } else { + cy.get(`[data-text="${sectionTitle}"]`).within(() => { + Step(this, `I type "${value}" into the "${fieldLabel}" form input`); + }); + } +}); + +Then('I replace the {string} with {string} form input', (input: string, text: string) => { + cy.contains('label', input).then(($label) => { + cy.wrap($label.parent().parent()) + .find('input[id*="input"]') + .then(($input) => { + cy.wrap($input) + .clear() + .type(text) + .should('have.value', text) + .focus(); + }); + }); +}); + +Then( + 'I replace the {string} with {string} form input for the {string}', + (fieldLabel: string, value: string, sectionTitle: string) => { + + if (sectionTitle === 'Primary location' || sectionTitle === 'Primary contact') { + cy.get('div.frame-01:first').within(() => { + Step(this, `I replace the "${fieldLabel}" with "${value}" form input`); + }); + } else { + cy.get(`[data-text="${sectionTitle}"]`).within(() => { + Step(this, `I replace the "${fieldLabel}" with "${value}" form input`); + }); + } +}); diff --git a/cypress/cypress/support/step_definitions/ui_ux_steps.ts b/cypress/cypress/support/step_definitions/ui_ux_steps.ts new file mode 100644 index 0000000000..6655d32230 --- /dev/null +++ b/cypress/cypress/support/step_definitions/ui_ux_steps.ts @@ -0,0 +1,97 @@ +import { Then, Step, BeforeStep } from "@badeball/cypress-cucumber-preprocessor"; + +let idir = true; + +BeforeStep({ tags: "@loginAsBCeID or @loginAsBCSC" }, function () { + idir = false; +}); +BeforeStep({ tags: "@loginAsEditor or @loginAsViewer or @loginAsAdmin" }, function () { + idir = true; +}); + +/* UX and UI checks */ + +Then('The {string} component is using the font {string}', (component: string, font: string) => { + cy.contains('label', component).should('exist').then(($label) => { + const parentShadow = $label[0].getRootNode() as ShadowRoot; + + if (parentShadow.host) { + const parentComponent = parentShadow.host as HTMLElement; // Host HTMLElement + cy.wrap(parentComponent).then((element) => { + checkComputedStyle(element, 'font-family', font); // Reuse the common function + }); + } else { + throw new Error('No host element found for the given shadow root.'); + } + }); +}); + +Then('The {string} component is using the font {string} for the {string}', (component: string, font: string, scope: string) => { + + if (scope === 'Primary location' || scope === 'Primary contact') { + cy.get('div.frame-01:first').within(() => { + Step(this, `The "${component}" component is using the font "${font}"`); + }); + } else if(idir){ + cy.get('cds-accordion cds-accordion-item') + .shadow() + .contains('div', scope).parent().parent().within(() => { + Step(this, `The "${component}" component is using the font "${font}"`); + }); + } else { + cy.get(`div.frame-01[data-text="${scope}"]`).within(() => { + Step(this, `The "${component}" component is using the font "${font}"`); + }); + } +}); + +Then('The {string} has weight {string}',(text: string, weight: string) => { + checkForCssProperty(text, 'font-weight', weight); +}); + +Then('The {string} has weight {string} inside the {string}',(text: string, weight: string, scope: string) => { + let computedScope = scope === 'form' ? 'div[role="main"]' : scope; + computedScope = scope === 'top' ? 'div[role="header"]' : computedScope; + computedScope = scope === 'bottom' ? 'div[role="footer"]' : computedScope; + checkForCssProperty(text, 'font-weight', weight, computedScope); +}); + +Then('The {string} size is {string}',(text: string, size: string) => { + checkForCssProperty(text, 'font-size', size); +}); + +Then('The {string} size is {string} inside the {string}',(text: string, size: string,scope: string) => { + let computedScope = scope === 'form' ? 'div[role="main"]' : scope; + computedScope = scope === 'top' ? 'div[role="header"]' : computedScope; + computedScope = scope === 'bottom' ? 'div[role="footer"]' : computedScope; + checkForCssProperty(text, 'font-size', size, computedScope); +}); + +// Helper functions to check computed CSS properties + +const checkComputedStyle = ( + element: JQuery, + property: string, + expectedValue: string +) => { + cy.window().then((win) => { + const computedStyle = win.getComputedStyle(element[0]); // Get the native DOM element + const styleValue = computedStyle.getPropertyValue(property); // Get the computed CSS property + // Assert that the style matches the expected value + expect(styleValue.trim().toLowerCase()).to.match(new RegExp(`^${expectedValue.toLowerCase()}`)); + }); +}; + +const checkForCssProperty = (text: string, property: string, value: string, scope: string = 'body') => { + cy.get(scope).find('*').each(($el) => { + cy.wrap($el).then((el) => { + const elementText = el.contents().filter(function () { + return this.nodeType === 3; + }).text().trim(); + + if (elementText === text) { + checkComputedStyle(el, property, value); // Reuse the common function + } + }); + }); +}; diff --git a/frontend/cypress/e2e/pages/staffform/BcRegisteredClientInformationWizardStep.cy.ts b/frontend/cypress/e2e/pages/staffform/BcRegisteredClientInformationWizardStep.cy.ts index 7d74e0e201..41bbe00300 100644 --- a/frontend/cypress/e2e/pages/staffform/BcRegisteredClientInformationWizardStep.cy.ts +++ b/frontend/cypress/e2e/pages/staffform/BcRegisteredClientInformationWizardStep.cy.ts @@ -1,7 +1,7 @@ import testCases from "../../../fixtures/staff/bcregisteredscenarios.json"; /* eslint-disable no-undef */ -describe("Bc Registered Staff Wizard Step", () => { +describe("BC Registered Staff Wizard Step", () => { beforeEach(() => { cy.viewport(1920, 1080); @@ -445,21 +445,21 @@ describe("Bc Registered Staff Wizard Step", () => { } if (scenario.showData) { - const success = Object.entries(scenario) - .filter( - ([key, value]) => - key.startsWith("show") && key.endsWith("Notification") - ) - .map(([key, value]) => value) - .every((value) => value === false); + /* + This variable might be useful in the future to test the button Next gets enabled on + success. But we'll probably need to fix FSADT1-1496 first. + */ + // const success = Object.entries(scenario) + // .filter( + // ([key, value]) => + // key.startsWith("show") && key.endsWith("Notification") + // ) + // .map(([key, value]) => value) + // .every((value) => value === false); cy.get( ".read-only-box > cds-inline-notification#readOnlyNotification" - ).should( - success || scenario.showDuplicatedNotification - ? "exist" - : "not.exist" - ); + ).should("exist"); cy.get(`.read-only-box > #legalType > .title-group-01 > .label-01`) .should("exist") @@ -629,6 +629,156 @@ describe("Bc Registered Staff Wizard Step", () => { }); }); + // See FSADT1-1511 (https://apps.nrs.gov.bc.ca/int/jira/browse/FSADT1-1511) + describe("when the selected Client name is replaced from a Sole proprietorship to something else", () => { + it("clears the Doing Business As field", () => { + loginAndNavigateToStaffForm(); + + cy.get("cds-inline-notification#bcRegistrySearchNotification").should( + "exist" + ); + + const sppSearch = "spp"; + const sppCode = "FM123123"; + + interceptClientsApi(sppSearch, sppCode); + + cy.selectAutocompleteEntry( + "#businessName", + sppSearch, + sppCode, + `@clientSearch${sppSearch}` + ); + + cy.get(`.read-only-box > #legalType`) + .should("exist") + .and("contains.text", "Sole Proprietorship"); + + cy.clearFormEntry("#businessName"); + + const cmpSearch = "cmp"; + const cmpCode = "C1231231"; + + interceptClientsApi(cmpSearch, cmpCode); + + cy.selectAutocompleteEntry( + "#businessName", + cmpSearch, + cmpCode, + `@clientSearch${cmpSearch}` + ); + + cy.get(`.read-only-box > #legalType`) + .should("exist") + .and("contains.text", "Continued In Corporation"); + + /* + Doing Business As is cleared, instead of holding the name of the previously selected Sole + proprietorship. + */ + cy.get("#doingBusinessAs").should("exist").and("have.value", ""); + }); + }); + + describe("when the selected Client name is a Sole proprietorship", () => { + beforeEach(() => { + loginAndNavigateToStaffForm(); + + cy.get("cds-inline-notification#bcRegistrySearchNotification").should("exist"); + + const sppSearch = "spp"; + const sppCode = "FM123123"; + + interceptClientsApi(sppSearch, sppCode); + + cy.selectAutocompleteEntry("#businessName", sppSearch, sppCode, `@clientSearch${sppSearch}`); + + cy.get(".read-only-box > #legalType") + .should("exist") + .and("contains.text", "Sole Proprietorship"); + }); + it("should not enable the button Next while Date of birth is empty", () => { + cy.get("[data-test='wizard-next-button']").find("button").should("be.disabled"); + }); + it("should enable the button Next when Date of birth is filled in", () => { + cy.fillFormEntry("#birthdateYear", "2001"); + cy.fillFormEntry("#birthdateMonth", "10"); + cy.fillFormEntry("#birthdateDay", "25"); + cy.get("[data-test='wizard-next-button']").find("button").should("be.enabled"); + }); + describe("and there is an error on the Date of birth", () => { + beforeEach(() => { + cy.get("#birthdateYear").find("input").focus().blur(); + + cy.contains("#birthdate + .field-error", "Date of birth must include a year"); + }); + it("enables the button Next when a new Client name from a different type gets selected", () => { + cy.clearFormEntry("#businessName"); + + const cmpSearch = "cmp"; + const cmpCode = "C1231231"; + + interceptClientsApi(cmpSearch, cmpCode); + + cy.selectAutocompleteEntry( + "#businessName", + cmpSearch, + cmpCode, + `@clientSearch${cmpSearch}`, + ); + + cy.get(".read-only-box > #legalType") + .should("exist") + .and("contains.text", "Continued In Corporation"); + + cy.get("[data-test='wizard-next-button']").find("button").should("be.enabled"); + }); + }); + }); + + describe("when the selected Client name is not a Sole proprietorship and there is an error on the Doing business as", () => { + beforeEach(() => { + loginAndNavigateToStaffForm(); + + cy.get("cds-inline-notification#bcRegistrySearchNotification").should("exist"); + + const cmpSearch = "cmp"; + const cmpCode = "C1231231"; + + interceptClientsApi(cmpSearch, cmpCode); + + cy.selectAutocompleteEntry("#businessName", cmpSearch, cmpCode, `@clientSearch${cmpSearch}`); + + cy.get(".read-only-box > #legalType") + .should("exist") + .and("contains.text", "Continued In Corporation"); + + cy.fillFormEntry("#doingBusinessAs", "Enchanté"); + + cy.checkInputErrorMessage("#doingBusinessAs", "The doing business as can only contain"); + }); + it("enables the button Next when a new Client name with type Sole proprietorship gets selected", () => { + cy.clearFormEntry("#businessName"); + + const sppSearch = "spp"; + const sppCode = "FM123123"; + + interceptClientsApi(sppSearch, sppCode); + + cy.selectAutocompleteEntry("#businessName", sppSearch, sppCode, `@clientSearch${sppSearch}`); + + cy.get(".read-only-box > #legalType") + .should("exist") + .and("contains.text", "Sole Proprietorship"); + + cy.fillFormEntry("#birthdateYear", "2001"); + cy.fillFormEntry("#birthdateMonth", "10"); + cy.fillFormEntry("#birthdateDay", "23"); + + cy.get("[data-test='wizard-next-button']").find("button").should("be.enabled"); + }); + }); + const loginAndNavigateToStaffForm = () => { cy.visit("/"); diff --git a/frontend/cypress/fixtures/clients/bcreg_FM123123.json b/frontend/cypress/fixtures/clients/bcreg_FM123123.json index a01f7fe490..a80ef08f2c 100644 --- a/frontend/cypress/fixtures/clients/bcreg_FM123123.json +++ b/frontend/cypress/fixtures/clients/bcreg_FM123123.json @@ -1,5 +1,5 @@ { - "name": "Sole proprietorship 1", + "name": "Sole Proprietorship 1", "id": "FM123123", "goodStanding": true, "addresses": [ @@ -31,4 +31,4 @@ } ], "isOwnedByPerson": true -} \ No newline at end of file +} diff --git a/frontend/cypress/fixtures/clients/bcreg_FM123456.json b/frontend/cypress/fixtures/clients/bcreg_FM123456.json index a01f7fe490..a80ef08f2c 100644 --- a/frontend/cypress/fixtures/clients/bcreg_FM123456.json +++ b/frontend/cypress/fixtures/clients/bcreg_FM123456.json @@ -1,5 +1,5 @@ { - "name": "Sole proprietorship 1", + "name": "Sole Proprietorship 1", "id": "FM123123", "goodStanding": true, "addresses": [ @@ -31,4 +31,4 @@ } ], "isOwnedByPerson": true -} \ No newline at end of file +} diff --git a/frontend/cypress/fixtures/staff/bcregisteredscenarios.json b/frontend/cypress/fixtures/staff/bcregisteredscenarios.json index bf5b9a1ef9..3c4b31ae52 100644 --- a/frontend/cypress/fixtures/staff/bcregisteredscenarios.json +++ b/frontend/cypress/fixtures/staff/bcregisteredscenarios.json @@ -10,7 +10,7 @@ "showBcRegDownNotification": false, "showDuplicatedNotification": true, "showNotOwnedByPersonError": false, - "type": "Corporation", + "type": "Continued In Corporation", "standing": "Unknown", "dba": "" }, @@ -25,7 +25,7 @@ "showBcRegDownNotification": false, "showDuplicatedNotification": false, "showNotOwnedByPersonError": false, - "type": "Sole proprietorship", + "type": "Sole Proprietorship", "standing": "Good standing", "dba": "Soleprop" }, @@ -40,7 +40,7 @@ "showBcRegDownNotification": true, "showDuplicatedNotification": false, "showNotOwnedByPersonError": false, - "type": "Corporation", + "type": "Continued In Corporation", "standing": "Good Standing", "dba": "" }, @@ -53,12 +53,12 @@ "showUnknowNotification": false, "showNotGoodStandingNotification": true, "showBcRegDownNotification": false, - "type": "Corporation", + "type": "Continued In Corporation", "standing": "Not in good standing", "dba": "" }, { - "scenarioName": "OK State - Corporation", + "scenarioName": "OK State - Continued In Corporation", "companySearch": "cmp", "companyCode": "C1231231", "showData": true, @@ -68,12 +68,12 @@ "showBcRegDownNotification": false, "showDuplicatedNotification": false, "showNotOwnedByPersonError": false, - "type": "Corporation", + "type": "Continued In Corporation", "standing": "Good standing", "dba": "" }, { - "scenarioName": "OK State - Corporation not found", + "scenarioName": "OK State - Continued In Corporation not found", "companySearch": "cnf", "companyCode": "C9999999", "showData": true, @@ -83,7 +83,7 @@ "showBcRegDownNotification": false, "showDuplicatedNotification": false, "showNotOwnedByPersonError": false, - "type": "Corporation", + "type": "Continued In Corporation", "standing": "Unknown", "dba": "" }, @@ -111,7 +111,7 @@ "showBcRegDownNotification": false, "showDuplicatedNotification": false, "showNotOwnedByPersonError": false, - "type": "Corporation", + "type": "Continued In Corporation", "standing": "Unknown", "dba": "" }, @@ -126,8 +126,8 @@ "showBcRegDownNotification": false, "showDuplicatedNotification": false, "showNotOwnedByPersonError": true, - "type": "Sole proprietorship", + "type": "Sole Proprietorship", "standing": "Good standing", "dba": "Soleprop" } -] \ No newline at end of file +] diff --git a/frontend/cypress/support/cypress.d.ts b/frontend/cypress/support/cypress.d.ts index 143efb7074..f4fb0c0c1a 100644 --- a/frontend/cypress/support/cypress.d.ts +++ b/frontend/cypress/support/cypress.d.ts @@ -12,8 +12,8 @@ declare namespace Cypress { fillFormEntry(field: string, value: string, delayMS: number = 10, area: boolean = false): Chainable; clearFormEntry(field: string, area: boolean = false): Chainable; selectFormEntry(field: string, value: string, box: boolean): Chainable; - checkFormEntry(field: string): Chainable; - uncheckFormEntry(field: string): Chainable; + markCheckbox(field: string): Chainable; + unmarkCheckbox(field: string): Chainable; selectAutocompleteEntry(field: string, value: string, dataid: string, delayTarget: string =''): Chainable; checkInputErrorMessage(field: string, message: string): Chainable; checkAutoCompleteErrorMessage(field: string, message: string): Chainable; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 98a19a144b..c023118c2a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,25 +12,25 @@ "@bcgov-nr/nr-fsa-theme": "^1.1.3", "@bcgov/bc-sans": "^2.0.0", "@carbon/icons-vue": "^10.92.0", - "@carbon/pictograms": "^12.40.0", - "@carbon/styles": "^1.65.0", + "@carbon/pictograms": "^12.41.0", + "@carbon/styles": "^1.66.0", "@carbon/web-components": "^2.12.0", "@vueuse/core": "^11.1.0", "aws-amplify": "^6.3.5", "axios": "^1.7.2", "date-fns": "^4.0.0", "dotenv": "^16.0.0", - "vue": "^3.5.6", + "vue": "^3.5.10", "vue-dompurify-html": "^5.0.1", "vue-router": "^4.4.5", "vue-the-mask": "^0.11.1" }, "devDependencies": { - "@cypress/code-coverage": "^3.13.0", + "@cypress/code-coverage": "^3.13.3", "@istanbuljs/nyc-config-typescript": "^1.0.2", "@rushstack/eslint-patch": "^1.1.0", "@types/jsdom": "^21.1.0", - "@types/node": "^20.12.11", + "@types/node": "^20.16.10", "@typescript-eslint/eslint-plugin": "^7.12.0", "@typescript-eslint/parser": "^7.12.0", "@vitejs/plugin-vue": "^5.0.0", @@ -54,15 +54,15 @@ "lcov-result-merger": "^5.0.0", "nyc": "^17.0.0", "prettier": "^3.0.3", - "sass": "^1.77.4", - "sass-loader": "^16.0.0", + "sass": "^1.79.4", + "sass-loader": "^16.0.2", "source-map-support": "^0.5.21", "start-server-and-test": "^2.0.8", "ts-node": "^10.9.1", "typescript": "~5.6.0", "unplugin-icons": "^0.19.0", "unplugin-vue-components": "^0.27.0", - "vite": "^5.4.6", + "vite": "^5.4.8", "vite-plugin-istanbul": "^5.0.0", "vitest": "^1.0.0", "volar-service-vetur": "latest", @@ -3272,9 +3272,9 @@ "license": "SIL" }, "node_modules/@carbon/colors": { - "version": "11.26.0", - "resolved": "https://registry.npmjs.org/@carbon/colors/-/colors-11.26.0.tgz", - "integrity": "sha512-36gCd8Oi9P2q2ZpCoGUmqwl2sj7FkwX4IdmONs+wPkG6eBA6PDET/h848bTBivrNKiSzgXaYeSim/qcs6yiXSg==", + "version": "11.27.0", + "resolved": "https://registry.npmjs.org/@carbon/colors/-/colors-11.27.0.tgz", + "integrity": "sha512-4H1Lfuw1WJYndoCSn+HOoA8JPW0old41FtuKgK+okc/QXmTpw21tK7KL6D+Yu68JEWAksEPssKUeggLAgWkYjA==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -3282,9 +3282,9 @@ } }, "node_modules/@carbon/feature-flags": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@carbon/feature-flags/-/feature-flags-0.22.0.tgz", - "integrity": "sha512-zIz2NPAljL5OpBTjasOIutTZdPOCQZbNDXpBT9NL7zWcDM7xCkVNtQITnEfLRE5vRcv5c96IVvJ+pYleX15vgg==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@carbon/feature-flags/-/feature-flags-0.23.0.tgz", + "integrity": "sha512-p98iYUNHPvBQ543hAZ2fbBedYegy3N58eemcqsexWaX0mDdbJNwZCc1fN/HtLvrgKqt70Mal/FKJHlocNRyaNA==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -3292,13 +3292,13 @@ } }, "node_modules/@carbon/grid": { - "version": "11.27.0", - "resolved": "https://registry.npmjs.org/@carbon/grid/-/grid-11.27.0.tgz", - "integrity": "sha512-UfFFpZCagdQf/PRmCQrmx8OnOBwgNmef0C0XP8tWt07DBCaa4cKI7/GI1hR2RW6EnrlHT1+d2Uk81e3EHnovmw==", + "version": "11.28.0", + "resolved": "https://registry.npmjs.org/@carbon/grid/-/grid-11.28.0.tgz", + "integrity": "sha512-J0E8gGYOOlNfKB4Omks9fJdAI5CmzWF6JvCDndTCcpq58By1atQt9n1C1jvo8iHf84ZDoU1Vd6+ZB9u4fXKdNg==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@carbon/layout": "^11.26.0", + "@carbon/layout": "^11.27.0", "@ibm/telemetry-js": "^1.5.0" } }, @@ -3341,9 +3341,9 @@ } }, "node_modules/@carbon/layout": { - "version": "11.26.0", - "resolved": "https://registry.npmjs.org/@carbon/layout/-/layout-11.26.0.tgz", - "integrity": "sha512-PYA2c9y9OaVwuxnTo9ez2FQfKZKejIvbpRdCedB4Z61JAfv03e7ZKU55AdKUstdElanL4HS/drEE/H71siPoiw==", + "version": "11.27.0", + "resolved": "https://registry.npmjs.org/@carbon/layout/-/layout-11.27.0.tgz", + "integrity": "sha512-o2++xUe2Wfg1nzneLl8ucbep9ObUE6vEgwlifVdOpvCti6zlsRjti5ojtYpx3v0yW2Qh7TJH8OYN7CyZDLeZUw==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -3351,9 +3351,9 @@ } }, "node_modules/@carbon/motion": { - "version": "11.22.0", - "resolved": "https://registry.npmjs.org/@carbon/motion/-/motion-11.22.0.tgz", - "integrity": "sha512-S5UDzgpK1sVWPPrOaoZAXycaEIj1vqCNHFFihKKk9mSSeBbVe0al4qU7yhXTQRgZzKaGtf3MDnUhSVu5EYLjSA==", + "version": "11.23.0", + "resolved": "https://registry.npmjs.org/@carbon/motion/-/motion-11.23.0.tgz", + "integrity": "sha512-zPxO/lp9FaHET967NHTbQ7pvmqATIQcsiM8WxzNuF2UUNlw6oRL/LMro1eZC9OgKAnyTXRUoxdtYphMhFOXWpA==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -3361,9 +3361,9 @@ } }, "node_modules/@carbon/pictograms": { - "version": "12.40.0", - "resolved": "https://registry.npmjs.org/@carbon/pictograms/-/pictograms-12.40.0.tgz", - "integrity": "sha512-gq4bxFCOfiiIvA+HqWpZC9o07zXKYUWv+mwle17IDNGZA9xoczoP1gmyDzpj2FDTUt7eaWa4XnAhvqBRNbkfWg==", + "version": "12.41.0", + "resolved": "https://registry.npmjs.org/@carbon/pictograms/-/pictograms-12.41.0.tgz", + "integrity": "sha512-32U5ZCKA4liqiwwtuk+B8WVc2mCH+fJ3sM/kg+ReUYyE7UoBeXQXiYH8isAfUWia3K0ITn7WASNqUycRkQwB8g==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -3371,19 +3371,19 @@ } }, "node_modules/@carbon/styles": { - "version": "1.65.0", - "resolved": "https://registry.npmjs.org/@carbon/styles/-/styles-1.65.0.tgz", - "integrity": "sha512-I+U1g2IhI0IAmIDqNIxhOyXclnAKQ4/FR7xUvpWFZ+RBqEn6kPQqxjmJiinnFYr3rk3tClPj60uAzqbHjk8S0g==", + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@carbon/styles/-/styles-1.66.0.tgz", + "integrity": "sha512-KOz5zZMFO2kx8iRazZ05SyBpKhO7ajO+ZZ7BiVy3RVb7MaziFmv9Xs9Fjx69/BO6tjMKG0Zte8GtexC6wTB8BQ==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@carbon/colors": "^11.26.0", - "@carbon/feature-flags": "^0.22.0", - "@carbon/grid": "^11.27.0", - "@carbon/layout": "^11.26.0", - "@carbon/motion": "^11.22.0", - "@carbon/themes": "^11.40.0", - "@carbon/type": "^11.31.0", + "@carbon/colors": "^11.27.0", + "@carbon/feature-flags": "^0.23.0", + "@carbon/grid": "^11.28.0", + "@carbon/layout": "^11.27.0", + "@carbon/motion": "^11.23.0", + "@carbon/themes": "^11.41.0", + "@carbon/type": "^11.32.0", "@ibm/plex": "6.0.0-next.6", "@ibm/telemetry-js": "^1.5.0" }, @@ -3397,28 +3397,28 @@ } }, "node_modules/@carbon/themes": { - "version": "11.40.0", - "resolved": "https://registry.npmjs.org/@carbon/themes/-/themes-11.40.0.tgz", - "integrity": "sha512-n/QHGmCqUHGHZsevyfjoB9fPY7THG46YSD3E5H8+pqbUPywnNTfldtU7D/hqx8b36LwtuY+I6Q4H8QQOqsCTIQ==", + "version": "11.41.0", + "resolved": "https://registry.npmjs.org/@carbon/themes/-/themes-11.41.0.tgz", + "integrity": "sha512-IhNXgbajpy0ktvO8zCBFDGARTUBDH+PrKqigg9H8B42UwfBXRIdSr8exCd5pvY9YQ0b+cM6JKQHEAm/SrfyUDw==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@carbon/colors": "^11.26.0", - "@carbon/layout": "^11.26.0", - "@carbon/type": "^11.31.0", + "@carbon/colors": "^11.27.0", + "@carbon/layout": "^11.27.0", + "@carbon/type": "^11.32.0", "@ibm/telemetry-js": "^1.5.0", "color": "^4.0.0" } }, "node_modules/@carbon/type": { - "version": "11.31.0", - "resolved": "https://registry.npmjs.org/@carbon/type/-/type-11.31.0.tgz", - "integrity": "sha512-ehcLIp8MOUy828hkcU5TZldvJPmXHAu55f9cUa5K9OU2LRjddwdYUIqOr3PlCiPQgXl9M/rvce4hXAHCfdwYeg==", + "version": "11.32.0", + "resolved": "https://registry.npmjs.org/@carbon/type/-/type-11.32.0.tgz", + "integrity": "sha512-av09976fl4YlaO4HEDs0RG17sg5X0TJbb9m9HfugZjNKDHFbWo87oGLmvgh4J+/ewkC40Wnp24rAewPEaYKFGw==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@carbon/grid": "^11.27.0", - "@carbon/layout": "^11.26.0", + "@carbon/grid": "^11.28.0", + "@carbon/layout": "^11.27.0", "@ibm/telemetry-js": "^1.5.0" } }, @@ -3511,9 +3511,9 @@ } }, "node_modules/@cypress/code-coverage": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@cypress/code-coverage/-/code-coverage-3.13.0.tgz", - "integrity": "sha512-KAyC+LNF60nh8/MHCUzmpHXoFmRws9oFkjjJwxGrHshRevCZTMcuVv/X8Uf5AUPnX9byFJLRdNGLFcK2U3Ufbw==", + "version": "3.13.3", + "resolved": "https://registry.npmjs.org/@cypress/code-coverage/-/code-coverage-3.13.3.tgz", + "integrity": "sha512-6cunouO0xYNoD6ZeS5392SC19RXbBstfuTrDG4PQsUwID2Gadg0wJdpeL0ZJUwBBq102COl2FxP8qzCfS1gWsg==", "dev": true, "license": "MIT", "dependencies": { @@ -6194,9 +6194,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.16.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz", - "integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==", + "version": "20.16.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.10.tgz", + "integrity": "sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA==", "dev": true, "license": "MIT", "dependencies": { @@ -6668,39 +6668,39 @@ "license": "MIT" }, "node_modules/@vue/compiler-core": { - "version": "3.5.6", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.6.tgz", - "integrity": "sha512-r+gNu6K4lrvaQLQGmf+1gc41p3FO2OUJyWmNqaIITaJU6YFiV5PtQSFZt8jfztYyARwqhoCayjprC7KMvT3nRA==", + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.10.tgz", + "integrity": "sha512-iXWlk+Cg/ag7gLvY0SfVucU8Kh2CjysYZjhhP70w9qI4MvSox4frrP+vDGvtQuzIcgD8+sxM6lZvCtdxGunTAA==", "license": "MIT", "dependencies": { "@babel/parser": "^7.25.3", - "@vue/shared": "3.5.6", + "@vue/shared": "3.5.10", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.0" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.6", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.6.tgz", - "integrity": "sha512-xRXqxDrIqK8v8sSScpistyYH0qYqxakpsIvqMD2e5sV/PXQ1mTwtXp4k42yHK06KXxKSmitop9e45Ui/3BrTEw==", + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.10.tgz", + "integrity": "sha512-DyxHC6qPcktwYGKOIy3XqnHRrrXyWR2u91AjP+nLkADko380srsC2DC3s7Y1Rk6YfOlxOlvEQKa9XXmLI+W4ZA==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.6", - "@vue/shared": "3.5.6" + "@vue/compiler-core": "3.5.10", + "@vue/shared": "3.5.10" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.6", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.6.tgz", - "integrity": "sha512-pjWJ8Kj9TDHlbF5LywjVso+BIxCY5wVOLhkEXRhuCHDxPFIeX1zaFefKs8RYoHvkSMqRWt93a0f2gNJVJixHwg==", + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.10.tgz", + "integrity": "sha512-to8E1BgpakV7224ZCm8gz1ZRSyjNCAWEplwFMWKlzCdP9DkMKhRRwt0WkCjY7jkzi/Vz3xgbpeig5Pnbly4Tow==", "license": "MIT", "dependencies": { "@babel/parser": "^7.25.3", - "@vue/compiler-core": "3.5.6", - "@vue/compiler-dom": "3.5.6", - "@vue/compiler-ssr": "3.5.6", - "@vue/shared": "3.5.6", + "@vue/compiler-core": "3.5.10", + "@vue/compiler-dom": "3.5.10", + "@vue/compiler-ssr": "3.5.10", + "@vue/shared": "3.5.10", "estree-walker": "^2.0.2", "magic-string": "^0.30.11", "postcss": "^8.4.47", @@ -6708,13 +6708,13 @@ } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.6", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.6.tgz", - "integrity": "sha512-VpWbaZrEOCqnmqjE83xdwegtr5qO/2OPUC6veWgvNqTJ3bYysz6vY3VqMuOijubuUYPRpG3OOKIh9TD0Stxb9A==", + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.10.tgz", + "integrity": "sha512-hxP4Y3KImqdtyUKXDRSxKSRkSm1H9fCvhojEYrnaoWhE4w/y8vwWhnosJoPPe2AXm5sU7CSbYYAgkt2ZPhDz+A==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.6", - "@vue/shared": "3.5.6" + "@vue/compiler-dom": "3.5.10", + "@vue/shared": "3.5.10" } }, "node_modules/@vue/compiler-vue2": { @@ -7027,53 +7027,53 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.5.6", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.6.tgz", - "integrity": "sha512-shZ+KtBoHna5GyUxWfoFVBCVd7k56m6lGhk5e+J9AKjheHF6yob5eukssHRI+rzvHBiU1sWs/1ZhNbLExc5oYQ==", + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.10.tgz", + "integrity": "sha512-kW08v06F6xPSHhid9DJ9YjOGmwNDOsJJQk0ax21wKaUYzzuJGEuoKNU2Ujux8FLMrP7CFJJKsHhXN9l2WOVi2g==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.6" + "@vue/shared": "3.5.10" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.6", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.6.tgz", - "integrity": "sha512-FpFULR6+c2lI+m1fIGONLDqPQO34jxV8g6A4wBOgne8eSRHP6PQL27+kWFIx5wNhhjkO7B4rgtsHAmWv7qKvbg==", + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.10.tgz", + "integrity": "sha512-9Q86I5Qq3swSkFfzrZ+iqEy7Vla325M7S7xc1NwKnRm/qoi1Dauz0rT6mTMmscqx4qz0EDJ1wjB+A36k7rl8mA==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.6", - "@vue/shared": "3.5.6" + "@vue/reactivity": "3.5.10", + "@vue/shared": "3.5.10" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.6", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.6.tgz", - "integrity": "sha512-SDPseWre45G38ENH2zXRAHL1dw/rr5qp91lS4lt/nHvMr0MhsbCbihGAWLXNB/6VfFOJe2O+RBRkXU+CJF7/sw==", + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.10.tgz", + "integrity": "sha512-t3x7ht5qF8ZRi1H4fZqFzyY2j+GTMTDxRheT+i8M9Ph0oepUxoadmbwlFwMoW7RYCpNQLpP2Yx3feKs+fyBdpA==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.6", - "@vue/runtime-core": "3.5.6", - "@vue/shared": "3.5.6", + "@vue/reactivity": "3.5.10", + "@vue/runtime-core": "3.5.10", + "@vue/shared": "3.5.10", "csstype": "^3.1.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.6", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.6.tgz", - "integrity": "sha512-zivnxQnOnwEXVaT9CstJ64rZFXMS5ZkKxCjDQKiMSvUhXRzFLWZVbaBiNF4HGDqGNNsTgmjcCSmU6TB/0OOxLA==", + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.10.tgz", + "integrity": "sha512-IVE97tt2kGKwHNq9yVO0xdh1IvYfZCShvDSy46JIh5OQxP1/EXSpoDqetVmyIzL7CYOWnnmMkVqd7YK2QSWkdw==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.6", - "@vue/shared": "3.5.6" + "@vue/compiler-ssr": "3.5.10", + "@vue/shared": "3.5.10" }, "peerDependencies": { - "vue": "3.5.6" + "vue": "3.5.10" } }, "node_modules/@vue/shared": { - "version": "3.5.6", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.6.tgz", - "integrity": "sha512-eidH0HInnL39z6wAt6SFIwBrvGOpDWsDxlw3rCgo1B+CQ1781WzQUSU3YjxgdkcJo9Q8S6LmXTkvI+cLHGkQfA==", + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.10.tgz", + "integrity": "sha512-VkkBhU97Ki+XJ0xvl4C9YJsIZ2uIlQ7HqPpZOS3m9VCvmROPaChZU6DexdMJqvz9tbgG+4EtFVrSuailUq5KGQ==", "license": "MIT" }, "node_modules/@vue/test-utils": { @@ -7584,7 +7584,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -8039,7 +8039,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8089,7 +8089,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -8389,7 +8389,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -8414,7 +8414,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -10928,7 +10928,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -11843,7 +11843,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -12011,7 +12011,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -12031,7 +12031,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -12074,7 +12074,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -13477,7 +13477,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -14303,7 +14303,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -14754,7 +14754,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -15204,13 +15204,13 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.78.0.tgz", - "integrity": "sha512-AaIqGSrjo5lA2Yg7RvFZrlXDBCp3nV4XP73GrLGvdRWWwk+8H3l0SDvq/5bA4eF+0RFPLuWUk3E+P1U/YqnpsQ==", + "version": "1.79.4", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.79.4.tgz", + "integrity": "sha512-K0QDSNPXgyqO4GZq2HO5Q70TLxTH6cIT59RdoCHMivrC8rqzaTw5ab9prjz9KUN1El4FLXrBXJhik61JR4HcGg==", "devOptional": true, "license": "MIT", "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", + "chokidar": "^4.0.0", "immutable": "^4.0.0", "source-map-js": ">=0.6.2 <2.0.0" }, @@ -15222,9 +15222,9 @@ } }, "node_modules/sass-loader": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.1.tgz", - "integrity": "sha512-xACl1ToTsKnL9Ce5yYpRxrLj9QUDCnwZNhzpC7tKiFyA8zXsd3Ap+HGVnbCgkdQcm43E+i6oKAWBsvGA6ZoiMw==", + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.2.tgz", + "integrity": "sha512-Ll6iXZ1EYwYT19SqW4mSBb76vSSi8JgzElmzIerhEGgzB5hRjDQIWsPmuk1UrAXkR16KJHqVY0eH+5/uw9Tmfw==", "dev": true, "license": "MIT", "dependencies": { @@ -15262,6 +15262,36 @@ } } }, + "node_modules/sass/node_modules/chokidar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.1.tgz", + "integrity": "sha512-GkMg9uOTpIWWKbSsgwb5fA4EavTR+SG/PMPoAY8hkhHfEEY0/vqljY+XHqtDf2cr2IJtoNRDbrrEpZUiZCkYRw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", @@ -16287,7 +16317,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -17082,9 +17112,9 @@ } }, "node_modules/vite": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz", - "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==", + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", + "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==", "dev": true, "license": "MIT", "dependencies": { @@ -17514,16 +17544,16 @@ "license": "MIT" }, "node_modules/vue": { - "version": "3.5.6", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.6.tgz", - "integrity": "sha512-zv+20E2VIYbcJOzJPUWp03NOGFhMmpCKOfSxVTmCYyYFFko48H9tmuQFzYj7tu4qX1AeXlp9DmhIP89/sSxxhw==", + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.10.tgz", + "integrity": "sha512-Vy2kmJwHPlouC/tSnIgXVg03SG+9wSqT1xu1Vehc+ChsXsRd7jLkKgMltVEFOzUdBr3uFwBCG+41LJtfAcBRng==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.6", - "@vue/compiler-sfc": "3.5.6", - "@vue/runtime-dom": "3.5.6", - "@vue/server-renderer": "3.5.6", - "@vue/shared": "3.5.6" + "@vue/compiler-dom": "3.5.10", + "@vue/compiler-sfc": "3.5.10", + "@vue/runtime-dom": "3.5.10", + "@vue/server-renderer": "3.5.10", + "@vue/shared": "3.5.10" }, "peerDependencies": { "typescript": "*" diff --git a/frontend/package.json b/frontend/package.json index d4c6845afd..3563ffbe2f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -50,25 +50,25 @@ "@bcgov-nr/nr-fsa-theme": "^1.1.3", "@bcgov/bc-sans": "^2.0.0", "@carbon/icons-vue": "^10.92.0", - "@carbon/pictograms": "^12.40.0", - "@carbon/styles": "^1.65.0", + "@carbon/pictograms": "^12.41.0", + "@carbon/styles": "^1.66.0", "@carbon/web-components": "^2.12.0", "@vueuse/core": "^11.1.0", "aws-amplify": "^6.3.5", "axios": "^1.7.2", "date-fns": "^4.0.0", "dotenv": "^16.0.0", - "vue": "^3.5.6", + "vue": "^3.5.10", "vue-dompurify-html": "^5.0.1", "vue-router": "^4.4.5", "vue-the-mask": "^0.11.1" }, "devDependencies": { - "@cypress/code-coverage": "^3.13.0", + "@cypress/code-coverage": "^3.13.3", "@istanbuljs/nyc-config-typescript": "^1.0.2", "@rushstack/eslint-patch": "^1.1.0", "@types/jsdom": "^21.1.0", - "@types/node": "^20.12.11", + "@types/node": "^20.16.10", "@typescript-eslint/eslint-plugin": "^7.12.0", "@typescript-eslint/parser": "^7.12.0", "@vitejs/plugin-vue": "^5.0.0", @@ -92,15 +92,15 @@ "lcov-result-merger": "^5.0.0", "nyc": "^17.0.0", "prettier": "^3.0.3", - "sass": "^1.77.4", - "sass-loader": "^16.0.0", + "sass": "^1.79.4", + "sass-loader": "^16.0.2", "source-map-support": "^0.5.21", "start-server-and-test": "^2.0.8", "ts-node": "^10.9.1", "typescript": "~5.6.0", "unplugin-icons": "^0.19.0", "unplugin-vue-components": "^0.27.0", - "vite": "^5.4.6", + "vite": "^5.4.8", "vite-plugin-istanbul": "^5.0.0", "vitest": "^1.0.0", "volar-service-vetur": "latest", diff --git a/frontend/src/assets/styles/global.scss b/frontend/src/assets/styles/global.scss index 9f4a9cf942..b5e6dbb799 100644 --- a/frontend/src/assets/styles/global.scss +++ b/frontend/src/assets/styles/global.scss @@ -642,6 +642,13 @@ cds-actionable-notification * { letter-spacing: 0.01rem; } +.body-compact-02 { + font-size: 0.8712rem; + font-weight: 700; + line-height: 1.12rem; + letter-spacing: 0.01rem; +} + .content { flex-grow: 1; align-self: stretch; diff --git a/frontend/src/components/MainHeaderComponent.vue b/frontend/src/components/MainHeaderComponent.vue index 1610924000..0042ba03a7 100644 --- a/frontend/src/components/MainHeaderComponent.vue +++ b/frontend/src/components/MainHeaderComponent.vue @@ -27,8 +27,6 @@ import Avatar16 from "@carbon/icons-vue/es/user--avatar/24"; // @ts-ignore import Result16 from "@carbon/icons-vue/es/result/16"; // @ts-ignore -import SignOut16 from "@carbon/icons-vue/es/user--follow/16"; -// @ts-ignore import Close16 from "@carbon/icons-vue/es/close/16"; // @ts-ignore import TaskAdd16 from "@carbon/icons-vue/es/task--add/16"; @@ -87,7 +85,7 @@ const session = instance?.appContext.config.globalProperties.$session; const logout = () => { session?.logOut(); -} +}; const route = useRoute(); @@ -100,14 +98,26 @@ const onClickLogout = () => { }; const headerBarButtonsSize = computed(() => - isSmallScreen.value || isMediumScreen.value ? "lg" : "sm", + isSmallScreen.value || isMediumScreen.value ? "lg" : "sm" ); const logoutBtnKind = computed(() => - isSmallScreen.value || isMediumScreen.value ? "ghost" : "tertiary", + isSmallScreen.value || isMediumScreen.value ? "ghost" : "tertiary" ); -const userHasAuthority = ["CLIENT_EDITOR", "CLIENT_ADMIN"].some(authority => ForestClientUserSession.authorities.includes(authority)); +const userHasAuthority = ["CLIENT_EDITOR", "CLIENT_ADMIN"].some((authority) => + ForestClientUserSession.authorities.includes(authority) +); + +const handleLogoutClick = (event) => { + event.preventDefault(); + + if (route.name === "staff-form") { + logoutModalActive.value = true; + } else { + logout(); + } +};