diff --git a/.codecov.yml b/.codecov.yml index f0ae880aff..bb79cac6b4 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -10,5 +10,5 @@ coverage: changes: default: off codecov: - branch: master + branch: main comment: false diff --git a/.design-system-version b/.design-system-version index 865604333e..10f98a1349 100644 --- a/.design-system-version +++ b/.design-system-version @@ -1 +1 @@ -46.1.4 +70.0.16 diff --git a/.development.env b/.development.env index b49987441c..86a1116505 100644 --- a/.development.env +++ b/.development.env @@ -1,5 +1,5 @@ FLASK_APP=application.py -FLASK_ENV=development +FLASK_DEBUG=1 EQ_QUESTIONNAIRE_STATE_TABLE_NAME=dev-questionnaire-state EQ_SESSION_TABLE_NAME=dev-eq-session EQ_USED_JTI_CLAIM_TABLE_NAME=dev-used-jti-claim @@ -27,3 +27,6 @@ CDN_ASSETS_PATH=/design-system ADDRESS_LOOKUP_API_URL=https://whitelodge-ai-api.census-gcp.onsdigital.uk COOKIE_SETTINGS_URL=# EQ_SUBMISSION_CONFIRMATION_BACKEND=log +SDS_API_BASE_URL=http://localhost:5003 +CIR_API_BASE_URL=http://localhost:5004 +OIDC_TOKEN_BACKEND=local diff --git a/.eslintrc b/.eslintrc index b2de38fc80..f6110e9fed 100755 --- a/.eslintrc +++ b/.eslintrc @@ -22,15 +22,13 @@ } }, "plugins": [ - "json", - "chai-friendly" + "json" ], "rules": { "no-loss-of-precision": 0, "no-nonoctal-decimal-escape": 0, "no-unsafe-optional-chaining": 0, "no-useless-backreference": 0, - "chai-friendly/no-unused-expressions": 2, "consistent-return": 1, "indent": [ 2, @@ -70,6 +68,7 @@ { "blocks": "never" } - ] + ], + "require-await": "error" } } diff --git a/.functional-tests.env b/.functional-tests.env index 87f6f7d9f6..9803a04cc0 100644 --- a/.functional-tests.env +++ b/.functional-tests.env @@ -1,5 +1,5 @@ FLASK_APP=application.py -FLASK_ENV=development +FLASK_DEBUG=1 EQ_QUESTIONNAIRE_STATE_TABLE_NAME=dev-questionnaire-state EQ_SESSION_TABLE_NAME=dev-eq-session EQ_USED_JTI_CLAIM_TABLE_NAME=dev-used-jti-claim @@ -24,7 +24,9 @@ WEB_SERVER_THREADS=10 WEB_SERVER_UWSGI_ASYNC_CORES=10 CDN_URL=https://cdn.eq.gcp.onsdigital.uk CDN_ASSETS_PATH=/design-system -ADDRESS_LOOKUP_API_URL=https://whitelodge-ai-api.census-gcp.onsdigital.uk COOKIE_SETTINGS_URL=# EQ_SUBMISSION_CONFIRMATION_BACKEND=log VIEW_SUBMITTED_RESPONSE_EXPIRATION_IN_SECONDS=35 +SDS_API_BASE_URL=http://localhost:5003 +CIR_API_BASE_URL=http://localhost:5004 +OIDC_TOKEN_BACKEND=local diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..c01362a6a4 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @ONSdigital/eq-runner diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 3f8d7a0bdf..d404e42927 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,10 +1,9 @@ ### What is the context of this PR? Describe what you have changed and why, link to other PRs or Issues as appropriate. -### How to review +### How to review Describe the steps required to test the changes (include screenshots if appropriate). ### Checklist - * [ ] New static content marked up for translation * [ ] Newly defined schema content included in eq-translations repo diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000000..da4a38cc95 --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,137 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + day: "friday" + time: "08:00" + timezone: "Europe/London" + labels: + - "dependencies" + - "github-actions" + + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "monthly" + day: "friday" + time: "08:00" + timezone: "Europe/London" + versioning-strategy: increase-if-necessary + labels: + - "dependencies" + - "node" + groups: + development-dependencies: + dependency-type: "development" + patterns: + - "@wdio*" + - "@babel*" + - "eslint*" + - "json*" + - "jsrsasign*" + - "livereload*" + - "node-forge*" + - "prettier*" + - "typescript*" + - "uuid*" + - "webdriverio*" + ignore: + # temporarily pinned to minor/patch only - eslint v9 not supported in eslint-config-standard v17.1.0: https://github.com/standard/eslint-config-standard/issues/410 + # This had a knock-on effect with `eslint-plugin-n` and `eslint-plugin-promise` + - dependency-name: "eslint*" + update-types: [ "version-update:semver-major" ] + + # temporarily pinned to minor/patch only - wdio v9 causes getHTML() to return strings with indentation & newlines, causing assertion errors - needs investigation + - dependency-name: "@wdio/local-runner" + update-types: [ "version-update:semver-major" ] + + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + day: "friday" + time: "08:00" + timezone: "Europe/London" + # Workaround to have two "pip" ecosystems: actively setting "target-branch: main" for one config, and leaving it unset for the other config + target-branch: main + versioning-strategy: increase-if-necessary + labels: + - "dependencies" + - "python" + allow: + - dependency-type: "production" + groups: + production-dependencies: + dependency-type: "production" + patterns: + - "flask*" + - "google*" + - "python*" + - "colorama" + - "grpcio" + - "gunicorn" + - "pika" + - "pyyaml" + - "requests" + - "sdc-cryptography" + - "structlog" + - "ua-parser" + - "blinker" + - "boto3" + - "humanize" + - "marshmallow" + - "jsonpointer" + - "redis" + - "htmlmin" + - "coloredlogs" + - "uwsgi" + - "email-validator" + - "itsdangerous" + - "simplejson" + - "markupsafe" + - "pdfkit" + - "ordered-set" + - "cachetools" + - "gevent" + - "babel" + ignore: + # "babel" temporarily pinned to v2.14.0 - problem for translations found in v2.15.0, see: https://github.com/ONSdigital/eq-questionnaire-runner/pull/1384 + - dependency-name: "babel" + update-types: [ "version-update:semver-major", "version-update:semver-minor" ] + + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "monthly" + day: "friday" + time: "08:00" + timezone: "Europe/London" + versioning-strategy: increase-if-necessary + labels: + - "dependencies" + - "python-dev" + allow: + - dependency-type: "development" + groups: + development-dependencies: + dependency-type: "development" + patterns: + - "pytest*" + - "pylint*" + - "types*" + - "pep8" + - "mock" + - "jsonschema" + - "beautifulsoup4" + - "httmock" + - "moto" + - "freezegun" + - "fakeredis" + - "mypy" + - "responses" + - "playwright" + - "black" + - "djlint" + - "ruff" diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000000..6f3e433fb5 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,20 @@ +changelog: + categories: + - title: âš ī¸ Breaking Changes + labels: + - Breaking Change + - title: New Features + labels: + - New Feature + - title: Enhancements + labels: + - Enhancement + - title: Bug Fixes + labels: + - Bug Fix + - title: Documentation + labels: + - Documentation + - title: Other Changes + labels: + - "*" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 865a1f3a5c..7af23d64cf 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,71 +1,49 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -name: "CodeQL" +name: "CodeQL Advanced" on: push: - branches: [master] + branches: [ "main" ] pull_request: - # The branches below must be a subset of the branches above - branches: [master] + branches: [ "main" ] schedule: - cron: '0 15 * * 3' jobs: analyze: - name: Analyze - runs-on: ubuntu-latest + name: Analyze (${{ matrix.language }}) + runs-on: [ubuntu-24.04] + permissions: + security-events: write + packages: read + actions: read + contents: read strategy: fail-fast: false matrix: - # Override automatic language detection by changing the below list - # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] - language: ['python', 'javascript'] - # Learn more... - # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection + include: + - language: actions + build-mode: none + - language: javascript-typescript + build-mode: none + - language: python + build-mode: none steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. fetch-depth: 2 - # If this run was triggered by a pull request event, then checkout - # the head of the pull request instead of the merge commit. - - run: git checkout HEAD^2 - if: ${{ github.event_name == 'pull_request' }} - # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 with: languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - # â„šī¸ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - + build-mode: ${{ matrix.build-mode }} - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 + with: + category: "/language:${{matrix.language}}" \ No newline at end of file diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml deleted file mode 100644 index ec9cafc65a..0000000000 --- a/.github/workflows/master.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Master - -on: - push: - branches: - - master - -jobs: - docker-push: - runs-on: ubuntu-18.04 - steps: - - uses: actions/checkout@v2 - - name: Write app version - run: printf "$GITHUB_SHA" > .application-version - - name: Build - run: > - docker build -t onsdigital/eq-questionnaire-runner:latest . - - name: Push to Docker Hub - run: | - echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - echo "Pushing with tag [latest]" - docker push onsdigital/eq-questionnaire-runner:latest diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 9a04a02d75..729a79556e 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -3,167 +3,197 @@ name: PR on: pull_request: branches: - - "master" + - "main" - "branch-v*" + - "bug-fix-*" + - "feature-*" + +concurrency: + group: '${{ github.head_ref }}' + cancel-in-progress: true jobs: python-dependencies: - runs-on: ubuntu-18.04 + permissions: + contents: 'read' + runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - run: | echo "PYTHON_VERSION=$(cat .python-version)" >> $GITHUB_ENV - - uses: actions/setup-python@v2 + - name: Install Poetry + uses: snok/install-poetry@76e04a911780d5b312d89783f7b1cd627778900a # v1.4.1 with: - python-version: ${{ env.PYTHON_VERSION }} - - name: Install apt dependencies - run: | - sudo apt-get install libsnappy-dev libgconf-2-4 - - # Install wkthtmltopdf with patched Qt - sudo apt-get install -y xfonts-base xfonts-75dpi - wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.bionic_amd64.deb - sudo dpkg -i wkhtmltox_0.12.6-1.bionic_amd64.deb - - name: Install Pipenv - run: pip install pipenv==2018.11.26 - - name: Cache virtualenv - id: cache-virtualenv - uses: actions/cache@v1 + version: 2.1.2 + virtualenvs-create: true + - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 with: - path: ~/.local/share/virtualenvs/ - key: ${{ runner.os }}-${{ env.PYTHON_VERSION }}-virtualenvs-${{ hashFiles('Pipfile.lock') }} + python-version: ${{ env.PYTHON_VERSION }} + cache: 'poetry' - name: Install virtual environment - if: steps.cache-virtualenv.outputs.cache-hit != 'true' - run: pipenv install --dev + run: | + sudo apt-get install libsnappy-dev + poetry install node-dependencies: - runs-on: ubuntu-18.04 + permissions: + contents: 'read' + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + with: + node-version-file : ".nvmrc" + - name: Install npm deps + run: npm install + lint: + permissions: + contents: 'read' + needs: [python-dependencies, node-dependencies] + runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - run: | + echo "PYTHON_VERSION=$(cat .python-version)" >> $GITHUB_ENV + - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 with: - node-version: "14.19.0" - - name: Get yarn cache - id: get-yarn-cache - run: echo "::set-output name=dir::$(yarn cache dir)" - - name: Cache yarn modules - id: cache-yarn - uses: actions/cache@v1 + node-version-file: ".nvmrc" + - name: Write app version + run: printf "${{ github.event.pull_request.head.sha }}" > .application-version + - name: Install Poetry + uses: snok/install-poetry@76e04a911780d5b312d89783f7b1cd627778900a # v1.4.1 + with: + version: 2.1.2 + virtualenvs-create: true + - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 with: - path: ${{ steps.get-yarn-cache.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} - - name: Install yarn deps - if: steps.cache-yarn.outputs.cache-hit != 'true' - run: yarn + python-version: ${{ env.PYTHON_VERSION }} + cache: "poetry" + - name: Compile translations + run: make translate + - name: Running translation tests + run: poetry run python -m scripts.extract_translation_templates --test + - name: Python linting + run: make lint-python + - name: Install npm deps + run: npm install + - name: Functional tests spec lint + run: ./scripts/lint_functional_test_specs.sh + - name: Javascript linting + run: make lint-js + - name: HTML linting + run: make lint-html test-unit: + if: "!contains(github.event.pull_request.labels.*.name, 'tests not required')" + permissions: + contents: 'read' needs: python-dependencies - runs-on: ubuntu-18.04 + runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - run: | echo "PYTHON_VERSION=$(cat .python-version)" >> $GITHUB_ENV - - uses: actions/setup-python@v2 - with: - python-version: ${{ env.PYTHON_VERSION }} - name: Install apt dependencies run: | sudo apt-get install libsnappy-dev libgconf-2-4 jq - # Install wkthtmltopdf with patched Qt sudo apt-get install -y xfonts-base xfonts-75dpi wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.bionic_amd64.deb sudo dpkg -i wkhtmltox_0.12.6-1.bionic_amd64.deb - name: Write app version run: printf "${{ github.event.pull_request.head.sha }}" > .application-version - - name: Install pipenv - run: pip install pipenv==2018.11.26 - - name: Cache virtualenv - id: cache-virtualenv - uses: actions/cache@v1 + - name: Install Poetry + uses: snok/install-poetry@76e04a911780d5b312d89783f7b1cd627778900a # v1.4.1 with: - path: ~/.local/share/virtualenvs/ - key: ${{ runner.os }}-${{ env.PYTHON_VERSION }}-virtualenvs-${{ hashFiles('Pipfile.lock') }} - - name: Install virtual environment - if: steps.cache-virtualenv.outputs.cache-hit != 'true' - run: pipenv install --dev + version: 2.1.2 + virtualenvs-create: true + - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: "poetry" + - name: Install dotenv plugin + run: poetry self add poetry-plugin-dotenv@2.9.0 - name: Load templates run: make load-design-system-templates - name: Compile translations run: make translate - name: Link env vars run: ln -sf .development.env .env - - name: Running translation tests - run: pipenv run python -m scripts.extract_translation_templates --test - - name: Running lint tests - run: pipenv run ./scripts/run_lint_python.sh - name: Running unit tests - run: pipenv run ./scripts/run_tests_unit.sh + run: make test-unit validate-schemas: - runs-on: ubuntu-18.04 + permissions: + contents: 'read' + runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - run: | + echo "PYTHON_VERSION=$(cat .python-version)" >> $GITHUB_ENV - name: Run validator run: ./scripts/run_validator.sh - - name: Running schema tests - run: ./scripts/validate_test_schemas.sh + - name: Install Poetry + uses: snok/install-poetry@76e04a911780d5b312d89783f7b1cd627778900a # v1.4.1 + with: + version: 2.1.2 + virtualenvs-create: true + - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: "poetry" + - name: Running schema validation + run: make validate-test-schemas test-functional: + if: "!contains(github.event.pull_request.labels.*.name, 'tests not required')" + permissions: + contents: 'read' + needs: [python-dependencies, node-dependencies] strategy: matrix: - suite: [ timeout_modal, features, general, components ] - needs: [python-dependencies, node-dependencies] - runs-on: ubuntu-18.04 + suite: [ timeout_modal_expired, timeout_modal_extended, timeout_modal_extended_new_window, features, summaries, general, journeys, components, list_collector] + runs-on: ubuntu-22.04 + timeout-minutes: 30 env: EQ_RUN_FUNCTIONAL_TESTS_HEADLESS: True + # :TODO: Revisit & update when 2 instances can be used without adverse effects + EQ_FUNCTIONAL_TEST_MAX_INSTANCES: 2 steps: - - uses: actions/checkout@v2 - - name: Update to latest stable Chrome - run: | - wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb - sudo apt install ./google-chrome-stable_current_amd64.deb - - uses: actions/setup-node@v1 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 with: - node-version: "14.19.0" + node-version-file: ".nvmrc" - run: | echo "PYTHON_VERSION=$(cat .python-version)" >> $GITHUB_ENV - - uses: actions/setup-python@v2 + - name: Install Poetry + uses: snok/install-poetry@76e04a911780d5b312d89783f7b1cd627778900a # v1.4.1 with: - python-version: ${{ env.PYTHON_VERSION }} - - name: Install pipenv - run: pip install pipenv==2018.11.26 - - name: Cache virtualenv - id: cache-virtualenv - uses: actions/cache@v1 - with: - path: ~/.local/share/virtualenvs/ - key: ${{ runner.os }}-${{ env.PYTHON_VERSION }}-virtualenvs-${{ hashFiles('Pipfile.lock') }} - - name: Install virtual environment - if: steps.cache-virtualenv.outputs.cache-hit != 'true' - run: | - pipenv install --dev - - name: Get yarn cache - id: get-yarn-cache - run: echo "::set-output name=dir::$(yarn cache dir)" - - name: Cache yarn modules - uses: actions/cache@v1 - id: cache-yarn + version: 2.1.2 + virtualenvs-create: true + - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 with: - path: ${{ steps.get-yarn-cache.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} - - name: Install yarn deps - run: yarn install - - name: Functional tests spec lint - run: ./scripts/lint_functional_test_specs.sh - - name: Javascript linting check - run: yarn lint + python-version: ${{ env.PYTHON_VERSION }} + cache: "poetry" + - name: Install npm deps + run: npm install - name: Docker compose - run: docker-compose --version && RUNNER_ENV_FILE=.functional-tests.env docker-compose up --build -d + run: docker compose --version && RUNNER_ENV_FILE=.functional-tests.env docker compose up --build -d - name: Functional tests - run: ./scripts/run_tests_functional.sh ${{ matrix.suite }} + run: make test-functional-suite SUITE=${{ matrix.suite }} - name: Docker compose shutdown - run: RUNNER_ENV_FILE=.functional-tests.env docker-compose kill + run: RUNNER_ENV_FILE=.functional-tests.env docker compose kill docker-push: - runs-on: ubuntu-18.04 + permissions: + contents: 'read' + id-token: 'write' + runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - id: auth + name: Authenticate to Google Cloud + uses: google-github-actions/auth@6fc4af4b145ae7821d527454aa9bd537d1f2dc5f # v2.17 + with: + token_format: 'access_token' + workload_identity_provider: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ secrets.SERVICE_ACCOUNT }} + - name: Set Tag and SHA run: | CLEAN_TAG=$(echo "${{ github.event.pull_request.head.ref }}" | tr / -) @@ -174,20 +204,9 @@ jobs: echo "Writing SHA $SHA to .application_version" printf $SHA > .application-version - name: Build - run: > - docker build -t onsdigital/eq-questionnaire-runner:$TAG - -t ${{ secrets.GAR_LOCATION }}/${{ secrets.GAR_PROJECT_ID }}/docker-images/eq-questionnaire-runner:$TAG . - - name: Push to Docker Hub - run: | - echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - echo "Pushing to DockerHub with tag $TAG" - docker push onsdigital/eq-questionnaire-runner:$TAG - + run: docker build -t ${{ secrets.GAR_LOCATION }}/${{ secrets.GAR_PROJECT_ID }}/docker-images/eq-questionnaire-runner:$TAG . - name: Push to GAR - env: - GAR_SERVICE_KEY: ${{ secrets.GAR_SERVICE_KEY }} run: | - echo $GAR_SERVICE_KEY | docker login -u _json_key --password-stdin https://${{ secrets.GAR_LOCATION }} gcloud auth configure-docker ${{ secrets.GAR_LOCATION }} echo "Pushing to GAR with tag $TAG" docker push ${{ secrets.GAR_LOCATION }}/${{ secrets.GAR_PROJECT_ID }}/docker-images/eq-questionnaire-runner:$TAG diff --git a/.nvmrc b/.nvmrc index 7b16f790a7..d2c5c8a013 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v14.19.0 +v22.15.0 \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index b3bd224028..8eb11fc6b6 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1,2 @@ -tests/functional/generated_pages/ \ No newline at end of file +# Exclude generated_pages directory +tests/functional/generated_pages/ diff --git a/.pylintrc b/.pylintrc index 0262a0dfe2..a30a8bcc96 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,421 +1,613 @@ -[MASTER] +[MAIN] -# Specify a configuration file. -#rcfile= +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Load and enable all available extensions. Use --list-extensions to see a list +# all available extensions. +#enable-all-extensions= + +# In error mode, messages with a category besides ERROR or FATAL are +# suppressed, and no reports are done by default. Error mode is compatible with +# disabling specific errors. +#errors-only= + +# Always return a 0 (non-error) status code, even if lint errors are found. +# This is primarily useful in continuous integration scripts. +#exit-zero= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-allow-list= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +extension-pkg-whitelist= + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= + +# Specify a score threshold under which the program will exit with error. +fail-under=10 + +# Interpret the stdin as a python script, whose filename needs to be passed as +# the module_or_package argument. +#from-stdin= + +# Files or directories to be skipped. They should be base names, not paths. +ignore=CVS + +# Add files or directories matching the regular expressions patterns to the +# ignore-list. The regex matches against paths and can be in Posix or Windows +# format. Because '\' represents the directory delimiter on Windows systems, it +# can't be used as an escape character. +ignore-paths=node_modules + +# Files or directories matching the regular expression patterns are skipped. +# The regex matches against base names, not paths. The default value ignores +# Emacs file locks +ignore-patterns=^\.# + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). #init-hook= -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=CVS +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use, and will cap the count on Windows to +# avoid hangs. +jobs=0 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 -# Add files or directories matching the regex patterns to the blacklist. The -# regex matches against base names, not paths. -ignore-patterns= +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins=pylint.extensions.mccabe,pylint.extensions.no_self_use,pylint_absolute_imports # Pickle collected data for later comparisons. persistent=yes -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins=pylint.extensions.mccabe,pylint_quotes +# Minimum Python version to use for version dependent checks. Will default to +# the version used to run pylint. +py-version=3.12 + +# Discover python modules and packages in the file system subtree. +recursive=no -# Use multiple processes to speed up Pylint. -jobs=1 +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes # Allow loading of arbitrary C extensions. Extensions are imported into the # active Python interpreter and may run arbitrary code. unsafe-load-any-extension=no -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code -extension-pkg-whitelist= +# In verbose mode, extra non-checker-related info will be displayed. +#verbose= -# Allow optimization of some AST trees. This will activate a peephole AST -# optimizer, which will apply various small optimizations. For instance, it can -# be used to obtain the result of joining multiple strings with the addition -# operator. Joining a lot of strings can lead to a maximum recursion error in -# Pylint and this flag can prevent that. It has one side effect, the resulting -# AST will be different than the one from reality. This option is deprecated -# and it will be removed in Pylint 2.0. -optimize-ast=no +[BASIC] -[MESSAGES CONTROL] +# Naming style matching correct argument names. +argument-naming-style=snake_case -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED -confidence= +# Regular expression matching correct argument names. Overrides argument- +# naming-style. If left empty, argument names will be checked with the set +# naming style. +argument-rgx=[a-z_][a-z0-9_]{2,30}$ -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -#enable= +# Naming style matching correct attribute names. +attr-naming-style=snake_case -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once).You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use"--disable=all --enable=classes -# --disable=W" -disable= - too-many-arguments, - too-many-instance-attributes, - too-few-public-methods, - missing-docstring, - invalid-name, - inconsistent-return-statements, - bad-continuation, - duplicate-code, +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. If left empty, attribute names will be checked with the set naming +# style. +attr-rgx=[a-z_][a-z0-9_]{2,30}$ +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata -[REPORTS] +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= -# Set the output format. Available formats are text, parseable, colorized, msvs -# (visual studio) and html. You can also give a reporter class, eg -# mypackage.mymodule.MyReporterClass. -output-format=text +# Naming style matching correct class attribute names. +class-attribute-naming-style=any -# Put messages in a separate file for each module / package specified on the -# command line instead of printing them on stdout. Reports (if any) will be -# written in a file name "pylint_global.[txt|html]". This option is deprecated -# and it will be removed in Pylint 2.0. -files-output=no +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. If left empty, class attribute names will be checked +# with the set naming style. +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ -# Tells whether to display a full report or only the messages -reports=yes +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. If left empty, class constant names will be checked with +# the set naming style. +#class-const-rgx= -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details -#msg-template= +# Naming style matching correct class names. +class-naming-style=PascalCase +# Regular expression matching correct class names. Overrides class-naming- +# style. If left empty, class names will be checked with the set naming style. +class-rgx=[A-Z_][a-zA-Z0-9]+$ -[BASIC] +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. If left empty, constant names will be checked with the set naming +# style. +const-rgx=(([A-Za-z_][A-Za-z0-9_]*)|(__.*__))$ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. If left empty, function names will be checked with the set +# naming style. +function-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Good variable names which should always be accepted, separated by a comma. +good-names=e,i,j,k,ex,Run,_ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no -# Good variable names which should always be accepted, separated by a comma -good-names=e,i,j,k,v,f,ex,_ +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. If left empty, inline iteration names will be checked +# with the set naming style. +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. If left empty, method names will be checked with the set naming style. +method-rgx=[a-z_][a-z0-9_]{2,30}$ -# Bad variable names which should always be refused, separated by a comma -bad-names=foo,bar,baz,toto,tutu,tata +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. If left empty, module names will be checked with the set naming style. +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ # Colon-delimited sets of names that determine each other's naming style when # the name regexes allow several styles. name-group= -# Include a hint for the correct naming format with invalid-name -include-naming-hint=no +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ # List of decorators that produce properties, such as abc.abstractproperty. Add # to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. property-classes=abc.abstractproperty -# Regular expression matching correct class names -class-rgx=[A-Z_][a-zA-Z0-9]+$ +# Regular expression matching correct type variable names. If left empty, type +# variable names will be checked with the set naming style. +#typevar-rgx= -# Naming hint for class names -class-name-hint=[A-Z_][a-zA-Z0-9]+$ +# Naming style matching correct variable names. +variable-naming-style=snake_case -# Regular expression matching correct variable names +# Regular expression matching correct variable names. Overrides variable- +# naming-style. If left empty, variable names will be checked with the set +# naming style. variable-rgx=[a-z_][a-z0-9_]{2,30}$ -# Naming hint for variable names -variable-name-hint=[a-z_][a-z0-9_]{2,30}$ -# Regular expression matching correct constant names -const-rgx=(([A-Za-z_][A-Za-z0-9_]*)|(__.*__))$ +[CLASSES] -# Naming hint for constant names -const-name-hint=(([A-Za-z_][A-Za-z0-9_]*)|(__.*__))$ +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no -# Regular expression matching correct inline iteration names -inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, -# Naming hint for inline iteration names -inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls -# Regular expression matching correct class attribute names -class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs -# Naming hint for class attribute names -class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ -# Regular expression matching correct attribute names -attr-rgx=[a-z_][a-z0-9_]{2,30}$ +[DESIGN] -# Naming hint for attribute names -attr-name-hint=[a-z_][a-z0-9_]{2,30}$ +# List of regular expressions of class ancestor names to ignore when counting +# public methods (see R0903) +exclude-too-few-public-methods= -# Regular expression matching correct function names -function-rgx=[a-z_][a-z0-9_]{2,30}$ +# List of qualified class names to ignore when counting class parents (see +# R0901) +ignored-parents= -# Naming hint for function names -function-name-hint=[a-z_][a-z0-9_]{2,30}$ +# Maximum number of arguments for function / method. +max-args=5 -# Regular expression matching correct argument names -argument-rgx=[a-z_][a-z0-9_]{2,30}$ +# Maximum number of attributes for a class (see R0902). +max-attributes=7 -# Naming hint for argument names -argument-name-hint=[a-z_][a-z0-9_]{2,30}$ +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 -# Regular expression matching correct method names -method-rgx=[a-z_][a-z0-9_]{2,30}$ +# Maximum number of branch for function / method body. +max-branches=12 -# Naming hint for method names -method-name-hint=[a-z_][a-z0-9_]{2,30}$ +# Maximum number of locals for function / method body. +max-locals=15 -# Regular expression matching correct module names -module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ +# Maximum number of parents for a class (see R0901). +max-parents=7 -# Naming hint for module names -module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ +# Maximum number of return / yield for function / method body. +max-returns=6 -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + +# Maximum cyclomatic complexity +max-complexity=10 +# Maximum positional arguments +max-positional-arguments=12 -[ELIF] +[EXCEPTIONS] -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 +# Exceptions that will emit a warning when caught. +overgeneral-exceptions=builtins.BaseException, + builtins.Exception [FORMAT] -# Maximum number of characters on a single line. -max-line-length=160 +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= # Regexp for a line that is allowed to be longer than the limit. ignore-long-lines=^\s*(# )??$ +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=160 + +# Maximum number of lines in a module. +max-module-lines=1200 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + # Allow the body of an if to be on the same line as the test if there is no # else. single-line-if-stmt=no -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma,dict-separator +[IMPORTS] -# Maximum number of lines in a module -max-module-lines=1200 +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules= -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= -string-quote=double-avoid-escape -triple-quote=double -docstring-quote=double +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= [LOGGING] +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + # Logging modules to check that the string format arguments are in logging -# function parameter format +# function parameter format. logging-modules=logging -[MISCELLANEOUS] +[MESSAGES CONTROL] -# List of note tags to take in consideration, separated by a comma. -notes=FIXME,XXX,TODO +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, +# UNDEFINED. +confidence= +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then re-enable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable= + too-many-arguments, + too-many-instance-attributes, + too-few-public-methods, + missing-docstring, + invalid-name, + inconsistent-return-statements, + duplicate-code +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member,useless-suppression -[SIMILARITIES] -# Minimum lines number of a similarity. -min-similarity-lines=4 +[METHOD_ARGS] -# Ignore comments when computing similarities. -ignore-comments=yes +# List of qualified names (i.e., library.method) which require a timeout +# parameter e.g. 'requests.api.get,requests.api.post' +timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request -# Ignore docstrings when computing similarities. -ignore-docstrings=yes -# Ignore imports when computing similarities. -ignore-imports=yes +[MISCELLANEOUS] +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO -[SPELLING] +# Regular expression of note tags to take in consideration. +notes-rgx= -# Spelling dictionary name. Available dictionaries: none. To make it working -# install python-enchant package. -spelling-dict= -# List of comma separated words that should not be checked. -spelling-ignore-words= +[REFACTORING] -# A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file= +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 -# Tells whether to store unknown words to indicated private dictionary in -# --spelling-private-dict-file option instead of raising a message. -spelling-store-unknown-words=no +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error -[TYPECHECK] +[REPORTS] -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'fatal', 'error', 'warning', 'refactor', +# 'convention', and 'info' which contain the number of messages in each +# category, as well as 'statement' which is the total number of statements +# analyzed. This score is used by the global evaluation report (RP0004). +evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis. It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +msg-template= -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= +# Tells whether to display a full report or only the messages. +reports=yes -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager +# Activate the evaluation score. +score=yes -[VARIABLES] +[SIMILARITIES] -# Tells whether we should check for unused import in __init__ files. -init-import=no +# Comments are removed from the similarity computation +ignore-comments=yes -# A regular expression matching the name of dummy variables (i.e. expectedly -# not used). -dummy-variables-rgx=(_+[a-zA-Z0-9]*?$)|dummy +# Docstrings are removed from the similarity computation +ignore-docstrings=yes -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid to define new builtins when possible. -additional-builtins= +# Imports are removed from the similarity computation +ignore-imports=yes -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_,_cb +# Signatures are removed from the similarity computation +ignore-signatures=yes -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,future.builtins +# Minimum lines number of a similarity. +min-similarity-lines=4 -[CLASSES] +[SPELLING] -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__,__new__,setUp +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls +# Spelling dictionary name. Available dictionaries: none. To make it work, +# install the 'python-enchant' package. +spelling-dict= -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=mcs +# List of comma separated words that should be considered directives if they +# appear at the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict,_fields,_replace,_source,_make +# List of comma separated words that should not be checked. +spelling-ignore-words= +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= -[DESIGN] +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no -# Maximum number of arguments for function / method -max-args=5 -# Argument names that match this expression will be ignored. Default to name -# with leading underscore -ignored-argument-names=_.* +[STRING] -# Maximum number of locals for function / method body -max-locals=15 +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no -# Maximum number of return / yield for function / method body -max-returns=6 +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no -# Maximum number of branch for function / method body -max-branches=12 -# Maximum number of statements in function / method body -max-statements=50 +[TYPECHECK] +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members = true -# Maximum number of parents for a class (see R0901). -max-parents=7 +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager -# Maximum number of attributes for a class (see R0902). -max-attributes=7 +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes -# Maximum number of boolean expressions in a if statement -max-bool-expr=5 +# List of symbolic message names to ignore for Mixin members. +ignored-checks-for-mixins=no-member, + not-async-context-manager, + not-context-manager, + attribute-defined-outside-init -# Maximum cyclomatic complexity -max-complexity=10 +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local -[IMPORTS] +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes -# Deprecated modules which should not be used, separated by a comma -deprecated-modules=optparse +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled) -import-graph= +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled) -ext-import-graph= +# Regex pattern to define which classes are considered mixins. +mixin-class-rgx=.*[Mm]ixin -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled) -int-import-graph= +# List of decorators that change the signature of a decorated function. +signature-mutators= -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant +[VARIABLES] -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes -[EXCEPTIONS] +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb -# Exceptions that will emit a warning when being caught. Defaults to -# "Exception" -overgeneral-exceptions=Exception +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=(_+[a-zA-Z0-9]*?$)|dummy + +# Argument names that match this expression will be ignored. +ignored-argument-names=_.* + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,future.builtins diff --git a/.python-version b/.python-version index f69abe410a..35f236d6e5 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.9.7 +3.12.6 diff --git a/.schemas-version b/.schemas-version index 6d260c3af0..2ccc90f1e6 100644 --- a/.schemas-version +++ b/.schemas-version @@ -1 +1 @@ -v3.2.0 +v5.39.0 diff --git a/.yarnrc b/.yarnrc deleted file mode 100644 index 075332deec..0000000000 --- a/.yarnrc +++ /dev/null @@ -1 +0,0 @@ -strict-ssl true diff --git a/Dockerfile b/Dockerfile index 2d43cc7d2c..38263fc195 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9-slim-bullseye +FROM python:3.12-slim-bookworm EXPOSE 5000 @@ -14,11 +14,14 @@ ENV WEB_SERVER_UWSGI_ASYNC_CORES 10 ENV HTTP_KEEP_ALIVE 2 ENV GUNICORN_CMD_ARGS -c gunicorn_config.py -COPY Pipfile Pipfile -COPY Pipfile.lock Pipfile.lock +COPY pyproject.toml pyproject.toml +COPY poetry.lock poetry.lock RUN groupadd -r appuser && useradd -r -g appuser -u 9000 appuser && chown -R appuser:appuser . -RUN pip install pipenv==2018.11.26 && pipenv install --deploy --system && make build +RUN pip install "poetry==2.1.2" && \ + poetry config virtualenvs.create false && \ + poetry install --only main && \ + make build USER appuser diff --git a/Makefile b/Makefile index 3f522ef5e2..bb9da1f6f6 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ SCHEMAS_VERSION=`cat .schemas-version` DESIGN_SYSTEM_VERSION=`cat .design-system-version` RUNNER_ENV_FILE?=.development.env +SCHEMA_PATH=./schemas/test/en/ clean: find schemas/* -prune | grep -v "schemas/test" | xargs rm -r @@ -16,76 +17,106 @@ load-design-system-templates: build: load-design-system-templates load-schemas translate -lint: lint-python - yarn lint +generate-pages: + npm run generate_pages + +lint: lint-python lint-js lint-html + +lint-html: + poetry run djlint ./templates --profile=jinja lint-python: - pipenv run ./scripts/run_lint_python.sh + poetry run ./scripts/run_lint_python.sh + +lint-test-python: lint-python test-unit + +format: format-python format-js format-html -format: format-python - yarn format +format-html: + poetry run djlint ./templates --reformat --profile=jinja format-python: - pipenv run isort . - pipenv run black . + poetry run isort . + poetry run black . test: - pipenv run ./scripts/run_tests.sh + poetry run ./scripts/run_tests.sh test-unit: - pipenv run ./scripts/run_tests_unit.sh + poetry run ./scripts/run_tests_unit.sh + +test-functional: generate-pages + npm run test_functional + +test-functional-headless: generate-pages + EQ_RUN_FUNCTIONAL_TESTS_HEADLESS='True' make test-functional + +test-functional-spec: generate-pages + npm run test_functional -- --spec=./tests/functional/spec/$(SPEC) -test-functional: - pipenv run ./scripts/run_tests_functional.sh +test-functional-suite: generate-pages + npm run test_functional -- --suite=$(SUITE) + +lint-js: + npm run lint + +format-js: + npm run format + +generate-spec: + poetry run python -m tests.functional.generate_pages schemas/test/en/$(SCHEMA).json ./tests/functional/generated_pages/$(patsubst test_%,%,$(SCHEMA)) -r '../../base_pages' -s tests/functional/spec/$(SCHEMA).spec.js validate-test-schemas: - pipenv run ./scripts/validate_test_schemas.sh + poetry run python -m scripts.validate_test_schemas + +validate-test-schema: + poetry run python -m scripts.validate_test_schemas $(SCHEMA_PATH)$(SCHEMA).json translation-templates: - pipenv run python -m scripts.extract_translation_templates + poetry run python -m scripts.extract_translation_templates test-translation-templates: - pipenv run python -m scripts.extract_translation_templates --test + poetry run python -m scripts.extract_translation_templates --test translate: - pipenv run pybabel compile -d app/translations + poetry run pybabel compile -d app/translations run-validator: - pipenv run ./scripts/run_validator.sh + poetry run ./scripts/run_validator.sh link-development-env: ln -sf $(RUNNER_ENV_FILE) .env run: build link-development-env - pipenv run flask run + poetry run flask run run-gunicorn-async: link-development-env - WEB_SERVER_TYPE=gunicorn-async pipenv run ./run_app.sh + WEB_SERVER_TYPE=gunicorn-async poetry run ./run_app.sh run-gunicorn-threads: link-development-env - WEB_SERVER_TYPE=gunicorn-threads pipenv run ./run_app.sh + WEB_SERVER_TYPE=gunicorn-threads poetry run ./run_app.sh run-uwsgi: link-development-env - WEB_SERVER_TYPE=uwsgi pipenv run ./run_app.sh + WEB_SERVER_TYPE=uwsgi poetry run ./run_app.sh run-uwsgi-threads: link-development-env - WEB_SERVER_TYPE=uwsgi-threads pipenv run ./run_app.sh + WEB_SERVER_TYPE=uwsgi-threads poetry run ./run_app.sh run-uwsgi-async: link-development-env - WEB_SERVER_TYPE=uwsgi-async pipenv run ./run_app.sh + WEB_SERVER_TYPE=uwsgi-async poetry run ./run_app.sh dev-compose-up: - docker-compose -f docker-compose-dev-mac.yml pull eq-questionnaire-launcher - docker-compose -f docker-compose-dev-mac.yml up -d - -dev-compose-up-linux: - docker-compose -f docker-compose-dev-linux.yml up -d + docker compose -f docker-compose-dev.yml pull eq-questionnaire-launcher + docker compose -f docker-compose-dev.yml pull sds + docker compose -f docker-compose-dev.yml pull cir + docker compose -f docker-compose-dev.yml up -d dev-compose-down: - docker-compose -f docker-compose-dev-mac.yml down - -dev-compose-down-linux: - docker-compose -f docker-compose-dev-linux.yml down + docker compose -f docker-compose-dev.yml down profile: - pipenv run python profile_application.py + poetry run python profile_application.py + +generate-integration-test: + poetry run python -m scripts.generate_integration_test + poetry run black ./scripts/test_* diff --git a/Pipfile b/Pipfile deleted file mode 100644 index 2730b7a630..0000000000 --- a/Pipfile +++ /dev/null @@ -1,80 +0,0 @@ -[[source]] -url = "https://pypi.python.org/simple" -name = "pypi" -verify_ssl = true - -[dev-packages] -"pep8" = "*" -mock = "*" -pytest-cov = "*" -jsonschema = "*" -pylint = "*" -pylint-mccabe = "*" -pylint-quotes = "*" -"beautifulsoup4" = "*" -httmock = "*" -"flake8" = "*" -"flake8-debugger" = "*" -"flake8-mock" = "*" -"flake8-print" = "*" -"flake8-tuple" = "*" -"flake8-datetimez" = "*" -moto = "*" -freezegun = "*" -pytest-xdist = "*" -fakeredis = "*" -mypy = "*" -black = "==20.8b1" -pytest-flask = "*" -pytest = "*" -pytest-sugar = "*" -responses = "*" -types-simplejson = "*" -types-requests = "*" -types-redis = "*" -types-PyYAML = "*" -types-python-dateutil = "*" -pytest-mock = "*" - -[packages] -colorama = "*" -flask = "*" -flask-babel = "*" -flask-login = "*" -flask-wtf = "*" -gevent = {version = "*",platform_python_implementation = "=='CPython'"} -google-cloud-datastore = "*" -grpcio = "*" -gunicorn = "*" -pika = "*" -pyyaml = "*" -requests = "*" -sdc-cryptography = "*" -structlog = "*" -ua-parser = "*" -blinker = "*" -"boto3" = "*" -humanize = "*" -flask-talisman = "*" -marshmallow = "*" -python-snappy = "*" -google-cloud-storage = "*" -jsonpointer = "*" -redis = "*" -flask-compress = "*" -htmlmin = "*" -coloredlogs = "*" -uwsgi = "*" -email-validator = "*" -itsdangerous = "*" -google-cloud-pubsub = "*" -google-cloud-tasks = "*" -simplejson = "*" -markupsafe = "*" -pdfkit = "*" - -[requires] -python_version = "3.9" - -[pipenv] -allow_prereleases = false diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index e81d05639f..0000000000 --- a/Pipfile.lock +++ /dev/null @@ -1,2346 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "ffc2eb50dd1b342d1de3c59d51d23f7412d379909ad25adf8308856a48e8ea74" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.9" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.python.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "babel": { - "hashes": [ - "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9", - "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.9.1" - }, - "blinker": { - "hashes": [ - "sha256:471aee25f3992bd325afa3772f1063dbdbbca947a041b8b89466dc00d606f8b6" - ], - "index": "pypi", - "version": "==1.4" - }, - "boto3": { - "hashes": [ - "sha256:0e8d4d814f94031947035a4c2bb2c23832d5de941a6a492fb85794a02bafc44d", - "sha256:95d9b5b6fe3383fbf8f33d58f62258d3b3ea138d4369017031339b60fd5b8887" - ], - "index": "pypi", - "version": "==1.21.6" - }, - "botocore": { - "hashes": [ - "sha256:359b9ea3870a1f8264113cb0b1216baa94bf1e8cee5d5d8af63a2e7ca6e7b33c", - "sha256:69aaa5a78ac7371f573e463be51fb962213c42fab08ef82380e84b9a87386949" - ], - "markers": "python_version >= '3.6'", - "version": "==1.24.6" - }, - "brotli": { - "hashes": [ - "sha256:12effe280b8ebfd389022aa65114e30407540ccb89b177d3fbc9a4f177c4bd5d", - "sha256:160c78292e98d21e73a4cc7f76a234390e516afcd982fa17e1422f7c6a9ce9c8", - "sha256:16d528a45c2e1909c2798f27f7bf0a3feec1dc9e50948e738b961618e38b6a7b", - "sha256:19598ecddd8a212aedb1ffa15763dd52a388518c4550e615aed88dc3753c0f0c", - "sha256:1c48472a6ba3b113452355b9af0a60da5c2ae60477f8feda8346f8fd48e3e87c", - "sha256:268fe94547ba25b58ebc724680609c8ee3e5a843202e9a381f6f9c5e8bdb5c70", - "sha256:269a5743a393c65db46a7bb982644c67ecba4b8d91b392403ad8a861ba6f495f", - "sha256:26d168aac4aaec9a4394221240e8a5436b5634adc3cd1cdf637f6645cecbf181", - "sha256:29d1d350178e5225397e28ea1b7aca3648fcbab546d20e7475805437bfb0a130", - "sha256:2aad0e0baa04517741c9bb5b07586c642302e5fb3e75319cb62087bd0995ab19", - "sha256:3496fc835370da351d37cada4cf744039616a6db7d13c430035e901443a34daa", - "sha256:35a3edbe18e876e596553c4007a087f8bcfd538f19bc116917b3c7522fca0429", - "sha256:3b78a24b5fd13c03ee2b7b86290ed20efdc95da75a3557cc06811764d5ad1126", - "sha256:40d15c79f42e0a2c72892bf407979febd9cf91f36f495ffb333d1d04cebb34e4", - "sha256:44bb8ff420c1d19d91d79d8c3574b8954288bdff0273bf788954064d260d7ab0", - "sha256:4688c1e42968ba52e57d8670ad2306fe92e0169c6f3af0089be75bbac0c64a3b", - "sha256:495ba7e49c2db22b046a53b469bbecea802efce200dffb69b93dd47397edc9b6", - "sha256:4d1b810aa0ed773f81dceda2cc7b403d01057458730e309856356d4ef4188438", - "sha256:503fa6af7da9f4b5780bb7e4cbe0c639b010f12be85d02c99452825dd0feef3f", - "sha256:56d027eace784738457437df7331965473f2c0da2c70e1a1f6fdbae5402e0389", - "sha256:5913a1177fc36e30fcf6dc868ce23b0453952c78c04c266d3149b3d39e1410d6", - "sha256:5b6ef7d9f9c38292df3690fe3e302b5b530999fa90014853dcd0d6902fb59f26", - "sha256:5cb1e18167792d7d21e21365d7650b72d5081ed476123ff7b8cac7f45189c0c7", - "sha256:61a7ee1f13ab913897dac7da44a73c6d44d48a4adff42a5701e3239791c96e14", - "sha256:622a231b08899c864eb87e85f81c75e7b9ce05b001e59bbfbf43d4a71f5f32b2", - "sha256:68715970f16b6e92c574c30747c95cf8cf62804569647386ff032195dc89a430", - "sha256:6b2ae9f5f67f89aade1fab0f7fd8f2832501311c363a21579d02defa844d9296", - "sha256:6c772d6c0a79ac0f414a9f8947cc407e119b8598de7621f39cacadae3cf57d12", - "sha256:6d847b14f7ea89f6ad3c9e3901d1bc4835f6b390a9c71df999b0162d9bb1e20f", - "sha256:76ffebb907bec09ff511bb3acc077695e2c32bc2142819491579a695f77ffd4d", - "sha256:7bbff90b63328013e1e8cb50650ae0b9bac54ffb4be6104378490193cd60f85a", - "sha256:7cb81373984cc0e4682f31bc3d6be9026006d96eecd07ea49aafb06897746452", - "sha256:7ee83d3e3a024a9618e5be64648d6d11c37047ac48adff25f12fa4226cf23d1c", - "sha256:854c33dad5ba0fbd6ab69185fec8dab89e13cda6b7d191ba111987df74f38761", - "sha256:85f7912459c67eaab2fb854ed2bc1cc25772b300545fe7ed2dc03954da638649", - "sha256:87fdccbb6bb589095f413b1e05734ba492c962b4a45a13ff3408fa44ffe6479b", - "sha256:88c63a1b55f352b02c6ffd24b15ead9fc0e8bf781dbe070213039324922a2eea", - "sha256:8a674ac10e0a87b683f4fa2b6fa41090edfd686a6524bd8dedbd6138b309175c", - "sha256:93130612b837103e15ac3f9cbacb4613f9e348b58b3aad53721d92e57f96d46a", - "sha256:9744a863b489c79a73aba014df554b0e7a0fc44ef3f8a0ef2a52919c7d155031", - "sha256:9749a124280a0ada4187a6cfd1ffd35c350fb3af79c706589d98e088c5044267", - "sha256:97f715cf371b16ac88b8c19da00029804e20e25f30d80203417255d239f228b5", - "sha256:9bf919756d25e4114ace16a8ce91eb340eb57a08e2c6950c3cebcbe3dff2a5e7", - "sha256:9d12cf2851759b8de8ca5fde36a59c08210a97ffca0eb94c532ce7b17c6a3d1d", - "sha256:9ed4c92a0665002ff8ea852353aeb60d9141eb04109e88928026d3c8a9e5433c", - "sha256:a72661af47119a80d82fa583b554095308d6a4c356b2a554fdc2799bc19f2a43", - "sha256:afde17ae04d90fbe53afb628f7f2d4ca022797aa093e809de5c3cf276f61bbfa", - "sha256:b336c5e9cf03c7be40c47b5fd694c43c9f1358a80ba384a21969e0b4e66a9b17", - "sha256:b663f1e02de5d0573610756398e44c130add0eb9a3fc912a09665332942a2efb", - "sha256:b83bb06a0192cccf1eb8d0a28672a1b79c74c3a8a5f2619625aeb6f28b3a82bb", - "sha256:c2415d9d082152460f2bd4e382a1e85aed233abc92db5a3880da2257dc7daf7b", - "sha256:c83aa123d56f2e060644427a882a36b3c12db93727ad7a7b9efd7d7f3e9cc2c4", - "sha256:cfc391f4429ee0a9370aa93d812a52e1fee0f37a81861f4fdd1f4fb28e8547c3", - "sha256:db844eb158a87ccab83e868a762ea8024ae27337fc7ddcbfcddd157f841fdfe7", - "sha256:defed7ea5f218a9f2336301e6fd379f55c655bea65ba2476346340a0ce6f74a1", - "sha256:e16eb9541f3dd1a3e92b89005e37b1257b157b7256df0e36bd7b33b50be73bcb", - "sha256:e23281b9a08ec338469268f98f194658abfb13658ee98e2b7f85ee9dd06caa91", - "sha256:e2d9e1cbc1b25e22000328702b014227737756f4b5bf5c485ac1d8091ada078b", - "sha256:e48f4234f2469ed012a98f4b7874e7f7e173c167bed4934912a29e03167cf6b1", - "sha256:e4c4e92c14a57c9bd4cb4be678c25369bf7a092d55fd0866f759e425b9660806", - "sha256:ec1947eabbaf8e0531e8e899fc1d9876c179fc518989461f5d24e2223395a9e3", - "sha256:f909bbbc433048b499cb9db9e713b5d8d949e8c109a2a548502fb9aa8630f0b1" - ], - "version": "==1.0.9" - }, - "cachetools": { - "hashes": [ - "sha256:486471dfa8799eb7ec503a8059e263db000cdda20075ce5e48903087f79d5fd6", - "sha256:8fecd4203a38af17928be7b90689d8083603073622229ca7077b72d8e5a976e4" - ], - "markers": "python_version ~= '3.7'", - "version": "==5.0.0" - }, - "certifi": { - "hashes": [ - "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", - "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" - ], - "version": "==2021.10.8" - }, - "cffi": { - "hashes": [ - "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3", - "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2", - "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636", - "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20", - "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728", - "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27", - "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66", - "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443", - "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0", - "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7", - "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39", - "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605", - "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a", - "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37", - "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029", - "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139", - "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc", - "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df", - "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14", - "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880", - "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2", - "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a", - "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e", - "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474", - "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024", - "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8", - "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0", - "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e", - "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a", - "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e", - "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032", - "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6", - "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e", - "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b", - "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e", - "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954", - "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962", - "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c", - "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4", - "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55", - "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962", - "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023", - "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c", - "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6", - "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8", - "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382", - "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7", - "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc", - "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997", - "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796" - ], - "version": "==1.15.0" - }, - "charset-normalizer": { - "hashes": [ - "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", - "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" - ], - "markers": "python_version >= '3'", - "version": "==2.0.12" - }, - "click": { - "hashes": [ - "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1", - "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb" - ], - "markers": "python_version >= '3.6'", - "version": "==8.0.4" - }, - "colorama": { - "hashes": [ - "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", - "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" - ], - "index": "pypi", - "version": "==0.4.4" - }, - "coloredlogs": { - "hashes": [ - "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", - "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0" - ], - "index": "pypi", - "version": "==15.0.1" - }, - "cryptography": { - "hashes": [ - "sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3", - "sha256:2d87cdcb378d3cfed944dac30596da1968f88fb96d7fc34fdae30a99054b2e31", - "sha256:30ee1eb3ebe1644d1c3f183d115a8c04e4e603ed6ce8e394ed39eea4a98469ac", - "sha256:391432971a66cfaf94b21c24ab465a4cc3e8bf4a939c1ca5c3e3a6e0abebdbcf", - "sha256:39bdf8e70eee6b1c7b289ec6e5d84d49a6bfa11f8b8646b5b3dfe41219153316", - "sha256:4caa4b893d8fad33cf1964d3e51842cd78ba87401ab1d2e44556826df849a8ca", - "sha256:53e5c1dc3d7a953de055d77bef2ff607ceef7a2aac0353b5d630ab67f7423638", - "sha256:596f3cd67e1b950bc372c33f1a28a0692080625592ea6392987dba7f09f17a94", - "sha256:5d59a9d55027a8b88fd9fd2826c4392bd487d74bf628bb9d39beecc62a644c12", - "sha256:6c0c021f35b421ebf5976abf2daacc47e235f8b6082d3396a2fe3ccd537ab173", - "sha256:73bc2d3f2444bcfeac67dd130ff2ea598ea5f20b40e36d19821b4df8c9c5037b", - "sha256:74d6c7e80609c0f4c2434b97b80c7f8fdfaa072ca4baab7e239a15d6d70ed73a", - "sha256:7be0eec337359c155df191d6ae00a5e8bbb63933883f4f5dffc439dac5348c3f", - "sha256:94ae132f0e40fe48f310bba63f477f14a43116f05ddb69d6fa31e93f05848ae2", - "sha256:bb5829d027ff82aa872d76158919045a7c1e91fbf241aec32cb07956e9ebd3c9", - "sha256:ca238ceb7ba0bdf6ce88c1b74a87bffcee5afbfa1e41e173b1ceb095b39add46", - "sha256:ca28641954f767f9822c24e927ad894d45d5a1e501767599647259cbf030b903", - "sha256:e0344c14c9cb89e76eb6a060e67980c9e35b3f36691e15e1b7a9e58a0a6c6dc3", - "sha256:ebc15b1c22e55c4d5566e3ca4db8689470a0ca2babef8e3a9ee057a8b82ce4b1", - "sha256:ec63da4e7e4a5f924b90af42eddf20b698a70e58d86a72d943857c4c6045b3ee" - ], - "markers": "python_version >= '3.6'", - "version": "==36.0.1" - }, - "deprecated": { - "hashes": [ - "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d", - "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.2.13" - }, - "dnspython": { - "hashes": [ - "sha256:081649da27ced5e75709a1ee542136eaba9842a0fe4c03da4fb0a3d3ed1f3c44", - "sha256:e79351e032d0b606b98d38a4b0e6e2275b31a5b85c873e587cc11b73aca026d6" - ], - "markers": "python_version >= '3.6' and python_version < '4.0'", - "version": "==2.2.0" - }, - "email-validator": { - "hashes": [ - "sha256:5675c8ceb7106a37e40e2698a57c056756bf3f272cfa8682a4f87ebd95d8440b", - "sha256:aa237a65f6f4da067119b7df3f13e89c25c051327b2b5b66dc075f33d62480d7" - ], - "index": "pypi", - "version": "==1.1.3" - }, - "flask": { - "hashes": [ - "sha256:59da8a3170004800a2837844bfa84d49b022550616070f7cb1a659682b2e7c9f", - "sha256:e1120c228ca2f553b470df4a5fa927ab66258467526069981b3eb0a91902687d" - ], - "index": "pypi", - "version": "==2.0.3" - }, - "flask-babel": { - "hashes": [ - "sha256:e6820a052a8d344e178cdd36dd4bb8aea09b4bda3d5f9fa9f008df2c7f2f5468", - "sha256:f9faf45cdb2e1a32ea2ec14403587d4295108f35017a7821a2b1acb8cfd9257d" - ], - "index": "pypi", - "version": "==2.0.0" - }, - "flask-compress": { - "hashes": [ - "sha256:28352387efbbe772cfb307570019f81957a13ff718d994a9125fa705efb73680", - "sha256:a6c2d1ff51771e9b39d7a612754f4cb4e8af20cebe16b02fd19d98d8dd6966e5" - ], - "index": "pypi", - "version": "==1.10.1" - }, - "flask-login": { - "hashes": [ - "sha256:6d33aef15b5bcead780acc339464aae8a6e28f13c90d8b1cf9de8b549d1c0b4b", - "sha256:7451b5001e17837ba58945aead261ba425fdf7b4f0448777e597ddab39f4fba0" - ], - "index": "pypi", - "version": "==0.5.0" - }, - "flask-talisman": { - "hashes": [ - "sha256:08a25360c771f7a79d6d4db2abfa71f7422e62a714418b671d69d6a201764d05", - "sha256:5d502ec0c51bf1755a797b8cffbe4e94f8684af712ba0b56f3d80b79277ef285" - ], - "index": "pypi", - "version": "==0.8.1" - }, - "flask-wtf": { - "hashes": [ - "sha256:01feccfc395405cea48a3f36c23f0d766e2cc6fd2a5a065ad50ad3e5827ec797", - "sha256:872fbb17b5888bfc734edbdcf45bc08fb365ca39f69d25dc752465a455517b28" - ], - "index": "pypi", - "version": "==1.0.0" - }, - "gevent": { - "hashes": [ - "sha256:0082d8a5d23c35812ce0e716a91ede597f6dd2c5ff508a02a998f73598c59397", - "sha256:01928770972181ad8866ee37ea3504f1824587b188fcab782ef1619ce7538766", - "sha256:05c5e8a50cd6868dd36536c92fb4468d18090e801bd63611593c0717bab63692", - "sha256:08b4c17064e28f4eb85604486abc89f442c7407d2aed249cf54544ce5c9baee6", - "sha256:177f93a3a90f46a5009e0841fef561601e5c637ba4332ab8572edd96af650101", - "sha256:22ce1f38fdfe2149ffe8ec2131ca45281791c1e464db34b3b4321ae9d8d2efbb", - "sha256:24d3550fbaeef5fddd794819c2853bca45a86c3d64a056a2c268d981518220d1", - "sha256:2afa3f3ad528155433f6ac8bd64fa5cc303855b97004416ec719a6b1ca179481", - "sha256:2bcec9f80196c751fdcf389ca9f7141e7b0db960d8465ed79be5e685bfcad682", - "sha256:2cfff82f05f14b7f5d9ed53ccb7a609ae8604df522bb05c971bca78ec9d8b2b9", - "sha256:3baeeccc4791ba3f8db27179dff11855a8f9210ddd754f6c9b48e0d2561c2aea", - "sha256:3c012c73e6c61f13c75e3a4869dbe6a2ffa025f103421a6de9c85e627e7477b1", - "sha256:3dad62f55fad839d498c801e139481348991cee6e1c7706041b5fe096cb6a279", - "sha256:542ae891e2aa217d2cf6d8446538fcd2f3263a40eec123b970b899bac391c47a", - "sha256:6a02a88723ed3f0fd92cbf1df3c4cd2fbd87d82b0a4bac3e36a8875923115214", - "sha256:74fc1ef16b86616cfddcc74f7292642b0f72dde4dd95aebf4c45bb236744be54", - "sha256:7909780f0cf18a1fc32aafd8c8e130cdd93c6e285b11263f7f2d1a0f3678bc50", - "sha256:7ccffcf708094564e442ac6fde46f0ae9e40015cb69d995f4b39cc29a7643881", - "sha256:8c21cb5c9f4e14d75b3fe0b143ec875d7dbd1495fad6d49704b00e57e781ee0f", - "sha256:973749bacb7bc4f4181a8fb2a7e0e2ff44038de56d08e856dd54a5ac1d7331b4", - "sha256:9d86438ede1cbe0fde6ef4cc3f72bf2f1ecc9630d8b633ff344a3aeeca272cdd", - "sha256:9f9652d1e4062d4b5b5a0a49ff679fa890430b5f76969d35dccb2df114c55e0f", - "sha256:a5ad4ed8afa0a71e1927623589f06a9b5e8b5e77810be3125cb4d93050d3fd1f", - "sha256:b7709c64afa8bb3000c28bb91ec42c79594a7cb0f322e20427d57f9762366a5b", - "sha256:bb5cb8db753469c7a9a0b8a972d2660fe851aa06eee699a1ca42988afb0aaa02", - "sha256:c43f081cbca41d27fd8fef9c6a32cf83cb979345b20abc07bf68df165cdadb24", - "sha256:cc2fef0f98ee180704cf95ec84f2bc2d86c6c3711bb6b6740d74e0afe708b62c", - "sha256:da8d2d51a49b2a5beb02ad619ca9ddbef806ef4870ba04e5ac7b8b41a5b61db3", - "sha256:e1899b921219fc8959ff9afb94dae36be82e0769ed13d330a393594d478a0b3a", - "sha256:eae3c46f9484eaacd67ffcdf4eaf6ca830f587edd543613b0f5c4eb3c11d052d", - "sha256:ec21f9eaaa6a7b1e62da786132d6788675b314f25f98d9541f1bf00584ed4749", - "sha256:f289fae643a3f1c3b909d6b033e6921b05234a4907e9c9c8c3f1fe403e6ac452", - "sha256:f48b64578c367b91fa793bf8eaaaf4995cb93c8bc45860e473bf868070ad094e" - ], - "index": "pypi", - "markers": "platform_python_implementation == 'CPython'", - "version": "==21.12.0" - }, - "google-api-core": { - "extras": [ - "grpc" - ], - "hashes": [ - "sha256:7d030edbd3a0e994d796e62716022752684e863a6df9864b6ca82a1616c2a5a6", - "sha256:f33863a6709651703b8b18b67093514838c79f2b04d02aa501203079f24b8018" - ], - "markers": "python_version >= '3.6'", - "version": "==2.5.0" - }, - "google-auth": { - "hashes": [ - "sha256:218ca03d7744ca0c8b6697b6083334be7df49b7bf76a69d555962fd1a7657b5f", - "sha256:ad160fc1ea8f19e331a16a14a79f3d643d813a69534ba9611d2c80dc10439dad" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==2.6.0" - }, - "google-cloud-core": { - "hashes": [ - "sha256:7d19bf8868b410d0bdf5a03468a3f3f2db233c0ee86a023f4ecc2b7a4b15f736", - "sha256:d9cffaf86df6a876438d4e8471183bbe404c9a15de9afe60433bc7dce8cb4252" - ], - "markers": "python_version >= '3.6'", - "version": "==2.2.2" - }, - "google-cloud-datastore": { - "hashes": [ - "sha256:4a6f04112f2685a0a5cd8c7cb7946572bb7e0f6ca7cbe0088514006fca8594ca", - "sha256:4e10f3c080213a61b8d6869effc4b7d51aab2bc87f687020d0c6e0c2aceddd52" - ], - "index": "pypi", - "version": "==2.4.0" - }, - "google-cloud-pubsub": { - "hashes": [ - "sha256:2b3d9336afab0e5df67201234976519a28da3ccb7c9a0e463be28e2827a9fdaa", - "sha256:f303a0e8fba3b8d0beccc84d6282ae4864cf40c9cd9431ae7ca4a3191309a10c" - ], - "index": "pypi", - "version": "==2.9.0" - }, - "google-cloud-storage": { - "hashes": [ - "sha256:0a5e7ab1a38d2c24be8e566e50b8b0daa8af8fd49d4ab312b1fda5c147429893", - "sha256:53e4f6030d771526e4dbe9c6000cca44eab812f39ae8c7a810be999473e4f02c" - ], - "index": "pypi", - "version": "==2.1.0" - }, - "google-cloud-tasks": { - "hashes": [ - "sha256:2afdae14dc85fd4f931456ed02dff846b92673fa5b072b25d6fef4a9b3ebd37c", - "sha256:fe211ec5b070fe50610feec16b06981d0a4307df23c6a241e2192651a14724df" - ], - "index": "pypi", - "version": "==2.8.0" - }, - "google-crc32c": { - "hashes": [ - "sha256:04e7c220798a72fd0f08242bc8d7a05986b2a08a0573396187fd32c1dcdd58b3", - "sha256:05340b60bf05b574159e9bd940152a47d38af3fb43803ffe71f11d704b7696a6", - "sha256:12674a4c3b56b706153a358eaa1018c4137a5a04635b92b4652440d3d7386206", - "sha256:127f9cc3ac41b6a859bd9dc4321097b1a4f6aa7fdf71b4f9227b9e3ebffb4422", - "sha256:13af315c3a0eec8bb8b8d80b8b128cb3fcd17d7e4edafc39647846345a3f003a", - "sha256:1926fd8de0acb9d15ee757175ce7242e235482a783cd4ec711cc999fc103c24e", - "sha256:226f2f9b8e128a6ca6a9af9b9e8384f7b53a801907425c9a292553a3a7218ce0", - "sha256:276de6273eb074a35bc598f8efbc00c7869c5cf2e29c90748fccc8c898c244df", - "sha256:318f73f5484b5671f0c7f5f63741ab020a599504ed81d209b5c7129ee4667407", - "sha256:3bbce1be3687bbfebe29abdb7631b83e6b25da3f4e1856a1611eb21854b689ea", - "sha256:42ae4781333e331a1743445931b08ebdad73e188fd554259e772556fc4937c48", - "sha256:58be56ae0529c664cc04a9c76e68bb92b091e0194d6e3c50bea7e0f266f73713", - "sha256:5da2c81575cc3ccf05d9830f9e8d3c70954819ca9a63828210498c0774fda1a3", - "sha256:6311853aa2bba4064d0c28ca54e7b50c4d48e3de04f6770f6c60ebda1e975267", - "sha256:650e2917660e696041ab3dcd7abac160b4121cd9a484c08406f24c5964099829", - "sha256:6a4db36f9721fdf391646685ecffa404eb986cbe007a3289499020daf72e88a2", - "sha256:779cbf1ce375b96111db98fca913c1f5ec11b1d870e529b1dc7354b2681a8c3a", - "sha256:7f6fe42536d9dcd3e2ffb9d3053f5d05221ae3bbcefbe472bdf2c71c793e3183", - "sha256:891f712ce54e0d631370e1f4997b3f182f3368179198efc30d477c75d1f44942", - "sha256:95c68a4b9b7828ba0428f8f7e3109c5d476ca44996ed9a5f8aac6269296e2d59", - "sha256:96a8918a78d5d64e07c8ea4ed2bc44354e3f93f46a4866a40e8db934e4c0d74b", - "sha256:9c3cf890c3c0ecfe1510a452a165431b5831e24160c5fcf2071f0f85ca5a47cd", - "sha256:9f58099ad7affc0754ae42e6d87443299f15d739b0ce03c76f515153a5cda06c", - "sha256:a0b9e622c3b2b8d0ce32f77eba617ab0d6768b82836391e4f8f9e2074582bf02", - "sha256:a7f9cbea4245ee36190f85fe1814e2d7b1e5f2186381b082f5d59f99b7f11328", - "sha256:bab4aebd525218bab4ee615786c4581952eadc16b1ff031813a2fd51f0cc7b08", - "sha256:c124b8c8779bf2d35d9b721e52d4adb41c9bfbde45e6a3f25f0820caa9aba73f", - "sha256:c9da0a39b53d2fab3e5467329ed50e951eb91386e9d0d5b12daf593973c3b168", - "sha256:ca60076c388728d3b6ac3846842474f4250c91efbfe5afa872d3ffd69dd4b318", - "sha256:cb6994fff247987c66a8a4e550ef374671c2b82e3c0d2115e689d21e511a652d", - "sha256:d1c1d6236feab51200272d79b3d3e0f12cf2cbb12b208c835b175a21efdb0a73", - "sha256:dd7760a88a8d3d705ff562aa93f8445ead54f58fd482e4f9e2bafb7e177375d4", - "sha256:dda4d8a3bb0b50f540f6ff4b6033f3a74e8bf0bd5320b70fab2c03e512a62812", - "sha256:e0f1ff55dde0ebcfbef027edc21f71c205845585fffe30d4ec4979416613e9b3", - "sha256:e7a539b9be7b9c00f11ef16b55486141bc2cdb0c54762f84e3c6fc091917436d", - "sha256:eb0b14523758e37802f27b7f8cd973f5f3d33be7613952c0df904b68c4842f0e", - "sha256:ed447680ff21c14aaceb6a9f99a5f639f583ccfe4ce1a5e1d48eb41c3d6b3217", - "sha256:f52a4ad2568314ee713715b1e2d79ab55fab11e8b304fd1462ff5cccf4264b3e", - "sha256:fbd60c6aaa07c31d7754edbc2334aef50601b7f1ada67a96eb1eb57c7c72378f", - "sha256:fc28e0db232c62ca0c3600884933178f0825c99be4474cdd645e378a10588125", - "sha256:fe31de3002e7b08eb20823b3735b97c86c5926dd0581c7710a680b418a8709d4", - "sha256:fec221a051150eeddfdfcff162e6db92c65ecf46cb0f7bb1bf812a1520ec026b", - "sha256:ff71073ebf0e42258a42a0b34f2c09ec384977e7f6808999102eedd5b49920e3" - ], - "markers": "python_version >= '3.6'", - "version": "==1.3.0" - }, - "google-resumable-media": { - "hashes": [ - "sha256:1a7dce5790b04518edc02c2ce33965556660d64957106d66a945086e2b642572", - "sha256:36dc2f7201ee1cb360ef502187aa4e1f2b6ec4467fcee92e08a8cf165e36f587" - ], - "markers": "python_version >= '3.6'", - "version": "==2.3.0" - }, - "googleapis-common-protos": { - "extras": [], - "hashes": [ - "sha256:183bb0356bd614c4330ad5158bc1c1bcf9bcf7f5e7f911317559fe209496eeee", - "sha256:53eb313064738f45d5ac634155ae208e121c963659627b90dfcb61ef514c03e1" - ], - "markers": "python_version >= '3.6'", - "version": "==1.55.0" - }, - "greenlet": { - "hashes": [ - "sha256:0051c6f1f27cb756ffc0ffbac7d2cd48cb0362ac1736871399a739b2885134d3", - "sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711", - "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd", - "sha256:049fe7579230e44daef03a259faa24511d10ebfa44f69411d99e6a184fe68073", - "sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708", - "sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67", - "sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23", - "sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1", - "sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08", - "sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd", - "sha256:2bde6792f313f4e918caabc46532aa64aa27a0db05d75b20edfc5c6f46479de2", - "sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa", - "sha256:356b3576ad078c89a6107caa9c50cc14e98e3a6c4874a37c3e0273e4baf33de8", - "sha256:40b951f601af999a8bf2ce8c71e8aaa4e8c6f78ff8afae7b808aae2dc50d4c40", - "sha256:572e1787d1460da79590bf44304abbc0a2da944ea64ec549188fa84d89bba7ab", - "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6", - "sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc", - "sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b", - "sha256:7418b6bfc7fe3331541b84bb2141c9baf1ec7132a7ecd9f375912eca810e714e", - "sha256:7cbd7574ce8e138bda9df4efc6bf2ab8572c9aff640d8ecfece1b006b68da963", - "sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3", - "sha256:833e1551925ed51e6b44c800e71e77dacd7e49181fdc9ac9a0bf3714d515785d", - "sha256:8639cadfda96737427330a094476d4c7a56ac03de7265622fcf4cfe57c8ae18d", - "sha256:8c5d5b35f789a030ebb95bff352f1d27a93d81069f2adb3182d99882e095cefe", - "sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28", - "sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3", - "sha256:903bbd302a2378f984aef528f76d4c9b1748f318fe1294961c072bdc7f2ffa3e", - "sha256:93f81b134a165cc17123626ab8da2e30c0455441d4ab5576eed73a64c025b25c", - "sha256:95e69877983ea39b7303570fa6760f81a3eec23d0e3ab2021b7144b94d06202d", - "sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0", - "sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497", - "sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee", - "sha256:aa5b467f15e78b82257319aebc78dd2915e4c1436c3c0d1ad6f53e47ba6e2713", - "sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58", - "sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a", - "sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06", - "sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88", - "sha256:b336501a05e13b616ef81ce329c0e09ac5ed8c732d9ba7e3e983fcc1a9e86965", - "sha256:b8c008de9d0daba7b6666aa5bbfdc23dcd78cafc33997c9b7741ff6353bafb7f", - "sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4", - "sha256:be5f425ff1f5f4b3c1e33ad64ab994eed12fc284a6ea71c5243fd564502ecbe5", - "sha256:dd0b1e9e891f69e7675ba5c92e28b90eaa045f6ab134ffe70b52e948aa175b3c", - "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a", - "sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1", - "sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43", - "sha256:eb6ea6da4c787111adf40f697b4e58732ee0942b5d3bd8f435277643329ba627", - "sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b", - "sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168", - "sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d", - "sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5", - "sha256:f3acda1924472472ddd60c29e5b9db0cec629fbe3c5c5accb74d6d6d14773478", - "sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf", - "sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce", - "sha256:fa877ca7f6b48054f847b61d6fa7bed5cebb663ebc55e018fda12db09dcc664c", - "sha256:fdcec0b8399108577ec290f55551d926d9a1fa6cad45882093a7a07ac5ec147b" - ], - "markers": "platform_python_implementation == 'CPython'", - "version": "==1.1.2" - }, - "grpc-google-iam-v1": { - "hashes": [ - "sha256:0bfb5b56f648f457021a91c0df0db4934b6e0c300bd0f2de2333383fe958aa72" - ], - "version": "==0.12.3" - }, - "grpcio": { - "hashes": [ - "sha256:05467acd391e3fffb05991c76cb2ed2fa1309d0e3815ac379764bc5670b4b5d4", - "sha256:0ac72d4b953b76924f8fa21436af060d7e6d8581e279863f30ee14f20751ac27", - "sha256:11f811c0fffd84fca747fbc742464575e5eb130fd4fb4d6012ccc34febd001db", - "sha256:13343e7b840c20f43b44f0e6d3bbdc037c964f0aec9735d7cb685c407731c9ff", - "sha256:14eefcf623890f3f7dd7831decd2a2116652b5ce1e0f1d4b464b8f52110743b0", - "sha256:19e54f0c7083c8332b5a75a9081fc5127f1dbb67b6c1a32bd7fe896ef0934918", - "sha256:36a7bdd6ef9bca050c7ade8cba5f0e743343ea0756d5d3d520e915098a9dc503", - "sha256:3d47553b8e86ab1e59b0185ba6491a187f94a0239f414c8fc867a22b0405b798", - "sha256:41036a574cab3468f24d41d6ed2b52588fb85ed60f8feaa925d7e424a250740b", - "sha256:4201c597e5057a9bfef9ea5777a6d83f6252cb78044db7d57d941ec2300734a5", - "sha256:46d4843192e7d36278884282e100b8f305cf37d1b3d8c6b4f736d4454640a069", - "sha256:4bae1c99896045d3062ab95478411c8d5a52cb84b91a1517312629fa6cfeb50e", - "sha256:4ee51964edfd0a1293a95bb0d72d134ecf889379d90d2612cbf663623ce832b4", - "sha256:4fcb53e4eb8c271032c91b8981df5fc1bb974bc73e306ec2c27da41bd95c44b5", - "sha256:5c30a9a7d3a05920368a60b080cbbeaf06335303be23ac244034c71c03a0fd24", - "sha256:5f3c54ebb5d9633a557335c01d88d3d4928e9b1b131692283b6184da1edbec0b", - "sha256:6641a28cc826a92ef717201cca9a035c34a0185e38b0c93f3ce5f01a01a1570a", - "sha256:790d7493337558ae168477d1be3178f4c9b8f91d8cd9b8b719d06fd9b2d48836", - "sha256:871078218fa9117e2a378678f327e32fda04e363ed6bc0477275444273255d4d", - "sha256:898c159148f27e23c08a337fb80d31ece6b76bb24f359d83929460d813665b74", - "sha256:89b390b1c0de909965280d175c53128ce2f0f4f5c0f011382243dd7f2f894060", - "sha256:8fa6584046a7cf281649975a363673fa5d9c6faf9dc923f261cc0e56713b5892", - "sha256:9075c0c003c1ff14ebce8f0ba55cc692158cb55c68da09cf8b0f9fc5b749e343", - "sha256:9a86a91201f8345502ea81dee0a55ae13add5fafadf109b17acd858fe8239651", - "sha256:a8d610b7b557a7609fecee80b6dd793ecb7a9a3c3497fbdce63ce7d151cdd705", - "sha256:b81dc7894062ed2d25b74a2725aaa0a6895ce97ce854f432fe4e87cad5a07316", - "sha256:b8d852329336c584c636caa9c2db990f3a332b19bc86a80f4646b58d27c142db", - "sha256:be857b7ec2ac43455156e6ba89262f7d7ae60227049427d01a3fecd218a3f88d", - "sha256:bebe90b8020b4248e5a2076b56154cc6ff45691bbbe980579fc9db26717ac968", - "sha256:bfd36b959c3c4e945119387baed1414ea46f7116886aa23de0172302b49d7ff1", - "sha256:c122dac5cb299b8ad7308d61bd9fe0413de13b0347cce465398436b3fdf1f609", - "sha256:c5c2f8417d13386e18ccc8c61467cb6a6f9667a1ff7000a2d7d378e5d7df693f", - "sha256:ccd388b8f37b19d06e4152189726ce309e36dc03b53f2216a4ea49f09a7438e6", - "sha256:cd61b52d9cf8fcf8d9628c0b640b9e44fdc5e93d989cc268086a858540ed370c", - "sha256:cf220199b7b4992729ad4d55d5d3f652f4ccfe1a35b5eacdbecf189c245e1859", - "sha256:d1e22d3a510438b7f3365c0071b810672d09febac6e8ca8a47eab657ae5f347b", - "sha256:d2ec124a986093e26420a5fb10fa3f02b2c232f924cdd7b844ddf7e846c020cd", - "sha256:dc3290d0411ddd2bd49adba5793223de8de8b01588d45e9376f1a9f7d25414f4", - "sha256:e2149077d71e060678130644670389ddf1491200bcea16c5560d4ccdc65e3f2e", - "sha256:e2de61005118ae59d48d5d749283ebfd1ba4ca68cc1000f8a395cd2bdcff7ceb", - "sha256:e50ddea6de76c09b656df4b5a55ae222e2a56e625c44250e501ff3c904113ec1", - "sha256:e898194f76212facbaeb6d7545debff29351afa23b53ff8f0834d66611af5139", - "sha256:f6a9cf0e77f72f2ac30c9c6e086bc7446c984c51bebc6c7f50fbcd718037edba", - "sha256:fdb0a3e0e64843441793923d9532a3a23907b07b2a1e0a7a31f186dc185bb772" - ], - "index": "pypi", - "version": "==1.44.0" - }, - "grpcio-status": { - "hashes": [ - "sha256:ac613ab7a45380cbfa3e529022d0b37317d858f172ba6e65c188aa7355539398", - "sha256:caf831c1fdcafeb3f48f7f2500e6ffb0c755120354a302f8695b698b0a2faace" - ], - "version": "==1.44.0" - }, - "gunicorn": { - "hashes": [ - "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e", - "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8" - ], - "index": "pypi", - "version": "==20.1.0" - }, - "htmlmin": { - "hashes": [ - "sha256:50c1ef4630374a5d723900096a961cff426dff46b48f34d194a81bbe14eca178" - ], - "index": "pypi", - "version": "==0.1.12" - }, - "humanfriendly": { - "hashes": [ - "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", - "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==10.0" - }, - "humanize": { - "hashes": [ - "sha256:8d86333b8557dacffd4dce1dbe09c81c189e2caf7bb17a970b2212f0f58f10f2", - "sha256:ee1f872fdfc7d2ef4a28d4f80ddde9f96d36955b5d6b0dac4bdeb99502bddb00" - ], - "index": "pypi", - "version": "==4.0.0" - }, - "idna": { - "hashes": [ - "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", - "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" - ], - "markers": "python_version >= '3.5'", - "version": "==3.3" - }, - "itsdangerous": { - "hashes": [ - "sha256:29285842166554469a56d427addc0843914172343784cb909695fdbe90a3e129", - "sha256:d848fcb8bc7d507c4546b448574e8a44fc4ea2ba84ebf8d783290d53e81992f5" - ], - "index": "pypi", - "version": "==2.1.0" - }, - "jinja2": { - "hashes": [ - "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8", - "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7" - ], - "markers": "python_version >= '3.6'", - "version": "==3.0.3" - }, - "jmespath": { - "hashes": [ - "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", - "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.10.0" - }, - "jsonpointer": { - "hashes": [ - "sha256:26d9a47a72d4dc3e3ae72c4c6cd432afd73c680164cd2540772eab53cb3823b6", - "sha256:f09f8deecaaa5aea65b5eb4f67ca4e54e1a61f7a11c75085e360fe6feb6a48bf" - ], - "index": "pypi", - "version": "==2.2" - }, - "jwcrypto": { - "hashes": [ - "sha256:db93a656d9a7a35dda5a68deb5c9f301f4e60507d8aef1559e0637b9ac497137", - "sha256:f88816eb0a41b8f006af978ced5f171f33782525006cdb055b536a40f4d46ac9" - ], - "version": "==1.0" - }, - "libcst": { - "hashes": [ - "sha256:05f97c0f56da7bf8a348d63603a04cdf8f9cc18b9880be62540788e968e4b6fa", - "sha256:06de1bc753d789f928f19f5bba5bc83b1b4b304a1b95f537b87c8d5d5cb4b9ce", - "sha256:2473609db1218ee3a3d69d39f97e97b65f6fdb90b2bfce0af7680448578ed6eb", - "sha256:2bd72ce428ac4123c075cbbacb66ae62ed0c166e248cc81b504779c27e263bb7", - "sha256:2f2a2d70f14628eaa2870b94f2c8094048af980754433ac1195af14be3f06e27", - "sha256:35194a24918b7386310b3ce02456dc8259a2fdb8ef5c6620132047fb014b4e8e", - "sha256:3f61d3be41946d4ed921afb5914e40027d639130771e89d6846c0cc5bee967ec", - "sha256:407e419f8f69663509e37c9ebad88ca6ea4904d09a2293f47bbfc7597f82e7db", - "sha256:427c88ca77d0c7beb71a0c7f0ea9dccaafad5fc86bb384f381cd8c56412bd0db", - "sha256:43f698ee4eeb0fde410a369a4c51c7a5e61974307039ab8ef5c2da83f21b061d", - "sha256:46bc765dccd9741951b3716ce8ead0d7014fe5fe04927a5920188aedf786133e", - "sha256:640256354d7183bc801a78a5b05238ccdc46b3646c7a7bee288f8cc046ed0b25", - "sha256:753ada0471c666befb33ccb73258161bd6493ba3bbb5931abce9d02e71cc673f", - "sha256:7aacd83126cf932c38cd58be3f8dd9b9aaa88feaf8aa42418156873a5f5ded70", - "sha256:7b2a6be4d8eace4670af9e596b8dd364d74072235e5a17cc7cff1509483a97c8", - "sha256:961ab38c0ef318c384a287f1e4f877bb61ce93945f352b14b5dbbe7a317882b1", - "sha256:9880a360d9a07283825844d415dc89aee00f13977a571e68f7c168b39a5b7f59", - "sha256:9eab2755d4796ac0b89e705133547677eaaacb3a913f6b7761f4dc964cca2886", - "sha256:ab268eae8a1fdbc23d510f598d0d5b1efe98d7e4f79045fd565c305adebe3a2d", - "sha256:ac37e00960d1ebffbad1b8723d11eaa69371ba49cbcb5680c4da3d50c0536dc3", - "sha256:af9526ecc53a515cb5a1761536d6cc6dce7b2ccd958a01d1f185fa580d844afa", - "sha256:b4a6bc639bf9f7991e6850329264657448c6516a3d07fe2e0df692ae0bfdac83", - "sha256:c5076d07d4f556d82a04654b72ac80c1b38eea4590189c40880202de40ac4237", - "sha256:c6bdb278244d35cc5a14275ac1c0c11de79c6df46031f537c7b707b5841dd518", - "sha256:ce228e20216bce09ddb4eceed9a669f7fb52568ff300edf99a8850a4d6ab9e86", - "sha256:e02d3141ce6960f8df5b3c2615ea112a7a5065a60e81e56ca65a498c2c7f2490", - "sha256:ef99c15d0ea671bc1ba914d9f11634748479b1476fd389de9647c918c729d042", - "sha256:f8f75ed9981ec9a96835f78809360847661cc9c8033d404dcc65c346ce480f4d", - "sha256:fe162be926af39bf307dd69b1ceb89af5ccdbfe21e1d92ba24ef7faa9d62be7b" - ], - "markers": "python_version >= '3.6'", - "version": "==0.4.1" - }, - "markupsafe": { - "hashes": [ - "sha256:023af8c54fe63530545f70dd2a2a7eed18d07a9a77b94e8bf1e2ff7f252db9a3", - "sha256:09c86c9643cceb1d87ca08cdc30160d1b7ab49a8a21564868921959bd16441b8", - "sha256:142119fb14a1ef6d758912b25c4e803c3ff66920635c44078666fe7cc3f8f759", - "sha256:1d1fb9b2eec3c9714dd936860850300b51dbaa37404209c8d4cb66547884b7ed", - "sha256:204730fd5fe2fe3b1e9ccadb2bd18ba8712b111dcabce185af0b3b5285a7c989", - "sha256:24c3be29abb6b34052fd26fc7a8e0a49b1ee9d282e3665e8ad09a0a68faee5b3", - "sha256:290b02bab3c9e216da57c1d11d2ba73a9f73a614bbdcc027d299a60cdfabb11a", - "sha256:3028252424c72b2602a323f70fbf50aa80a5d3aa616ea6add4ba21ae9cc9da4c", - "sha256:30c653fde75a6e5eb814d2a0a89378f83d1d3f502ab710904ee585c38888816c", - "sha256:3cace1837bc84e63b3fd2dfce37f08f8c18aeb81ef5cf6bb9b51f625cb4e6cd8", - "sha256:4056f752015dfa9828dce3140dbadd543b555afb3252507348c493def166d454", - "sha256:454ffc1cbb75227d15667c09f164a0099159da0c1f3d2636aa648f12675491ad", - "sha256:598b65d74615c021423bd45c2bc5e9b59539c875a9bdb7e5f2a6b92dfcfc268d", - "sha256:599941da468f2cf22bf90a84f6e2a65524e87be2fce844f96f2dd9a6c9d1e635", - "sha256:5ddea4c352a488b5e1069069f2f501006b1a4362cb906bee9a193ef1245a7a61", - "sha256:62c0285e91414f5c8f621a17b69fc0088394ccdaa961ef469e833dbff64bd5ea", - "sha256:679cbb78914ab212c49c67ba2c7396dc599a8479de51b9a87b174700abd9ea49", - "sha256:6e104c0c2b4cd765b4e83909cde7ec61a1e313f8a75775897db321450e928cce", - "sha256:736895a020e31b428b3382a7887bfea96102c529530299f426bf2e636aacec9e", - "sha256:75bb36f134883fdbe13d8e63b8675f5f12b80bb6627f7714c7d6c5becf22719f", - "sha256:7d2f5d97fcbd004c03df8d8fe2b973fe2b14e7bfeb2cfa012eaa8759ce9a762f", - "sha256:80beaf63ddfbc64a0452b841d8036ca0611e049650e20afcb882f5d3c266d65f", - "sha256:84ad5e29bf8bab3ad70fd707d3c05524862bddc54dc040982b0dbcff36481de7", - "sha256:8da5924cb1f9064589767b0f3fc39d03e3d0fb5aa29e0cb21d43106519bd624a", - "sha256:961eb86e5be7d0973789f30ebcf6caab60b844203f4396ece27310295a6082c7", - "sha256:96de1932237abe0a13ba68b63e94113678c379dca45afa040a17b6e1ad7ed076", - "sha256:a0a0abef2ca47b33fb615b491ce31b055ef2430de52c5b3fb19a4042dbc5cadb", - "sha256:b2a5a856019d2833c56a3dcac1b80fe795c95f401818ea963594b345929dffa7", - "sha256:b8811d48078d1cf2a6863dafb896e68406c5f513048451cd2ded0473133473c7", - "sha256:c532d5ab79be0199fa2658e24a02fce8542df196e60665dd322409a03db6a52c", - "sha256:d3b64c65328cb4cd252c94f83e66e3d7acf8891e60ebf588d7b493a55a1dbf26", - "sha256:d4e702eea4a2903441f2735799d217f4ac1b55f7d8ad96ab7d4e25417cb0827c", - "sha256:d5653619b3eb5cbd35bfba3c12d575db2a74d15e0e1c08bf1db788069d410ce8", - "sha256:d66624f04de4af8bbf1c7f21cc06649c1c69a7f84109179add573ce35e46d448", - "sha256:e67ec74fada3841b8c5f4c4f197bea916025cb9aa3fe5abf7d52b655d042f956", - "sha256:e6f7f3f41faffaea6596da86ecc2389672fa949bd035251eab26dc6697451d05", - "sha256:f02cf7221d5cd915d7fa58ab64f7ee6dd0f6cddbb48683debf5d04ae9b1c2cc1", - "sha256:f0eddfcabd6936558ec020130f932d479930581171368fd728efcfb6ef0dd357", - "sha256:fabbe18087c3d33c5824cb145ffca52eccd053061df1d79d4b66dafa5ad2a5ea", - "sha256:fc3150f85e2dbcf99e65238c842d1cfe69d3e7649b19864c1cc043213d9cd730" - ], - "index": "pypi", - "version": "==2.1.0" - }, - "marshmallow": { - "hashes": [ - "sha256:04438610bc6dadbdddb22a4a55bcc7f6f8099e69580b2e67f5a681933a1f4400", - "sha256:4c05c1684e0e97fe779c62b91878f173b937fe097b356cd82f793464f5bc6138" - ], - "index": "pypi", - "version": "==3.14.1" - }, - "mypy-extensions": { - "hashes": [ - "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", - "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" - ], - "version": "==0.4.3" - }, - "packaging": { - "hashes": [ - "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", - "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" - ], - "markers": "python_version >= '3.6'", - "version": "==21.3" - }, - "pdfkit": { - "hashes": [ - "sha256:992f821e1e18fc8a0e701ecae24b51a2d598296a180caee0a24c0af181da02a9", - "sha256:a7a4ca0d978e44fa8310c4909f087052430a6e8e0b1dd7ceef657f139789f96f", - "sha256:cc122e5aed594198ff7aaa566f2950d2163763576ab891c161bb1f6c630f5a8e" - ], - "index": "pypi", - "version": "==1.0.0" - }, - "pika": { - "hashes": [ - "sha256:59da6701da1aeaf7e5e93bb521cc03129867f6e54b7dd352c4b3ecb2bd7ec624", - "sha256:f023d6ac581086b124190cb3dc81dd581a149d216fa4540ac34f9be1e3970b89" - ], - "index": "pypi", - "version": "==1.2.0" - }, - "proto-plus": { - "hashes": [ - "sha256:b06be21c3848fbc20387d1d6891a9b97dfa1cdd0f10d3d42ef70b5700ec0f423", - "sha256:f28b225bc9e6c14e206fb7f8e996a46fb2ccd902648e512d496abb6a716a4ae5" - ], - "markers": "python_version >= '3.6'", - "version": "==1.20.3" - }, - "protobuf": { - "hashes": [ - "sha256:072fbc78d705d3edc7ccac58a62c4c8e0cec856987da7df8aca86e647be4e35c", - "sha256:09297b7972da685ce269ec52af761743714996b4381c085205914c41fcab59fb", - "sha256:16f519de1313f1b7139ad70772e7db515b1420d208cb16c6d7858ea989fc64a9", - "sha256:1c91ef4110fdd2c590effb5dca8fdbdcb3bf563eece99287019c4204f53d81a4", - "sha256:3112b58aac3bac9c8be2b60a9daf6b558ca3f7681c130dcdd788ade7c9ffbdca", - "sha256:36cecbabbda242915529b8ff364f2263cd4de7c46bbe361418b5ed859677ba58", - "sha256:4276cdec4447bd5015453e41bdc0c0c1234eda08420b7c9a18b8d647add51e4b", - "sha256:435bb78b37fc386f9275a7035fe4fb1364484e38980d0dd91bc834a02c5ec909", - "sha256:48ed3877fa43e22bcacc852ca76d4775741f9709dd9575881a373bd3e85e54b2", - "sha256:54a1473077f3b616779ce31f477351a45b4fef8c9fd7892d6d87e287a38df368", - "sha256:69da7d39e39942bd52848438462674c463e23963a1fdaa84d88df7fbd7e749b2", - "sha256:6cbc312be5e71869d9d5ea25147cdf652a6781cf4d906497ca7690b7b9b5df13", - "sha256:7bb03bc2873a2842e5ebb4801f5c7ff1bfbdf426f85d0172f7644fcda0671ae0", - "sha256:7ca7da9c339ca8890d66958f5462beabd611eca6c958691a8fe6eccbd1eb0c6e", - "sha256:835a9c949dc193953c319603b2961c5c8f4327957fe23d914ca80d982665e8ee", - "sha256:84123274d982b9e248a143dadd1b9815049f4477dc783bf84efe6250eb4b836a", - "sha256:8961c3a78ebfcd000920c9060a262f082f29838682b1f7201889300c1fbe0616", - "sha256:96bd766831596d6014ca88d86dc8fe0fb2e428c0b02432fd9db3943202bf8c5e", - "sha256:9df0c10adf3e83015ced42a9a7bd64e13d06c4cf45c340d2c63020ea04499d0a", - "sha256:b38057450a0c566cbd04890a40edf916db890f2818e8682221611d78dc32ae26", - "sha256:bd95d1dfb9c4f4563e6093a9aa19d9c186bf98fa54da5252531cc0d3a07977e7", - "sha256:c1068287025f8ea025103e37d62ffd63fec8e9e636246b89c341aeda8a67c934", - "sha256:c438268eebb8cf039552897d78f402d734a404f1360592fef55297285f7f953f", - "sha256:cdc076c03381f5c1d9bb1abdcc5503d9ca8b53cf0a9d31a9f6754ec9e6c8af0f", - "sha256:f358aa33e03b7a84e0d91270a4d4d8f5df6921abe99a377828839e8ed0c04e07", - "sha256:f51d5a9f137f7a2cec2d326a74b6e3fc79d635d69ffe1b036d39fc7d75430d37" - ], - "markers": "python_version >= '3.5'", - "version": "==3.19.4" - }, - "pyasn1": { - "hashes": [ - "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359", - "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576", - "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf", - "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7", - "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", - "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00", - "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8", - "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86", - "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12", - "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776", - "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba", - "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2", - "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3" - ], - "version": "==0.4.8" - }, - "pyasn1-modules": { - "hashes": [ - "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8", - "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199", - "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811", - "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed", - "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4", - "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e", - "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74", - "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb", - "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45", - "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd", - "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0", - "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d", - "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405" - ], - "version": "==0.2.8" - }, - "pycparser": { - "hashes": [ - "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", - "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.21" - }, - "pyparsing": { - "hashes": [ - "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea", - "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484" - ], - "markers": "python_version >= '3.6'", - "version": "==3.0.7" - }, - "python-dateutil": { - "hashes": [ - "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", - "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.8.2" - }, - "python-snappy": { - "hashes": [ - "sha256:03bb511380fca2a13325b6f16fe8234c8e12da9660f0258cd45d9a02ffc916af", - "sha256:0bdb6942180660bda7f7d01f4c0def3cfc72b1c6d99aad964801775a3e379aba", - "sha256:0d489b50f49433494160c45048fe806de6b3aeab0586e497ebd22a0bab56e427", - "sha256:1a993dc8aadd901915a510fe6af5f20ae4256f527040066c22a154db8946751f", - "sha256:1d029f7051ec1bbeaa3e03030b6d8ed47ceb69cae9016f493c802a08af54e026", - "sha256:277757d5dad4e239dc1417438a0871b65b1b155beb108888e7438c27ffc6a8cc", - "sha256:2a7e528ab6e09c0d67dcb61a1730a292683e5ff9bb088950638d3170cf2a0a54", - "sha256:2aaaf618c68d8c9daebc23a20436bd01b09ee70d7fbf7072b7f38b06d2fab539", - "sha256:2be4f4550acd484912441f5f1209ba611ac399aac9355fee73611b9a0d4f949c", - "sha256:39692bedbe0b717001a99915ac0eb2d9d0bad546440d392a2042b96d813eede1", - "sha256:3fb9a88a4dd6336488f3de67ce75816d0d796dce53c2c6e4d70e0b565633c7fd", - "sha256:4038019b1bcaadde726a57430718394076c5a21545ebc5badad2c045a09546cf", - "sha256:463fd340a499d47b26ca42d2f36a639188738f6e2098c6dbf80aef0e60f461e1", - "sha256:4d3cafdf454354a621c8ab7408e45aa4e9d5c0b943b61ff4815f71ca6bdf0130", - "sha256:4ec533a8c1f8df797bded662ec3e494d225b37855bb63eb0d75464a07947477c", - "sha256:530bfb9efebcc1aab8bb4ebcbd92b54477eed11f6cf499355e882970a6d3aa7d", - "sha256:546c1a7470ecbf6239101e9aff0f709b68ca0f0268b34d9023019a55baa1f7c6", - "sha256:5843feb914796b1f0405ccf31ea0fb51034ceb65a7588edfd5a8250cb369e3b2", - "sha256:586724a0276d7a6083a17259d0b51622e492289a9998848a1b01b6441ca12b2f", - "sha256:59e975be4206cc54d0a112ef72fa3970a57c2b1bcc2c97ed41d6df0ebe518228", - "sha256:5a453c45178d7864c1bdd6bfe0ee3ed2883f63b9ba2c9bb967c6b586bf763f96", - "sha256:5bb05c28298803a74add08ba496879242ef159c75bc86a5406fac0ffc7dd021b", - "sha256:5e973e637112391f05581f427659c05b30b6843bc522a65be35ac7b18ce3dedd", - "sha256:66c80e9b366012dbee262bb1869e4fc5ba8786cda85928481528bc4a72ec2ee8", - "sha256:6a7620404da966f637b9ce8d4d3d543d363223f7a12452a575189c5355fc2d25", - "sha256:6f8bf4708a11b47517baf962f9a02196478bbb10fdb9582add4aa1459fa82380", - "sha256:735cd4528c55dbe4516d6d2b403331a99fc304f8feded8ae887cf97b67d589bb", - "sha256:7778c224efc38a40d274da4eb82a04cac27aae20012372a7db3c4bbd8926c4d4", - "sha256:8277d1f6282463c40761f802b742f833f9f2449fcdbb20a96579aa05c8feb614", - "sha256:88b6ea78b83d2796f330b0af1b70cdd3965dbdab02d8ac293260ec2c8fe340ee", - "sha256:8c07220408d3268e8268c9351c5c08041bc6f8c6172e59d398b71020df108541", - "sha256:8d0c019ee7dcf2c60e240877107cddbd95a5b1081787579bf179938392d66480", - "sha256:90b0186516b7a101c14764b0c25931b741fb0102f21253eff67847b4742dfc72", - "sha256:9837ac1650cc68d22a3cf5f15fb62c6964747d16cecc8b22431f113d6e39555d", - "sha256:9eac51307c6a1a38d5f86ebabc26a889fddf20cbba7a116ccb54ba1446601d5b", - "sha256:9f0c0d88b84259f93c3aa46398680646f2c23e43394779758d9f739c34e15295", - "sha256:a0ad38bc98d0b0497a0b0dbc29409bcabfcecff4511ed7063403c86de16927bc", - "sha256:b265cde49774752aec9ca7f5d272e3f98718164afc85521622a8a5394158a2b5", - "sha256:b6a107ab06206acc5359d4c5632bd9b22d448702a79b3169b0c62e0fb808bb2a", - "sha256:b7f920eaf46ebf41bd26f9df51c160d40f9e00b7b48471c3438cb8d027f7fb9b", - "sha256:c20498bd712b6e31a4402e1d027a1cd64f6a4a0066a3fe3c7344475886d07fdf", - "sha256:cb18d9cd7b3f35a2f5af47bb8ed6a5bdbf4f3ddee37f3daade4ab7864c292f5b", - "sha256:cf5bb9254e1c38aacf253d510d3d9be631bba21f3d068b17672b38b5cbf2fff5", - "sha256:d017775851a778ec9cc32651c4464079d06d927303c2dde9ae9830ccf6fe94e1", - "sha256:dc96668d9c7cc656609764275c5f8da58ef56d89bdd6810f6923d36497468ff7", - "sha256:e066a0586833d610c4bbddba0be5ba0e3e4f8e0bc5bb6d82103d8f8fc47bb59a", - "sha256:e3a013895c64352b49d0d8e107a84f99631b16dbab156ded33ebf0becf56c8b2", - "sha256:eaf905a580f2747c4a474040a5063cd5e0cc3d1d2d6edb65f28196186493ad4a" - ], - "index": "pypi", - "version": "==0.6.1" - }, - "pytz": { - "hashes": [ - "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c", - "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326" - ], - "version": "==2021.3" - }, - "pyyaml": { - "hashes": [ - "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293", - "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b", - "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57", - "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b", - "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4", - "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07", - "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba", - "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9", - "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287", - "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513", - "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0", - "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0", - "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92", - "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f", - "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2", - "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc", - "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c", - "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86", - "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4", - "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c", - "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34", - "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b", - "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c", - "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb", - "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737", - "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3", - "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d", - "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53", - "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78", - "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803", - "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a", - "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174", - "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5" - ], - "index": "pypi", - "version": "==6.0" - }, - "redis": { - "hashes": [ - "sha256:04629f8e42be942c4f7d1812f2094568f04c612865ad19ad3ace3005da70631a", - "sha256:1d9a0cdf89fdd93f84261733e24f55a7bbd413a9b219fdaf56e3e728ca9a2306" - ], - "index": "pypi", - "version": "==4.1.4" - }, - "requests": { - "hashes": [ - "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", - "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d" - ], - "index": "pypi", - "version": "==2.27.1" - }, - "rsa": { - "hashes": [ - "sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17", - "sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb" - ], - "markers": "python_version >= '3.6'", - "version": "==4.8" - }, - "s3transfer": { - "hashes": [ - "sha256:25c140f5c66aa79e1ac60be50dcd45ddc59e83895f062a3aab263b870102911f", - "sha256:69d264d3e760e569b78aaa0f22c97e955891cd22e32b10c51f784eeda4d9d10a" - ], - "markers": "python_version >= '3.6'", - "version": "==0.5.1" - }, - "sdc-cryptography": { - "hashes": [ - "sha256:0bb9ff7347bf871c362d659f04b9988c48db75f50dd5545e0dc6ef71016ba8c0", - "sha256:b1d98a7bc73f8960c07fc6c9156eb1a6e3bfb9e3e057df0dfa8f979e08754166" - ], - "index": "pypi", - "version": "==1.1.0" - }, - "setuptools": { - "hashes": [ - "sha256:2347b2b432c891a863acadca2da9ac101eae6169b1d3dfee2ec605ecd50dbfe5", - "sha256:e4f30b9f84e5ab3decf945113119649fec09c1fc3507c6ebffec75646c56e62b" - ], - "markers": "python_version >= '3.7'", - "version": "==60.9.3" - }, - "simplejson": { - "hashes": [ - "sha256:04e31fa6ac8e326480703fb6ded1488bfa6f1d3f760d32e29dbf66d0838982ce", - "sha256:068670af975247acbb9fc3d5393293368cda17026db467bf7a51548ee8f17ee1", - "sha256:07ecaafc1b1501f275bf5acdee34a4ad33c7c24ede287183ea77a02dc071e0c0", - "sha256:0b4126cac7d69ac06ff22efd3e0b3328a4a70624fcd6bca4fc1b4e6d9e2e12bf", - "sha256:0de783e9c2b87bdd75b57efa2b6260c24b94605b5c9843517577d40ee0c3cc8a", - "sha256:12133863178a8080a3dccbf5cb2edfab0001bc41e5d6d2446af2a1131105adfe", - "sha256:1c9b1ed7ed282b36571638297525f8ef80f34b3e2d600a56f962c6044f24200d", - "sha256:23fe704da910ff45e72543cbba152821685a889cf00fc58d5c8ee96a9bad5f94", - "sha256:28221620f4dcabdeac310846629b976e599a13f59abb21616356a85231ebd6ad", - "sha256:35a49ebef25f1ebdef54262e54ae80904d8692367a9f208cdfbc38dbf649e00a", - "sha256:37bc0cf0e5599f36072077e56e248f3336917ded1d33d2688624d8ed3cefd7d2", - "sha256:3fe87570168b2ae018391e2b43fbf66e8593a86feccb4b0500d134c998983ccc", - "sha256:3ff5b3464e1ce86a8de8c88e61d4836927d5595c2162cab22e96ff551b916e81", - "sha256:401d40969cee3df7bda211e57b903a534561b77a7ade0dd622a8d1a31eaa8ba7", - "sha256:4b6bd8144f15a491c662f06814bd8eaa54b17f26095bb775411f39bacaf66837", - "sha256:4c09868ddb86bf79b1feb4e3e7e4a35cd6e61ddb3452b54e20cf296313622566", - "sha256:4d1c135af0c72cb28dd259cf7ba218338f4dc027061262e46fe058b4e6a4c6a3", - "sha256:4ff4ac6ff3aa8f814ac0f50bf218a2e1a434a17aafad4f0400a57a8cc62ef17f", - "sha256:521877c7bd060470806eb6335926e27453d740ac1958eaf0d8c00911bc5e1802", - "sha256:522fad7be85de57430d6d287c4b635813932946ebf41b913fe7e880d154ade2e", - "sha256:5540fba2d437edaf4aa4fbb80f43f42a8334206ad1ad3b27aef577fd989f20d9", - "sha256:5d6b4af7ad7e4ac515bc6e602e7b79e2204e25dbd10ab3aa2beef3c5a9cad2c7", - "sha256:5decdc78849617917c206b01e9fc1d694fd58caa961be816cb37d3150d613d9a", - "sha256:632ecbbd2228575e6860c9e49ea3cc5423764d5aa70b92acc4e74096fb434044", - "sha256:65b998193bd7b0c7ecdfffbc825d808eac66279313cb67d8892bb259c9d91494", - "sha256:67093a526e42981fdd954868062e56c9b67fdd7e712616cc3265ad0c210ecb51", - "sha256:681eb4d37c9a9a6eb9b3245a5e89d7f7b2b9895590bb08a20aa598c1eb0a1d9d", - "sha256:69bd56b1d257a91e763256d63606937ae4eb890b18a789b66951c00062afec33", - "sha256:724c1fe135aa437d5126138d977004d165a3b5e2ee98fc4eb3e7c0ef645e7e27", - "sha256:7255a37ff50593c9b2f1afa8fafd6ef5763213c1ed5a9e2c6f5b9cc925ab979f", - "sha256:743cd768affaa508a21499f4858c5b824ffa2e1394ed94eb85caf47ac0732198", - "sha256:80d3bc9944be1d73e5b1726c3bbfd2628d3d7fe2880711b1eb90b617b9b8ac70", - "sha256:82ff356ff91be0ab2293fc6d8d262451eb6ac4fd999244c4b5f863e049ba219c", - "sha256:8e8607d8f6b4f9d46fee11447e334d6ab50e993dd4dbfb22f674616ce20907ab", - "sha256:97202f939c3ff341fc3fa84d15db86156b1edc669424ba20b0a1fcd4a796a045", - "sha256:9b01e7b00654115965a206e3015f0166674ec1e575198a62a977355597c0bef5", - "sha256:9fa621b3c0c05d965882c920347b6593751b7ab20d8fa81e426f1735ca1a9fc7", - "sha256:a1aa6e4cae8e3b8d5321be4f51c5ce77188faf7baa9fe1e78611f93a8eed2882", - "sha256:a2d30d6c1652140181dc6861f564449ad71a45e4f165a6868c27d36745b65d40", - "sha256:a649d0f66029c7eb67042b15374bd93a26aae202591d9afd71e111dd0006b198", - "sha256:a7854326920d41c3b5d468154318fe6ba4390cb2410480976787c640707e0180", - "sha256:a89acae02b2975b1f8e4974cb8cdf9bf9f6c91162fb8dec50c259ce700f2770a", - "sha256:a8bbdb166e2fb816e43ab034c865147edafe28e1b19c72433147789ac83e2dda", - "sha256:ac786f6cb7aa10d44e9641c7a7d16d7f6e095b138795cd43503769d4154e0dc2", - "sha256:b09bc62e5193e31d7f9876220fb429ec13a6a181a24d897b9edfbbdbcd678851", - "sha256:b10556817f09d46d420edd982dd0653940b90151d0576f09143a8e773459f6fe", - "sha256:b81076552d34c27e5149a40187a8f7e2abb2d3185576a317aaf14aeeedad862a", - "sha256:bdfc54b4468ed4cd7415928cbe782f4d782722a81aeb0f81e2ddca9932632211", - "sha256:cf6e7d5fe2aeb54898df18db1baf479863eae581cce05410f61f6b4188c8ada1", - "sha256:cf98038d2abf63a1ada5730e91e84c642ba6c225b0198c3684151b1f80c5f8a6", - "sha256:d24a9e61df7a7787b338a58abfba975414937b609eb6b18973e25f573bc0eeeb", - "sha256:d74ee72b5071818a1a5dab47338e87f08a738cb938a3b0653b9e4d959ddd1fd9", - "sha256:dd16302d39c4d6f4afde80edd0c97d4db643327d355a312762ccd9bd2ca515ed", - "sha256:dd2fb11922f58df8528adfca123f6a84748ad17d066007e7ac977720063556bd", - "sha256:deac4bdafa19bbb89edfb73b19f7f69a52d0b5bd3bb0c4ad404c1bbfd7b4b7fd", - "sha256:e03c3b8cc7883a54c3f34a6a135c4a17bc9088a33f36796acdb47162791b02f6", - "sha256:e1ec8a9ee0987d4524ffd6299e778c16cc35fef6d1a2764e609f90962f0b293a", - "sha256:e8603e691580487f11306ecb066c76f1f4a8b54fb3bdb23fa40643a059509366", - "sha256:f444762fed1bc1fd75187ef14a20ed900c1fbb245d45be9e834b822a0223bc81", - "sha256:f63600ec06982cdf480899026f4fda622776f5fabed9a869fdb32d72bc17e99a", - "sha256:fb62d517a516128bacf08cb6a86ecd39fb06d08e7c4980251f5d5601d29989ba" - ], - "index": "pypi", - "version": "==3.17.6" - }, - "six": { - "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" - }, - "structlog": { - "hashes": [ - "sha256:68c4c29c003714fe86834f347cb107452847ba52414390a7ee583472bde00fc9", - "sha256:fd7922e195262b337da85c2a91c84be94ccab1f8fd1957bd6986f6904e3761c8" - ], - "index": "pypi", - "version": "==21.5.0" - }, - "typing-extensions": { - "hashes": [ - "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42", - "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2" - ], - "markers": "python_version >= '3.6'", - "version": "==4.1.1" - }, - "typing-inspect": { - "hashes": [ - "sha256:047d4097d9b17f46531bf6f014356111a1b6fb821a24fe7ac909853ca2a782aa", - "sha256:3cd7d4563e997719a710a3bfe7ffb544c6b72069b6812a02e9b414a8fa3aaa6b", - "sha256:b1f56c0783ef0f25fb064a01be6e5407e54cf4a4bf4f3ba3fe51e0bd6dcea9e5" - ], - "version": "==0.7.1" - }, - "ua-parser": { - "hashes": [ - "sha256:46ab2e383c01dbd2ab284991b87d624a26a08f72da4d7d413f5bfab8b9036f8a", - "sha256:47b1782ed130d890018d983fac37c2a80799d9e0b9c532e734c67cf70f185033" - ], - "index": "pypi", - "version": "==0.10.0" - }, - "urllib3": { - "hashes": [ - "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed", - "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4.0'", - "version": "==1.26.8" - }, - "uwsgi": { - "hashes": [ - "sha256:88ab9867d8973d8ae84719cf233b7dafc54326fcaec89683c3f9f77c002cdff9" - ], - "index": "pypi", - "version": "==2.0.20" - }, - "werkzeug": { - "hashes": [ - "sha256:1421ebfc7648a39a5c58c601b154165d05cf47a3cd0ccb70857cbdacf6c8f2b8", - "sha256:b863f8ff057c522164b6067c9e28b041161b4be5ba4d0daceeaa50a163822d3c" - ], - "markers": "python_version >= '3.6'", - "version": "==2.0.3" - }, - "wrapt": { - "hashes": [ - "sha256:086218a72ec7d986a3eddb7707c8c4526d677c7b35e355875a0fe2918b059179", - "sha256:0877fe981fd76b183711d767500e6b3111378ed2043c145e21816ee589d91096", - "sha256:0a017a667d1f7411816e4bf214646d0ad5b1da2c1ea13dec6c162736ff25a374", - "sha256:0cb23d36ed03bf46b894cfec777eec754146d68429c30431c99ef28482b5c1df", - "sha256:1fea9cd438686e6682271d36f3481a9f3636195578bab9ca3382e2f5f01fc185", - "sha256:220a869982ea9023e163ba915077816ca439489de6d2c09089b219f4e11b6785", - "sha256:25b1b1d5df495d82be1c9d2fad408f7ce5ca8a38085e2da41bb63c914baadff7", - "sha256:2dded5496e8f1592ec27079b28b6ad2a1ef0b9296d270f77b8e4a3a796cf6909", - "sha256:2ebdde19cd3c8cdf8df3fc165bc7827334bc4e353465048b36f7deeae8ee0918", - "sha256:43e69ffe47e3609a6aec0fe723001c60c65305784d964f5007d5b4fb1bc6bf33", - "sha256:46f7f3af321a573fc0c3586612db4decb7eb37172af1bc6173d81f5b66c2e068", - "sha256:47f0a183743e7f71f29e4e21574ad3fa95676136f45b91afcf83f6a050914829", - "sha256:498e6217523111d07cd67e87a791f5e9ee769f9241fcf8a379696e25806965af", - "sha256:4b9c458732450ec42578b5642ac53e312092acf8c0bfce140ada5ca1ac556f79", - "sha256:51799ca950cfee9396a87f4a1240622ac38973b6df5ef7a41e7f0b98797099ce", - "sha256:5601f44a0f38fed36cc07db004f0eedeaadbdcec90e4e90509480e7e6060a5bc", - "sha256:5f223101f21cfd41deec8ce3889dc59f88a59b409db028c469c9b20cfeefbe36", - "sha256:610f5f83dd1e0ad40254c306f4764fcdc846641f120c3cf424ff57a19d5f7ade", - "sha256:6a03d9917aee887690aa3f1747ce634e610f6db6f6b332b35c2dd89412912bca", - "sha256:705e2af1f7be4707e49ced9153f8d72131090e52be9278b5dbb1498c749a1e32", - "sha256:766b32c762e07e26f50d8a3468e3b4228b3736c805018e4b0ec8cc01ecd88125", - "sha256:77416e6b17926d953b5c666a3cb718d5945df63ecf922af0ee576206d7033b5e", - "sha256:778fd096ee96890c10ce96187c76b3e99b2da44e08c9e24d5652f356873f6709", - "sha256:78dea98c81915bbf510eb6a3c9c24915e4660302937b9ae05a0947164248020f", - "sha256:7dd215e4e8514004c8d810a73e342c536547038fb130205ec4bba9f5de35d45b", - "sha256:7dde79d007cd6dfa65afe404766057c2409316135cb892be4b1c768e3f3a11cb", - "sha256:81bd7c90d28a4b2e1df135bfbd7c23aee3050078ca6441bead44c42483f9ebfb", - "sha256:85148f4225287b6a0665eef08a178c15097366d46b210574a658c1ff5b377489", - "sha256:865c0b50003616f05858b22174c40ffc27a38e67359fa1495605f96125f76640", - "sha256:87883690cae293541e08ba2da22cacaae0a092e0ed56bbba8d018cc486fbafbb", - "sha256:8aab36778fa9bba1a8f06a4919556f9f8c7b33102bd71b3ab307bb3fecb21851", - "sha256:8c73c1a2ec7c98d7eaded149f6d225a692caa1bd7b2401a14125446e9e90410d", - "sha256:936503cb0a6ed28dbfa87e8fcd0a56458822144e9d11a49ccee6d9a8adb2ac44", - "sha256:944b180f61f5e36c0634d3202ba8509b986b5fbaf57db3e94df11abee244ba13", - "sha256:96b81ae75591a795d8c90edc0bfaab44d3d41ffc1aae4d994c5aa21d9b8e19a2", - "sha256:981da26722bebb9247a0601e2922cedf8bb7a600e89c852d063313102de6f2cb", - "sha256:ae9de71eb60940e58207f8e71fe113c639da42adb02fb2bcbcaccc1ccecd092b", - "sha256:b73d4b78807bd299b38e4598b8e7bd34ed55d480160d2e7fdaabd9931afa65f9", - "sha256:d4a5f6146cfa5c7ba0134249665acd322a70d1ea61732723c7d3e8cc0fa80755", - "sha256:dd91006848eb55af2159375134d724032a2d1d13bcc6f81cd8d3ed9f2b8e846c", - "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a", - "sha256:e6906d6f48437dfd80464f7d7af1740eadc572b9f7a4301e7dd3d65db285cacf", - "sha256:e92d0d4fa68ea0c02d39f1e2f9cb5bc4b4a71e8c442207433d8db47ee79d7aa3", - "sha256:e94b7d9deaa4cc7bac9198a58a7240aaf87fe56c6277ee25fa5b3aa1edebd229", - "sha256:ea3e746e29d4000cd98d572f3ee2a6050a4f784bb536f4ac1f035987fc1ed83e", - "sha256:ec7e20258ecc5174029a0f391e1b948bf2906cd64c198a9b8b281b811cbc04de", - "sha256:ec9465dd69d5657b5d2fa6133b3e1e989ae27d29471a672416fd729b429eb554", - "sha256:f122ccd12fdc69628786d0c947bdd9cb2733be8f800d88b5a37c57f1f1d73c10", - "sha256:f99c0489258086308aad4ae57da9e8ecf9e1f3f30fa35d5e170b4d4896554d80", - "sha256:f9c51d9af9abb899bd34ace878fbec8bf357b3194a10c4e8e0a25512826ef056", - "sha256:fd76c47f20984b43d93de9a82011bb6e5f8325df6c9ed4d8310029a55fa361ea" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.13.3" - }, - "wtforms": { - "hashes": [ - "sha256:6b351bbb12dd58af57ffef05bc78425d08d1914e0fd68ee14143b7ade023c5bc", - "sha256:837f2f0e0ca79481b92884962b914eba4e72b7a2daaf1f939c890ed0124b834b" - ], - "markers": "python_version >= '3.7'", - "version": "==3.0.1" - }, - "zope.event": { - "hashes": [ - "sha256:2666401939cdaa5f4e0c08cf7f20c9b21423b95e88f4675b1443973bdb080c42", - "sha256:5e76517f5b9b119acf37ca8819781db6c16ea433f7e2062c4afc2b6fbedb1330" - ], - "version": "==4.5.0" - }, - "zope.interface": { - "hashes": [ - "sha256:08f9636e99a9d5410181ba0729e0408d3d8748026ea938f3b970a0249daa8192", - "sha256:0b465ae0962d49c68aa9733ba92a001b2a0933c317780435f00be7ecb959c702", - "sha256:0cba8477e300d64a11a9789ed40ee8932b59f9ee05f85276dbb4b59acee5dd09", - "sha256:0cee5187b60ed26d56eb2960136288ce91bcf61e2a9405660d271d1f122a69a4", - "sha256:0ea1d73b7c9dcbc5080bb8aaffb776f1c68e807767069b9ccdd06f27a161914a", - "sha256:0f91b5b948686659a8e28b728ff5e74b1be6bf40cb04704453617e5f1e945ef3", - "sha256:15e7d1f7a6ee16572e21e3576d2012b2778cbacf75eb4b7400be37455f5ca8bf", - "sha256:17776ecd3a1fdd2b2cd5373e5ef8b307162f581c693575ec62e7c5399d80794c", - "sha256:194d0bcb1374ac3e1e023961610dc8f2c78a0f5f634d0c737691e215569e640d", - "sha256:1c0e316c9add0db48a5b703833881351444398b04111188069a26a61cfb4df78", - "sha256:205e40ccde0f37496904572035deea747390a8b7dc65146d30b96e2dd1359a83", - "sha256:273f158fabc5ea33cbc936da0ab3d4ba80ede5351babc4f577d768e057651531", - "sha256:2876246527c91e101184f63ccd1d716ec9c46519cc5f3d5375a3351c46467c46", - "sha256:2c98384b254b37ce50eddd55db8d381a5c53b4c10ee66e1e7fe749824f894021", - "sha256:2e5a26f16503be6c826abca904e45f1a44ff275fdb7e9d1b75c10671c26f8b94", - "sha256:334701327f37c47fa628fc8b8d28c7d7730ce7daaf4bda1efb741679c2b087fc", - "sha256:3748fac0d0f6a304e674955ab1365d515993b3a0a865e16a11ec9d86fb307f63", - "sha256:3c02411a3b62668200910090a0dff17c0b25aaa36145082a5a6adf08fa281e54", - "sha256:3dd4952748521205697bc2802e4afac5ed4b02909bb799ba1fe239f77fd4e117", - "sha256:3f24df7124c323fceb53ff6168da70dbfbae1442b4f3da439cd441681f54fe25", - "sha256:469e2407e0fe9880ac690a3666f03eb4c3c444411a5a5fddfdabc5d184a79f05", - "sha256:4de4bc9b6d35c5af65b454d3e9bc98c50eb3960d5a3762c9438df57427134b8e", - "sha256:5208ebd5152e040640518a77827bdfcc73773a15a33d6644015b763b9c9febc1", - "sha256:52de7fc6c21b419078008f697fd4103dbc763288b1406b4562554bd47514c004", - "sha256:5bb3489b4558e49ad2c5118137cfeaf59434f9737fa9c5deefc72d22c23822e2", - "sha256:5dba5f530fec3f0988d83b78cc591b58c0b6eb8431a85edd1569a0539a8a5a0e", - "sha256:5dd9ca406499444f4c8299f803d4a14edf7890ecc595c8b1c7115c2342cadc5f", - "sha256:5f931a1c21dfa7a9c573ec1f50a31135ccce84e32507c54e1ea404894c5eb96f", - "sha256:63b82bb63de7c821428d513607e84c6d97d58afd1fe2eb645030bdc185440120", - "sha256:66c0061c91b3b9cf542131148ef7ecbecb2690d48d1612ec386de9d36766058f", - "sha256:6f0c02cbb9691b7c91d5009108f975f8ffeab5dff8f26d62e21c493060eff2a1", - "sha256:71aace0c42d53abe6fc7f726c5d3b60d90f3c5c055a447950ad6ea9cec2e37d9", - "sha256:7d97a4306898b05404a0dcdc32d9709b7d8832c0c542b861d9a826301719794e", - "sha256:7df1e1c05304f26faa49fa752a8c690126cf98b40b91d54e6e9cc3b7d6ffe8b7", - "sha256:8270252effc60b9642b423189a2fe90eb6b59e87cbee54549db3f5562ff8d1b8", - "sha256:867a5ad16892bf20e6c4ea2aab1971f45645ff3102ad29bd84c86027fa99997b", - "sha256:877473e675fdcc113c138813a5dd440da0769a2d81f4d86614e5d62b69497155", - "sha256:8892f89999ffd992208754851e5a052f6b5db70a1e3f7d54b17c5211e37a98c7", - "sha256:9a9845c4c6bb56e508651f005c4aeb0404e518c6f000d5a1123ab077ab769f5c", - "sha256:a1e6e96217a0f72e2b8629e271e1b280c6fa3fe6e59fa8f6701bec14e3354325", - "sha256:a8156e6a7f5e2a0ff0c5b21d6bcb45145efece1909efcbbbf48c56f8da68221d", - "sha256:a9506a7e80bcf6eacfff7f804c0ad5350c8c95b9010e4356a4b36f5322f09abb", - "sha256:af310ec8335016b5e52cae60cda4a4f2a60a788cbb949a4fbea13d441aa5a09e", - "sha256:b0297b1e05fd128d26cc2460c810d42e205d16d76799526dfa8c8ccd50e74959", - "sha256:bf68f4b2b6683e52bec69273562df15af352e5ed25d1b6641e7efddc5951d1a7", - "sha256:d0c1bc2fa9a7285719e5678584f6b92572a5b639d0e471bb8d4b650a1a910920", - "sha256:d4d9d6c1a455d4babd320203b918ccc7fcbefe308615c521062bc2ba1aa4d26e", - "sha256:db1fa631737dab9fa0b37f3979d8d2631e348c3b4e8325d6873c2541d0ae5a48", - "sha256:dd93ea5c0c7f3e25335ab7d22a507b1dc43976e1345508f845efc573d3d779d8", - "sha256:f44e517131a98f7a76696a7b21b164bcb85291cee106a23beccce454e1f433a4", - "sha256:f7ee479e96f7ee350db1cf24afa5685a5899e2b34992fb99e1f7c1b0b758d263" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==5.4.0" - } - }, - "develop": { - "appdirs": { - "hashes": [ - "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", - "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" - ], - "version": "==1.4.4" - }, - "astroid": { - "hashes": [ - "sha256:1efdf4e867d4d8ba4a9f6cf9ce07cd182c4c41de77f23814feb27ca93ca9d877", - "sha256:506daabe5edffb7e696ad82483ad0228245a9742ed7d2d8c9cdb31537decf9f6" - ], - "markers": "python_full_version >= '3.6.2'", - "version": "==2.9.3" - }, - "attrs": { - "hashes": [ - "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", - "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==21.4.0" - }, - "beautifulsoup4": { - "hashes": [ - "sha256:9a315ce70049920ea4572a4055bc4bd700c940521d36fc858205ad4fcde149bf", - "sha256:c23ad23c521d818955a4151a67d81580319d4bf548d3d49f4223ae041ff98891" - ], - "index": "pypi", - "version": "==4.10.0" - }, - "black": { - "hashes": [ - "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea" - ], - "index": "pypi", - "version": "==20.8b1" - }, - "boto3": { - "hashes": [ - "sha256:0e8d4d814f94031947035a4c2bb2c23832d5de941a6a492fb85794a02bafc44d", - "sha256:95d9b5b6fe3383fbf8f33d58f62258d3b3ea138d4369017031339b60fd5b8887" - ], - "index": "pypi", - "version": "==1.21.6" - }, - "botocore": { - "hashes": [ - "sha256:359b9ea3870a1f8264113cb0b1216baa94bf1e8cee5d5d8af63a2e7ca6e7b33c", - "sha256:69aaa5a78ac7371f573e463be51fb962213c42fab08ef82380e84b9a87386949" - ], - "markers": "python_version >= '3.6'", - "version": "==1.24.6" - }, - "certifi": { - "hashes": [ - "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", - "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" - ], - "version": "==2021.10.8" - }, - "cffi": { - "hashes": [ - "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3", - "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2", - "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636", - "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20", - "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728", - "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27", - "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66", - "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443", - "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0", - "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7", - "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39", - "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605", - "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a", - "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37", - "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029", - "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139", - "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc", - "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df", - "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14", - "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880", - "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2", - "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a", - "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e", - "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474", - "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024", - "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8", - "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0", - "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e", - "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a", - "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e", - "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032", - "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6", - "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e", - "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b", - "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e", - "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954", - "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962", - "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c", - "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4", - "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55", - "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962", - "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023", - "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c", - "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6", - "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8", - "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382", - "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7", - "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc", - "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997", - "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796" - ], - "version": "==1.15.0" - }, - "charset-normalizer": { - "hashes": [ - "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", - "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df" - ], - "markers": "python_version >= '3'", - "version": "==2.0.12" - }, - "click": { - "hashes": [ - "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1", - "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb" - ], - "markers": "python_version >= '3.6'", - "version": "==8.0.4" - }, - "coverage": { - "extras": [ - "toml" - ], - "hashes": [ - "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9", - "sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d", - "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf", - "sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7", - "sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6", - "sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4", - "sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059", - "sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39", - "sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536", - "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac", - "sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c", - "sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903", - "sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d", - "sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05", - "sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684", - "sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1", - "sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f", - "sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7", - "sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca", - "sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad", - "sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca", - "sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d", - "sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92", - "sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4", - "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf", - "sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6", - "sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1", - "sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4", - "sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359", - "sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3", - "sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620", - "sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512", - "sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69", - "sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2", - "sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518", - "sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0", - "sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa", - "sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4", - "sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e", - "sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1", - "sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2" - ], - "markers": "python_version >= '3.7'", - "version": "==6.3.2" - }, - "cryptography": { - "hashes": [ - "sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3", - "sha256:2d87cdcb378d3cfed944dac30596da1968f88fb96d7fc34fdae30a99054b2e31", - "sha256:30ee1eb3ebe1644d1c3f183d115a8c04e4e603ed6ce8e394ed39eea4a98469ac", - "sha256:391432971a66cfaf94b21c24ab465a4cc3e8bf4a939c1ca5c3e3a6e0abebdbcf", - "sha256:39bdf8e70eee6b1c7b289ec6e5d84d49a6bfa11f8b8646b5b3dfe41219153316", - "sha256:4caa4b893d8fad33cf1964d3e51842cd78ba87401ab1d2e44556826df849a8ca", - "sha256:53e5c1dc3d7a953de055d77bef2ff607ceef7a2aac0353b5d630ab67f7423638", - "sha256:596f3cd67e1b950bc372c33f1a28a0692080625592ea6392987dba7f09f17a94", - "sha256:5d59a9d55027a8b88fd9fd2826c4392bd487d74bf628bb9d39beecc62a644c12", - "sha256:6c0c021f35b421ebf5976abf2daacc47e235f8b6082d3396a2fe3ccd537ab173", - "sha256:73bc2d3f2444bcfeac67dd130ff2ea598ea5f20b40e36d19821b4df8c9c5037b", - "sha256:74d6c7e80609c0f4c2434b97b80c7f8fdfaa072ca4baab7e239a15d6d70ed73a", - "sha256:7be0eec337359c155df191d6ae00a5e8bbb63933883f4f5dffc439dac5348c3f", - "sha256:94ae132f0e40fe48f310bba63f477f14a43116f05ddb69d6fa31e93f05848ae2", - "sha256:bb5829d027ff82aa872d76158919045a7c1e91fbf241aec32cb07956e9ebd3c9", - "sha256:ca238ceb7ba0bdf6ce88c1b74a87bffcee5afbfa1e41e173b1ceb095b39add46", - "sha256:ca28641954f767f9822c24e927ad894d45d5a1e501767599647259cbf030b903", - "sha256:e0344c14c9cb89e76eb6a060e67980c9e35b3f36691e15e1b7a9e58a0a6c6dc3", - "sha256:ebc15b1c22e55c4d5566e3ca4db8689470a0ca2babef8e3a9ee057a8b82ce4b1", - "sha256:ec63da4e7e4a5f924b90af42eddf20b698a70e58d86a72d943857c4c6045b3ee" - ], - "markers": "python_version >= '3.6'", - "version": "==36.0.1" - }, - "deprecated": { - "hashes": [ - "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d", - "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.2.13" - }, - "execnet": { - "hashes": [ - "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5", - "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.9.0" - }, - "fakeredis": { - "hashes": [ - "sha256:7c2c4ba1b42e0a75337c54b777bf0671056b4569650e3ff927e4b9b385afc8ec", - "sha256:be3668e50f6b57d5fc4abfd27f9f655bed07a2c5aecfc8b15d0aad59f997c1ba" - ], - "index": "pypi", - "version": "==1.7.1" - }, - "flake8": { - "hashes": [ - "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d", - "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d" - ], - "index": "pypi", - "version": "==4.0.1" - }, - "flake8-datetimez": { - "hashes": [ - "sha256:57aa2f55eb88797e2d8c06bd536ff8049b9f1ba877d81dc06ff8d9bdc195c1fc", - "sha256:78939f3bcbe2b7fe48235998545c869c27cdfac3f45685099a3f7366c1ffebc6" - ], - "index": "pypi", - "version": "==20.10.0" - }, - "flake8-debugger": { - "hashes": [ - "sha256:82e64faa72e18d1bdd0000407502ebb8ecffa7bc027c62b9d4110ce27c091032", - "sha256:e43dc777f7db1481db473210101ec2df2bd39a45b149d7218a618e954177eda6" - ], - "index": "pypi", - "version": "==4.0.0" - }, - "flake8-mock": { - "hashes": [ - "sha256:2fa775e7589f4e1ad74f35d60953eb20937f5d7355235e54bf852c6837f2bede" - ], - "index": "pypi", - "version": "==0.3" - }, - "flake8-print": { - "hashes": [ - "sha256:5afac374b7dc49aac2c36d04b5eb1d746d72e6f5df75a6ecaecd99e9f79c6516", - "sha256:6c0efce658513169f96d7a24cf136c434dc711eb00ebd0a985eb1120103fe584" - ], - "index": "pypi", - "version": "==4.0.0" - }, - "flake8-tuple": { - "hashes": [ - "sha256:8a1b42aab134ef4c3fef13c6a8f383363f158b19fbc165bd91aed9c51851a61d", - "sha256:d828cc8e461c50cacca116e9abb0c9e3be565e8451d3f5c00578c63670aae680" - ], - "index": "pypi", - "version": "==0.4.1" - }, - "flask": { - "hashes": [ - "sha256:59da8a3170004800a2837844bfa84d49b022550616070f7cb1a659682b2e7c9f", - "sha256:e1120c228ca2f553b470df4a5fa927ab66258467526069981b3eb0a91902687d" - ], - "index": "pypi", - "version": "==2.0.3" - }, - "freezegun": { - "hashes": [ - "sha256:177f9dd59861d871e27a484c3332f35a6e3f5d14626f2bf91be37891f18927f3", - "sha256:2ae695f7eb96c62529f03a038461afe3c692db3465e215355e1bb4b0ab408712" - ], - "index": "pypi", - "version": "==1.1.0" - }, - "httmock": { - "hashes": [ - "sha256:13e6c63f135a928e15d386af789a2890efb03e0e280f29bdc9961f3f0dc34cb9", - "sha256:44eaf4bb59cc64cd6f5d8bf8700b46aa3097cc5651b9bc85c527dfbc71792f41" - ], - "index": "pypi", - "version": "==1.4.0" - }, - "idna": { - "hashes": [ - "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", - "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" - ], - "markers": "python_version >= '3.5'", - "version": "==3.3" - }, - "iniconfig": { - "hashes": [ - "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", - "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" - ], - "version": "==1.1.1" - }, - "isort": { - "hashes": [ - "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7", - "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951" - ], - "markers": "python_version < '4' and python_full_version >= '3.6.1'", - "version": "==5.10.1" - }, - "itsdangerous": { - "hashes": [ - "sha256:29285842166554469a56d427addc0843914172343784cb909695fdbe90a3e129", - "sha256:d848fcb8bc7d507c4546b448574e8a44fc4ea2ba84ebf8d783290d53e81992f5" - ], - "index": "pypi", - "version": "==2.1.0" - }, - "jinja2": { - "hashes": [ - "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8", - "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7" - ], - "markers": "python_version >= '3.6'", - "version": "==3.0.3" - }, - "jmespath": { - "hashes": [ - "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", - "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.10.0" - }, - "jsonschema": { - "hashes": [ - "sha256:636694eb41b3535ed608fe04129f26542b59ed99808b4f688aa32dcf55317a83", - "sha256:77281a1f71684953ee8b3d488371b162419767973789272434bbc3f29d9c8823" - ], - "index": "pypi", - "version": "==4.4.0" - }, - "lazy-object-proxy": { - "hashes": [ - "sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7", - "sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a", - "sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c", - "sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc", - "sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f", - "sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09", - "sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442", - "sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e", - "sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029", - "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61", - "sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb", - "sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0", - "sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35", - "sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42", - "sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1", - "sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad", - "sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443", - "sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd", - "sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9", - "sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148", - "sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38", - "sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55", - "sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36", - "sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a", - "sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b", - "sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44", - "sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6", - "sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69", - "sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4", - "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84", - "sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de", - "sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28", - "sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c", - "sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1", - "sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8", - "sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b", - "sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb" - ], - "markers": "python_version >= '3.6'", - "version": "==1.7.1" - }, - "markupsafe": { - "hashes": [ - "sha256:023af8c54fe63530545f70dd2a2a7eed18d07a9a77b94e8bf1e2ff7f252db9a3", - "sha256:09c86c9643cceb1d87ca08cdc30160d1b7ab49a8a21564868921959bd16441b8", - "sha256:142119fb14a1ef6d758912b25c4e803c3ff66920635c44078666fe7cc3f8f759", - "sha256:1d1fb9b2eec3c9714dd936860850300b51dbaa37404209c8d4cb66547884b7ed", - "sha256:204730fd5fe2fe3b1e9ccadb2bd18ba8712b111dcabce185af0b3b5285a7c989", - "sha256:24c3be29abb6b34052fd26fc7a8e0a49b1ee9d282e3665e8ad09a0a68faee5b3", - "sha256:290b02bab3c9e216da57c1d11d2ba73a9f73a614bbdcc027d299a60cdfabb11a", - "sha256:3028252424c72b2602a323f70fbf50aa80a5d3aa616ea6add4ba21ae9cc9da4c", - "sha256:30c653fde75a6e5eb814d2a0a89378f83d1d3f502ab710904ee585c38888816c", - "sha256:3cace1837bc84e63b3fd2dfce37f08f8c18aeb81ef5cf6bb9b51f625cb4e6cd8", - "sha256:4056f752015dfa9828dce3140dbadd543b555afb3252507348c493def166d454", - "sha256:454ffc1cbb75227d15667c09f164a0099159da0c1f3d2636aa648f12675491ad", - "sha256:598b65d74615c021423bd45c2bc5e9b59539c875a9bdb7e5f2a6b92dfcfc268d", - "sha256:599941da468f2cf22bf90a84f6e2a65524e87be2fce844f96f2dd9a6c9d1e635", - "sha256:5ddea4c352a488b5e1069069f2f501006b1a4362cb906bee9a193ef1245a7a61", - "sha256:62c0285e91414f5c8f621a17b69fc0088394ccdaa961ef469e833dbff64bd5ea", - "sha256:679cbb78914ab212c49c67ba2c7396dc599a8479de51b9a87b174700abd9ea49", - "sha256:6e104c0c2b4cd765b4e83909cde7ec61a1e313f8a75775897db321450e928cce", - "sha256:736895a020e31b428b3382a7887bfea96102c529530299f426bf2e636aacec9e", - "sha256:75bb36f134883fdbe13d8e63b8675f5f12b80bb6627f7714c7d6c5becf22719f", - "sha256:7d2f5d97fcbd004c03df8d8fe2b973fe2b14e7bfeb2cfa012eaa8759ce9a762f", - "sha256:80beaf63ddfbc64a0452b841d8036ca0611e049650e20afcb882f5d3c266d65f", - "sha256:84ad5e29bf8bab3ad70fd707d3c05524862bddc54dc040982b0dbcff36481de7", - "sha256:8da5924cb1f9064589767b0f3fc39d03e3d0fb5aa29e0cb21d43106519bd624a", - "sha256:961eb86e5be7d0973789f30ebcf6caab60b844203f4396ece27310295a6082c7", - "sha256:96de1932237abe0a13ba68b63e94113678c379dca45afa040a17b6e1ad7ed076", - "sha256:a0a0abef2ca47b33fb615b491ce31b055ef2430de52c5b3fb19a4042dbc5cadb", - "sha256:b2a5a856019d2833c56a3dcac1b80fe795c95f401818ea963594b345929dffa7", - "sha256:b8811d48078d1cf2a6863dafb896e68406c5f513048451cd2ded0473133473c7", - "sha256:c532d5ab79be0199fa2658e24a02fce8542df196e60665dd322409a03db6a52c", - "sha256:d3b64c65328cb4cd252c94f83e66e3d7acf8891e60ebf588d7b493a55a1dbf26", - "sha256:d4e702eea4a2903441f2735799d217f4ac1b55f7d8ad96ab7d4e25417cb0827c", - "sha256:d5653619b3eb5cbd35bfba3c12d575db2a74d15e0e1c08bf1db788069d410ce8", - "sha256:d66624f04de4af8bbf1c7f21cc06649c1c69a7f84109179add573ce35e46d448", - "sha256:e67ec74fada3841b8c5f4c4f197bea916025cb9aa3fe5abf7d52b655d042f956", - "sha256:e6f7f3f41faffaea6596da86ecc2389672fa949bd035251eab26dc6697451d05", - "sha256:f02cf7221d5cd915d7fa58ab64f7ee6dd0f6cddbb48683debf5d04ae9b1c2cc1", - "sha256:f0eddfcabd6936558ec020130f932d479930581171368fd728efcfb6ef0dd357", - "sha256:fabbe18087c3d33c5824cb145ffca52eccd053061df1d79d4b66dafa5ad2a5ea", - "sha256:fc3150f85e2dbcf99e65238c842d1cfe69d3e7649b19864c1cc043213d9cd730" - ], - "index": "pypi", - "version": "==2.1.0" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, - "mock": { - "hashes": [ - "sha256:122fcb64ee37cfad5b3f48d7a7d51875d7031aaf3d8be7c42e2bee25044eee62", - "sha256:7d3fbbde18228f4ff2f1f119a45cdffa458b4c0dee32eb4d2bb2f82554bac7bc" - ], - "index": "pypi", - "version": "==4.0.3" - }, - "moto": { - "hashes": [ - "sha256:168b8a3cb4dd8a6df8e51d582761cefa9657b9f45ac7e1eb24dae394ebc9e000", - "sha256:79646213d8438385182f4eea79e28725f94b3d0d3dc9a3eda81db47e0ebef6cc" - ], - "index": "pypi", - "version": "==3.0.4" - }, - "mypy": { - "hashes": [ - "sha256:0038b21890867793581e4cb0d810829f5fd4441aa75796b53033af3aa30430ce", - "sha256:1171f2e0859cfff2d366da2c7092b06130f232c636a3f7301e3feb8b41f6377d", - "sha256:1b06268df7eb53a8feea99cbfff77a6e2b205e70bf31743e786678ef87ee8069", - "sha256:1b65714dc296a7991000b6ee59a35b3f550e0073411ac9d3202f6516621ba66c", - "sha256:1bf752559797c897cdd2c65f7b60c2b6969ffe458417b8d947b8340cc9cec08d", - "sha256:300717a07ad09525401a508ef5d105e6b56646f7942eb92715a1c8d610149714", - "sha256:3c5b42d0815e15518b1f0990cff7a705805961613e701db60387e6fb663fe78a", - "sha256:4365c60266b95a3f216a3047f1d8e3f895da6c7402e9e1ddfab96393122cc58d", - "sha256:50c7346a46dc76a4ed88f3277d4959de8a2bd0a0fa47fa87a4cde36fe247ac05", - "sha256:5b56154f8c09427bae082b32275a21f500b24d93c88d69a5e82f3978018a0266", - "sha256:74f7eccbfd436abe9c352ad9fb65872cc0f1f0a868e9d9c44db0893440f0c697", - "sha256:7b3f6f557ba4afc7f2ce6d3215d5db279bcf120b3cfd0add20a5d4f4abdae5bc", - "sha256:8c11003aaeaf7cc2d0f1bc101c1cc9454ec4cc9cb825aef3cafff8a5fdf4c799", - "sha256:8ca7f8c4b1584d63c9a0f827c37ba7a47226c19a23a753d52e5b5eddb201afcd", - "sha256:c89702cac5b302f0c5d33b172d2b55b5df2bede3344a2fbed99ff96bddb2cf00", - "sha256:d8f1ff62f7a879c9fe5917b3f9eb93a79b78aad47b533911b853a757223f72e7", - "sha256:d9d2b84b2007cea426e327d2483238f040c49405a6bf4074f605f0156c91a47a", - "sha256:e839191b8da5b4e5d805f940537efcaa13ea5dd98418f06dc585d2891d228cf0", - "sha256:f9fe20d0872b26c4bba1c1be02c5340de1019530302cf2dcc85c7f9fc3252ae0", - "sha256:ff3bf387c14c805ab1388185dd22d6b210824e164d4bb324b195ff34e322d166" - ], - "index": "pypi", - "version": "==0.931" - }, - "mypy-extensions": { - "hashes": [ - "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", - "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" - ], - "version": "==0.4.3" - }, - "packaging": { - "hashes": [ - "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", - "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" - ], - "markers": "python_version >= '3.6'", - "version": "==21.3" - }, - "pathspec": { - "hashes": [ - "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", - "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1" - ], - "version": "==0.9.0" - }, - "pep8": { - "hashes": [ - "sha256:b22cfae5db09833bb9bd7c8463b53e1a9c9b39f12e304a8d0bba729c501827ee", - "sha256:fe249b52e20498e59e0b5c5256aa52ee99fc295b26ec9eaa85776ffdb9fe6374" - ], - "index": "pypi", - "version": "==1.7.1" - }, - "platformdirs": { - "hashes": [ - "sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d", - "sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227" - ], - "markers": "python_version >= '3.7'", - "version": "==2.5.1" - }, - "pluggy": { - "hashes": [ - "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", - "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" - ], - "markers": "python_version >= '3.6'", - "version": "==1.0.0" - }, - "py": { - "hashes": [ - "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", - "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.11.0" - }, - "pycodestyle": { - "hashes": [ - "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20", - "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==2.8.0" - }, - "pycparser": { - "hashes": [ - "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", - "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.21" - }, - "pyflakes": { - "hashes": [ - "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c", - "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.4.0" - }, - "pylint": { - "hashes": [ - "sha256:9d945a73640e1fec07ee34b42f5669b770c759acd536ec7b16d7e4b87a9c9ff9", - "sha256:daabda3f7ed9d1c60f52d563b1b854632fd90035bcf01443e234d3dc794e3b74" - ], - "index": "pypi", - "version": "==2.12.2" - }, - "pylint-mccabe": { - "hashes": [ - "sha256:f3628affbc6064c08477243915f6752f3ef59fb82803b00be92f30d0ef7bbf29" - ], - "index": "pypi", - "version": "==0.1.3" - }, - "pylint-quotes": { - "hashes": [ - "sha256:2d6bb3fa8a1a85af3af8a0ca875a719ac5bcdb735c45756284699d809c109c95", - "sha256:89decd985d3c019314da630f5e3fe0e0df951c2310525fbd6e710bca329c810e" - ], - "index": "pypi", - "version": "==0.2.3" - }, - "pyparsing": { - "hashes": [ - "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea", - "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484" - ], - "markers": "python_version >= '3.6'", - "version": "==3.0.7" - }, - "pyrsistent": { - "hashes": [ - "sha256:0e3e1fcc45199df76053026a51cc59ab2ea3fc7c094c6627e93b7b44cdae2c8c", - "sha256:1b34eedd6812bf4d33814fca1b66005805d3640ce53140ab8bbb1e2651b0d9bc", - "sha256:4ed6784ceac462a7d6fcb7e9b663e93b9a6fb373b7f43594f9ff68875788e01e", - "sha256:5d45866ececf4a5fff8742c25722da6d4c9e180daa7b405dc0a2a2790d668c26", - "sha256:636ce2dc235046ccd3d8c56a7ad54e99d5c1cd0ef07d9ae847306c91d11b5fec", - "sha256:6455fc599df93d1f60e1c5c4fe471499f08d190d57eca040c0ea182301321286", - "sha256:6bc66318fb7ee012071b2792024564973ecc80e9522842eb4e17743604b5e045", - "sha256:7bfe2388663fd18bd8ce7db2c91c7400bf3e1a9e8bd7d63bf7e77d39051b85ec", - "sha256:7ec335fc998faa4febe75cc5268a9eac0478b3f681602c1f27befaf2a1abe1d8", - "sha256:914474c9f1d93080338ace89cb2acee74f4f666fb0424896fcfb8d86058bf17c", - "sha256:b568f35ad53a7b07ed9b1b2bae09eb15cdd671a5ba5d2c66caee40dbf91c68ca", - "sha256:cdfd2c361b8a8e5d9499b9082b501c452ade8bbf42aef97ea04854f4a3f43b22", - "sha256:d1b96547410f76078eaf66d282ddca2e4baae8964364abb4f4dcdde855cd123a", - "sha256:d4d61f8b993a7255ba714df3aca52700f8125289f84f704cf80916517c46eb96", - "sha256:d7a096646eab884bf8bed965bad63ea327e0d0c38989fc83c5ea7b8a87037bfc", - "sha256:df46c854f490f81210870e509818b729db4488e1f30f2a1ce1698b2295a878d1", - "sha256:e24a828f57e0c337c8d8bb9f6b12f09dfdf0273da25fda9e314f0b684b415a07", - "sha256:e4f3149fd5eb9b285d6bfb54d2e5173f6a116fe19172686797c056672689daf6", - "sha256:e92a52c166426efbe0d1ec1332ee9119b6d32fc1f0bbfd55d5c1088070e7fc1b", - "sha256:f87cc2863ef33c709e237d4b5f4502a62a00fab450c9e020892e8e2ede5847f5", - "sha256:fd8da6d0124efa2f67d86fa70c851022f87c98e205f0594e1fae044e7119a5a6" - ], - "markers": "python_version >= '3.7'", - "version": "==0.18.1" - }, - "pytest": { - "hashes": [ - "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db", - "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171" - ], - "index": "pypi", - "version": "==7.0.1" - }, - "pytest-cov": { - "hashes": [ - "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6", - "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470" - ], - "index": "pypi", - "version": "==3.0.0" - }, - "pytest-flask": { - "hashes": [ - "sha256:46fde652f77777bf02dc91205aec4ce20cdf2acbbbd66a918ab91f5c14693d3d", - "sha256:fe25b39ad0db09c3d1fe728edecf97ced85e774c775db259a6d25f0270a4e7c9" - ], - "index": "pypi", - "version": "==1.2.0" - }, - "pytest-forked": { - "hashes": [ - "sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e", - "sha256:bbbb6717efc886b9d64537b41fb1497cfaf3c9601276be8da2cccfea5a3c8ad8" - ], - "markers": "python_version >= '3.6'", - "version": "==1.4.0" - }, - "pytest-mock": { - "hashes": [ - "sha256:5112bd92cc9f186ee96e1a92efc84969ea494939c3aead39c50f421c4cc69534", - "sha256:6cff27cec936bf81dc5ee87f07132b807bcda51106b5ec4b90a04331cba76231" - ], - "index": "pypi", - "version": "==3.7.0" - }, - "pytest-sugar": { - "hashes": [ - "sha256:b1b2186b0a72aada6859bea2a5764145e3aaa2c1cfbb23c3a19b5f7b697563d3" - ], - "index": "pypi", - "version": "==0.9.4" - }, - "pytest-xdist": { - "hashes": [ - "sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf", - "sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65" - ], - "index": "pypi", - "version": "==2.5.0" - }, - "python-dateutil": { - "hashes": [ - "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", - "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.8.2" - }, - "pytz": { - "hashes": [ - "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c", - "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326" - ], - "version": "==2021.3" - }, - "redis": { - "hashes": [ - "sha256:04629f8e42be942c4f7d1812f2094568f04c612865ad19ad3ace3005da70631a", - "sha256:1d9a0cdf89fdd93f84261733e24f55a7bbd413a9b219fdaf56e3e728ca9a2306" - ], - "index": "pypi", - "version": "==4.1.4" - }, - "regex": { - "hashes": [ - "sha256:04611cc0f627fc4a50bc4a9a2e6178a974c6a6a4aa9c1cca921635d2c47b9c87", - "sha256:0b5d6f9aed3153487252d00a18e53f19b7f52a1651bc1d0c4b5844bc286dfa52", - "sha256:0d2f5c3f7057530afd7b739ed42eb04f1011203bc5e4663e1e1d01bb50f813e3", - "sha256:11772be1eb1748e0e197a40ffb82fb8fd0d6914cd147d841d9703e2bef24d288", - "sha256:1333b3ce73269f986b1fa4d5d395643810074dc2de5b9d262eb258daf37dc98f", - "sha256:16f81025bb3556eccb0681d7946e2b35ff254f9f888cff7d2120e8826330315c", - "sha256:1a171eaac36a08964d023eeff740b18a415f79aeb212169080c170ec42dd5184", - "sha256:1d6301f5288e9bdca65fab3de6b7de17362c5016d6bf8ee4ba4cbe833b2eda0f", - "sha256:1e031899cb2bc92c0cf4d45389eff5b078d1936860a1be3aa8c94fa25fb46ed8", - "sha256:1f8c0ae0a0de4e19fddaaff036f508db175f6f03db318c80bbc239a1def62d02", - "sha256:2245441445099411b528379dee83e56eadf449db924648e5feb9b747473f42e3", - "sha256:22709d701e7037e64dae2a04855021b62efd64a66c3ceed99dfd684bfef09e38", - "sha256:24c89346734a4e4d60ecf9b27cac4c1fee3431a413f7aa00be7c4d7bbacc2c4d", - "sha256:25716aa70a0d153cd844fe861d4f3315a6ccafce22b39d8aadbf7fcadff2b633", - "sha256:2dacb3dae6b8cc579637a7b72f008bff50a94cde5e36e432352f4ca57b9e54c4", - "sha256:34316bf693b1d2d29c087ee7e4bb10cdfa39da5f9c50fa15b07489b4ab93a1b5", - "sha256:36b2d700a27e168fa96272b42d28c7ac3ff72030c67b32f37c05616ebd22a202", - "sha256:37978254d9d00cda01acc1997513f786b6b971e57b778fbe7c20e30ae81a97f3", - "sha256:38289f1690a7e27aacd049e420769b996826f3728756859420eeee21cc857118", - "sha256:385ccf6d011b97768a640e9d4de25412204fbe8d6b9ae39ff115d4ff03f6fe5d", - "sha256:3c7ea86b9ca83e30fa4d4cd0eaf01db3ebcc7b2726a25990966627e39577d729", - "sha256:49810f907dfe6de8da5da7d2b238d343e6add62f01a15d03e2195afc180059ed", - "sha256:519c0b3a6fbb68afaa0febf0d28f6c4b0a1074aefc484802ecb9709faf181607", - "sha256:51f02ca184518702975b56affde6c573ebad4e411599005ce4468b1014b4786c", - "sha256:552a39987ac6655dad4bf6f17dd2b55c7b0c6e949d933b8846d2e312ee80005a", - "sha256:596f5ae2eeddb79b595583c2e0285312b2783b0ec759930c272dbf02f851ff75", - "sha256:6014038f52b4b2ac1fa41a58d439a8a00f015b5c0735a0cd4b09afe344c94899", - "sha256:61ebbcd208d78658b09e19c78920f1ad38936a0aa0f9c459c46c197d11c580a0", - "sha256:6213713ac743b190ecbf3f316d6e41d099e774812d470422b3a0f137ea635832", - "sha256:637e27ea1ebe4a561db75a880ac659ff439dec7f55588212e71700bb1ddd5af9", - "sha256:6aa427c55a0abec450bca10b64446331b5ca8f79b648531138f357569705bc4a", - "sha256:6ca45359d7a21644793de0e29de497ef7f1ae7268e346c4faf87b421fea364e6", - "sha256:6db1b52c6f2c04fafc8da17ea506608e6be7086715dab498570c3e55e4f8fbd1", - "sha256:752e7ddfb743344d447367baa85bccd3629c2c3940f70506eb5f01abce98ee68", - "sha256:760c54ad1b8a9b81951030a7e8e7c3ec0964c1cb9fee585a03ff53d9e531bb8e", - "sha256:768632fd8172ae03852e3245f11c8a425d95f65ff444ce46b3e673ae5b057b74", - "sha256:7a0b9f6a1a15d494b35f25ed07abda03209fa76c33564c09c9e81d34f4b919d7", - "sha256:7e070d3aef50ac3856f2ef5ec7214798453da878bb5e5a16c16a61edf1817cc3", - "sha256:7e12949e5071c20ec49ef00c75121ed2b076972132fc1913ddf5f76cae8d10b4", - "sha256:7e26eac9e52e8ce86f915fd33380f1b6896a2b51994e40bb094841e5003429b4", - "sha256:85ffd6b1cb0dfb037ede50ff3bef80d9bf7fa60515d192403af6745524524f3b", - "sha256:8618d9213a863c468a865e9d2ec50221015f7abf52221bc927152ef26c484b4c", - "sha256:8acef4d8a4353f6678fd1035422a937c2170de58a2b29f7da045d5249e934101", - "sha256:8d2f355a951f60f0843f2368b39970e4667517e54e86b1508e76f92b44811a8a", - "sha256:90b6840b6448203228a9d8464a7a0d99aa8fa9f027ef95fe230579abaf8a6ee1", - "sha256:9187500d83fd0cef4669385cbb0961e227a41c0c9bc39219044e35810793edf7", - "sha256:93c20777a72cae8620203ac11c4010365706062aa13aaedd1a21bb07adbb9d5d", - "sha256:93cce7d422a0093cfb3606beae38a8e47a25232eea0f292c878af580a9dc7605", - "sha256:94c623c331a48a5ccc7d25271399aff29729fa202c737ae3b4b28b89d2b0976d", - "sha256:97f32dc03a8054a4c4a5ab5d761ed4861e828b2c200febd4e46857069a483916", - "sha256:9a2bf98ac92f58777c0fafc772bf0493e67fcf677302e0c0a630ee517a43b949", - "sha256:a602bdc8607c99eb5b391592d58c92618dcd1537fdd87df1813f03fed49957a6", - "sha256:a9d24b03daf7415f78abc2d25a208f234e2c585e5e6f92f0204d2ab7b9ab48e3", - "sha256:abfcb0ef78df0ee9df4ea81f03beea41849340ce33a4c4bd4dbb99e23ec781b6", - "sha256:b013f759cd69cb0a62de954d6d2096d648bc210034b79b1881406b07ed0a83f9", - "sha256:b02e3e72665cd02afafb933453b0c9f6c59ff6e3708bd28d0d8580450e7e88af", - "sha256:b52cc45e71657bc4743a5606d9023459de929b2a198d545868e11898ba1c3f59", - "sha256:ba37f11e1d020969e8a779c06b4af866ffb6b854d7229db63c5fdddfceaa917f", - "sha256:bb804c7d0bfbd7e3f33924ff49757de9106c44e27979e2492819c16972ec0da2", - "sha256:bf594cc7cc9d528338d66674c10a5b25e3cde7dd75c3e96784df8f371d77a298", - "sha256:c38baee6bdb7fe1b110b6b3aaa555e6e872d322206b7245aa39572d3fc991ee4", - "sha256:c73d2166e4b210b73d1429c4f1ca97cea9cc090e5302df2a7a0a96ce55373f1c", - "sha256:c9099bf89078675c372339011ccfc9ec310310bf6c292b413c013eb90ffdcafc", - "sha256:cf0db26a1f76aa6b3aa314a74b8facd586b7a5457d05b64f8082a62c9c49582a", - "sha256:d19a34f8a3429bd536996ad53597b805c10352a8561d8382e05830df389d2b43", - "sha256:da80047524eac2acf7c04c18ac7a7da05a9136241f642dd2ed94269ef0d0a45a", - "sha256:de2923886b5d3214be951bc2ce3f6b8ac0d6dfd4a0d0e2a4d2e5523d8046fdfb", - "sha256:defa0652696ff0ba48c8aff5a1fac1eef1ca6ac9c660b047fc8e7623c4eb5093", - "sha256:e54a1eb9fd38f2779e973d2f8958fd575b532fe26013405d1afb9ee2374e7ab8", - "sha256:e5c31d70a478b0ca22a9d2d76d520ae996214019d39ed7dd93af872c7f301e52", - "sha256:ebaeb93f90c0903233b11ce913a7cb8f6ee069158406e056f884854c737d2442", - "sha256:ecfe51abf7f045e0b9cdde71ca9e153d11238679ef7b5da6c82093874adf3338", - "sha256:f99112aed4fb7cee00c7f77e8b964a9b10f69488cdff626ffd797d02e2e4484f", - "sha256:fd914db437ec25bfa410f8aa0aa2f3ba87cdfc04d9919d608d02330947afaeab" - ], - "version": "==2022.1.18" - }, - "requests": { - "hashes": [ - "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", - "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d" - ], - "index": "pypi", - "version": "==2.27.1" - }, - "responses": { - "hashes": [ - "sha256:15c63ad16de13ee8e7182d99c9334f64fd81f1ee79f90748d527c28f7ca9dd51", - "sha256:380cad4c1c1dc942e5e8a8eaae0b4d4edf708f4f010db8b7bcfafad1fcd254ff" - ], - "index": "pypi", - "version": "==0.18.0" - }, - "s3transfer": { - "hashes": [ - "sha256:25c140f5c66aa79e1ac60be50dcd45ddc59e83895f062a3aab263b870102911f", - "sha256:69d264d3e760e569b78aaa0f22c97e955891cd22e32b10c51f784eeda4d9d10a" - ], - "markers": "python_version >= '3.6'", - "version": "==0.5.1" - }, - "setuptools": { - "hashes": [ - "sha256:2347b2b432c891a863acadca2da9ac101eae6169b1d3dfee2ec605ecd50dbfe5", - "sha256:e4f30b9f84e5ab3decf945113119649fec09c1fc3507c6ebffec75646c56e62b" - ], - "markers": "python_version >= '3.7'", - "version": "==60.9.3" - }, - "six": { - "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" - }, - "sortedcontainers": { - "hashes": [ - "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", - "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0" - ], - "version": "==2.4.0" - }, - "soupsieve": { - "hashes": [ - "sha256:1a3cca2617c6b38c0343ed661b1fa5de5637f257d4fe22bd9f1338010a1efefb", - "sha256:b8d49b1cd4f037c7082a9683dfa1801aa2597fb11c3a1155b7a5b94829b4f1f9" - ], - "markers": "python_version >= '3.6'", - "version": "==2.3.1" - }, - "termcolor": { - "hashes": [ - "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b" - ], - "version": "==1.1.0" - }, - "toml": { - "hashes": [ - "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", - "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.10.2" - }, - "tomli": { - "hashes": [ - "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", - "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" - ], - "markers": "python_version >= '3.7'", - "version": "==2.0.1" - }, - "typed-ast": { - "hashes": [ - "sha256:0eb77764ea470f14fcbb89d51bc6bbf5e7623446ac4ed06cbd9ca9495b62e36e", - "sha256:1098df9a0592dd4c8c0ccfc2e98931278a6c6c53cb3a3e2cf7e9ee3b06153344", - "sha256:183b183b7771a508395d2cbffd6db67d6ad52958a5fdc99f450d954003900266", - "sha256:18fe320f354d6f9ad3147859b6e16649a0781425268c4dde596093177660e71a", - "sha256:26a432dc219c6b6f38be20a958cbe1abffcc5492821d7e27f08606ef99e0dffd", - "sha256:294a6903a4d087db805a7656989f613371915fc45c8cc0ddc5c5a0a8ad9bea4d", - "sha256:31d8c6b2df19a777bc8826770b872a45a1f30cfefcfd729491baa5237faae837", - "sha256:33b4a19ddc9fc551ebabca9765d54d04600c4a50eda13893dadf67ed81d9a098", - "sha256:42c47c3b43fe3a39ddf8de1d40dbbfca60ac8530a36c9b198ea5b9efac75c09e", - "sha256:525a2d4088e70a9f75b08b3f87a51acc9cde640e19cc523c7e41aa355564ae27", - "sha256:58ae097a325e9bb7a684572d20eb3e1809802c5c9ec7108e85da1eb6c1a3331b", - "sha256:676d051b1da67a852c0447621fdd11c4e104827417bf216092ec3e286f7da596", - "sha256:74cac86cc586db8dfda0ce65d8bcd2bf17b58668dfcc3652762f3ef0e6677e76", - "sha256:8c08d6625bb258179b6e512f55ad20f9dfef019bbfbe3095247401e053a3ea30", - "sha256:90904d889ab8e81a956f2c0935a523cc4e077c7847a836abee832f868d5c26a4", - "sha256:963a0ccc9a4188524e6e6d39b12c9ca24cc2d45a71cfdd04a26d883c922b4b78", - "sha256:bbebc31bf11762b63bf61aaae232becb41c5bf6b3461b80a4df7e791fabb3aca", - "sha256:bc2542e83ac8399752bc16e0b35e038bdb659ba237f4222616b4e83fb9654985", - "sha256:c29dd9a3a9d259c9fa19d19738d021632d673f6ed9b35a739f48e5f807f264fb", - "sha256:c7407cfcad702f0b6c0e0f3e7ab876cd1d2c13b14ce770e412c0c4b9728a0f88", - "sha256:da0a98d458010bf4fe535f2d1e367a2e2060e105978873c04c04212fb20543f7", - "sha256:df05aa5b241e2e8045f5f4367a9f6187b09c4cdf8578bb219861c4e27c443db5", - "sha256:f290617f74a610849bd8f5514e34ae3d09eafd521dceaa6cf68b3f4414266d4e", - "sha256:f30ddd110634c2d7534b2d4e0e22967e88366b0d356b24de87419cc4410c41b7" - ], - "markers": "python_version >= '3.6'", - "version": "==1.5.2" - }, - "types-python-dateutil": { - "hashes": [ - "sha256:90f95a6b6d4faba359287f17a2cae511ccc9d4abc89b01969bdac1185815c05d", - "sha256:d60db7f5d40ce85ce54e7fb14e4157daf33e24f5a4bfb5f44ee7a5b790dfabd0" - ], - "index": "pypi", - "version": "==2.8.9" - }, - "types-pyyaml": { - "hashes": [ - "sha256:6252f62d785e730e454dfa0c9f0fb99d8dae254c5c3c686903cf878ea27c04b7", - "sha256:693b01c713464a6851f36ff41077f8adbc6e355eda929addfb4a97208aea9b4b" - ], - "index": "pypi", - "version": "==6.0.4" - }, - "types-redis": { - "hashes": [ - "sha256:5c8707423c60e70ba6ff9a5f01baacbb6c871e44f6a2bd562790cee9edd5b5b1", - "sha256:7e98c567f0e279b47b0a0ddee8c0180a086e4a5f1b95e6890b40b2a84dc97fb6" - ], - "index": "pypi", - "version": "==4.1.17" - }, - "types-requests": { - "hashes": [ - "sha256:506279bad570c7b4b19ac1f22e50146538befbe0c133b2cea66a9b04a533a859", - "sha256:6a7ed24b21780af4a5b5e24c310b2cd885fb612df5fd95584d03d87e5f2a195a" - ], - "index": "pypi", - "version": "==2.27.11" - }, - "types-simplejson": { - "hashes": [ - "sha256:95c2b53e6492226461db360ee94012196c2a3ca3f06511b38902ad0ee6609f5a", - "sha256:cb50282bc3319e99ed345af7343ece6e7f14d1c57b3bc41e4288a5b4a3c53253" - ], - "index": "pypi", - "version": "==3.17.3" - }, - "types-urllib3": { - "hashes": [ - "sha256:4a54f6274ab1c80968115634a55fb9341a699492b95e32104a7c513db9fe02e9", - "sha256:abd2d4857837482b1834b4817f0587678dcc531dbc9abe4cde4da28cef3f522c" - ], - "version": "==1.26.9" - }, - "typing-extensions": { - "hashes": [ - "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42", - "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2" - ], - "markers": "python_version >= '3.6'", - "version": "==4.1.1" - }, - "urllib3": { - "hashes": [ - "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed", - "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4.0'", - "version": "==1.26.8" - }, - "werkzeug": { - "hashes": [ - "sha256:1421ebfc7648a39a5c58c601b154165d05cf47a3cd0ccb70857cbdacf6c8f2b8", - "sha256:b863f8ff057c522164b6067c9e28b041161b4be5ba4d0daceeaa50a163822d3c" - ], - "markers": "python_version >= '3.6'", - "version": "==2.0.3" - }, - "wrapt": { - "hashes": [ - "sha256:086218a72ec7d986a3eddb7707c8c4526d677c7b35e355875a0fe2918b059179", - "sha256:0877fe981fd76b183711d767500e6b3111378ed2043c145e21816ee589d91096", - "sha256:0a017a667d1f7411816e4bf214646d0ad5b1da2c1ea13dec6c162736ff25a374", - "sha256:0cb23d36ed03bf46b894cfec777eec754146d68429c30431c99ef28482b5c1df", - "sha256:1fea9cd438686e6682271d36f3481a9f3636195578bab9ca3382e2f5f01fc185", - "sha256:220a869982ea9023e163ba915077816ca439489de6d2c09089b219f4e11b6785", - "sha256:25b1b1d5df495d82be1c9d2fad408f7ce5ca8a38085e2da41bb63c914baadff7", - "sha256:2dded5496e8f1592ec27079b28b6ad2a1ef0b9296d270f77b8e4a3a796cf6909", - "sha256:2ebdde19cd3c8cdf8df3fc165bc7827334bc4e353465048b36f7deeae8ee0918", - "sha256:43e69ffe47e3609a6aec0fe723001c60c65305784d964f5007d5b4fb1bc6bf33", - "sha256:46f7f3af321a573fc0c3586612db4decb7eb37172af1bc6173d81f5b66c2e068", - "sha256:47f0a183743e7f71f29e4e21574ad3fa95676136f45b91afcf83f6a050914829", - "sha256:498e6217523111d07cd67e87a791f5e9ee769f9241fcf8a379696e25806965af", - "sha256:4b9c458732450ec42578b5642ac53e312092acf8c0bfce140ada5ca1ac556f79", - "sha256:51799ca950cfee9396a87f4a1240622ac38973b6df5ef7a41e7f0b98797099ce", - "sha256:5601f44a0f38fed36cc07db004f0eedeaadbdcec90e4e90509480e7e6060a5bc", - "sha256:5f223101f21cfd41deec8ce3889dc59f88a59b409db028c469c9b20cfeefbe36", - "sha256:610f5f83dd1e0ad40254c306f4764fcdc846641f120c3cf424ff57a19d5f7ade", - "sha256:6a03d9917aee887690aa3f1747ce634e610f6db6f6b332b35c2dd89412912bca", - "sha256:705e2af1f7be4707e49ced9153f8d72131090e52be9278b5dbb1498c749a1e32", - "sha256:766b32c762e07e26f50d8a3468e3b4228b3736c805018e4b0ec8cc01ecd88125", - "sha256:77416e6b17926d953b5c666a3cb718d5945df63ecf922af0ee576206d7033b5e", - "sha256:778fd096ee96890c10ce96187c76b3e99b2da44e08c9e24d5652f356873f6709", - "sha256:78dea98c81915bbf510eb6a3c9c24915e4660302937b9ae05a0947164248020f", - "sha256:7dd215e4e8514004c8d810a73e342c536547038fb130205ec4bba9f5de35d45b", - "sha256:7dde79d007cd6dfa65afe404766057c2409316135cb892be4b1c768e3f3a11cb", - "sha256:81bd7c90d28a4b2e1df135bfbd7c23aee3050078ca6441bead44c42483f9ebfb", - "sha256:85148f4225287b6a0665eef08a178c15097366d46b210574a658c1ff5b377489", - "sha256:865c0b50003616f05858b22174c40ffc27a38e67359fa1495605f96125f76640", - "sha256:87883690cae293541e08ba2da22cacaae0a092e0ed56bbba8d018cc486fbafbb", - "sha256:8aab36778fa9bba1a8f06a4919556f9f8c7b33102bd71b3ab307bb3fecb21851", - "sha256:8c73c1a2ec7c98d7eaded149f6d225a692caa1bd7b2401a14125446e9e90410d", - "sha256:936503cb0a6ed28dbfa87e8fcd0a56458822144e9d11a49ccee6d9a8adb2ac44", - "sha256:944b180f61f5e36c0634d3202ba8509b986b5fbaf57db3e94df11abee244ba13", - "sha256:96b81ae75591a795d8c90edc0bfaab44d3d41ffc1aae4d994c5aa21d9b8e19a2", - "sha256:981da26722bebb9247a0601e2922cedf8bb7a600e89c852d063313102de6f2cb", - "sha256:ae9de71eb60940e58207f8e71fe113c639da42adb02fb2bcbcaccc1ccecd092b", - "sha256:b73d4b78807bd299b38e4598b8e7bd34ed55d480160d2e7fdaabd9931afa65f9", - "sha256:d4a5f6146cfa5c7ba0134249665acd322a70d1ea61732723c7d3e8cc0fa80755", - "sha256:dd91006848eb55af2159375134d724032a2d1d13bcc6f81cd8d3ed9f2b8e846c", - "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a", - "sha256:e6906d6f48437dfd80464f7d7af1740eadc572b9f7a4301e7dd3d65db285cacf", - "sha256:e92d0d4fa68ea0c02d39f1e2f9cb5bc4b4a71e8c442207433d8db47ee79d7aa3", - "sha256:e94b7d9deaa4cc7bac9198a58a7240aaf87fe56c6277ee25fa5b3aa1edebd229", - "sha256:ea3e746e29d4000cd98d572f3ee2a6050a4f784bb536f4ac1f035987fc1ed83e", - "sha256:ec7e20258ecc5174029a0f391e1b948bf2906cd64c198a9b8b281b811cbc04de", - "sha256:ec9465dd69d5657b5d2fa6133b3e1e989ae27d29471a672416fd729b429eb554", - "sha256:f122ccd12fdc69628786d0c947bdd9cb2733be8f800d88b5a37c57f1f1d73c10", - "sha256:f99c0489258086308aad4ae57da9e8ecf9e1f3f30fa35d5e170b4d4896554d80", - "sha256:f9c51d9af9abb899bd34ace878fbec8bf357b3194a10c4e8e0a25512826ef056", - "sha256:fd76c47f20984b43d93de9a82011bb6e5f8325df6c9ed4d8310029a55fa361ea" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.13.3" - }, - "xmltodict": { - "hashes": [ - "sha256:50d8c638ed7ecb88d90561beedbf720c9b4e851a9fa6c47ebd64e99d166d8a21", - "sha256:8bbcb45cc982f48b2ca8fe7e7827c5d792f217ecf1792626f808bf41c3b86051" - ], - "version": "==0.12.0" - } - } -} diff --git a/README.md b/README.md index 30e043b019..7e899af335 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,36 @@ # eQ Questionnaire Runner -![Build Status](https://github.com/ONSdigital/eq-questionnaire-runner/workflows/Master/badge.svg) -[![codecov](https://codecov.io/gh/ONSdigital/eq-questionnaire-runner/branch/master/graph/badge.svg)](https://codecov.io/gh/ONSdigital/eq-questionnaire-runner/branch/master) -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/4c39ddd3285748f8bfb6b70fd5aaf9cc)](https://www.codacy.com/manual/ONSDigital/eq-questionnaire-runner?utm_source=github.com&utm_medium=referral&utm_content=ONSdigital/eq-questionnaire-runner&utm_campaign=Badge_Grade) +[![Build Status](https://github.com/ONSdigital/eq-questionnaire-runner/actions/workflows/main.yml/badge.svg)](https://github.com/ONSdigital/eq-questionnaire-runner/actions/workflows/main.yml) +[![Build Status](https://github.com/ONSdigital/eq-questionnaire-runner/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/ONSdigital/eq-questionnaire-runner/actions/workflows/codeql-analysis.yml) +![Coverage](https://img.shields.io/badge/Coverage-100%25-2FC050.svg) + +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/) +[![poetry-managed](https://img.shields.io/badge/poetry-managed-blue)](https://python-poetry.org/) +[![License - MIT](https://img.shields.io/badge/licence%20-MIT-1ac403.svg)](https://github.com/ONSdigital/eq-questionnaire-runner/blob/main/LICENSE) ## Run with Docker -Install Docker for your system: [https://www.docker.com/](https://www.docker.com/) +Install [Docker](https://www.docker.com/) for your system. Make sure that you've installed both docker and docker-compose packages, preferably using Homebrew: + +``` shell +brew install docker +brew install docker-compose +``` +On MacOS install container runtimes, eg. [Colima](https://github.com/abiosoft/colima): +```shell +brew install colima +``` + +Make sure Colima is started every time you want to use Docker images: +```shell +colima start +``` To get eq-questionnaire-runner running the following command will build and run the containers ``` shell -RUNNER_ENV_FILE=.development.env docker-compose up -d +RUNNER_ENV_FILE=.development.env docker compose up -d ``` To launch a survey, navigate to [http://localhost:8000/](http://localhost:8000/) @@ -22,13 +41,13 @@ However, any new dependencies that are added would require a re-build. To rebuild the eq-questionnaire-runner container, the following command can be used. ``` shell -RUNNER_ENV_FILE=.development.env docker-compose build +RUNNER_ENV_FILE=.development.env docker compose build ``` If you need to rebuild the container from scratch to re-load any dependencies then you can run the following ``` shell -RUNNER_ENV_FILE=.development.env docker-compose build --no-cache +RUNNER_ENV_FILE=.development.env docker compose build --no-cache ``` ## Run locally @@ -49,6 +68,8 @@ brew install snappy npm pyenv jq wkhtmltopdf ### Setup +#### Application version + Create `.application-version` for local development This file is automatically created and populated with the git revision id during CI for anything other than development, @@ -58,32 +79,71 @@ to `local` removes the implication that any particular revision is used when run ``` shell echo "local" > .application-version ``` +#### Python version It is preferable to use the version of Python locally that matches that used on deployment. This project has a `.python_version` file for this purpose. -Upgrade pip and install dependencies: +#### Pyenv + +It is recommended to install the `pyenv` Python version management tool to easily switch between Python versions. +To install `pyenv` use this command: +```shell +curl https://pyenv.run | bash +``` +After the installation it should tell you to execute a command to add `pyenv` to path. It should look something like this: +```shell +export PYENV_ROOT="$HOME/.pyenv" + +command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH" + +eval "$(pyenv init -)" +``` +Python versions can be changed with the `pyenv local` or `pyenv global` commands suffixed with the desired version (e.g. 3.12.6). Different versions of Python can be installed first with the `pyenv install` command. Refer to the pyenv project Readme [here](https://github.com/pyenv/pyenv). To avoid confusion, check the current Python version at any given time using `python --version` or `python3 --version`. + +#### Python & dependencies + +Inside the project directory install python version, upgrade pip: ``` shell pyenv install -pip install --upgrade pip setuptools pipenv -pipenv install --dev +pip install --upgrade pip setuptools ``` +Install poetry, poetry dotenv plugin and install dependencies: + +``` shell +curl -sSL https://install.python-poetry.org | python3 - --version 2.1.2 +poetry self add poetry-plugin-dotenv +poetry install +``` + +We use [poetry-plugin-up](https://github.com/MousaZeidBaker/poetry-plugin-up) to update dependencies in the `pyproject.toml` file: + +``` shell +poetry self add poetry-plugin-up +``` + +#### Design system templates + To update the design system templates run: ``` shell make load-design-system-templates ``` +#### Schemas + To download the latest schemas from the [Questionnaire Registry](https://github.com/ONSdigital/eq-questionnaire-schemas): ``` shell make load-schemas ``` -Run the server inside the virtual env created by Pipenv with: +#### Run server + +Run the server inside the virtual env created by Poetry with: ``` shell make run @@ -91,11 +151,11 @@ make run ### Supporting services -Runner requires three supporting services - a questionnaire launcher, a storage backend, and a cache. +Runner requires five supporting services - a questionnaire launcher, a storage backend, a cache, the supplementary data service and the collection instrument registry. #### Run supporting services with Docker -To run the app locally, but the supporting services in Docker, run: +To run the app locally, but the supporting services in Docker, make sure you have Docker and Colima installed [from this step](#run-with-docker), then run: ``` shell make dev-compose-up @@ -112,7 +172,19 @@ make dev-compose-up-linux ##### [Questionnaire launcher](https://github.com/ONSDigital/eq-questionnaire-launcher) ``` shell -docker run -e SURVEY_RUNNER_SCHEMA_URL=http://docker.for.mac.host.internal:5000 -it -p 8000:8000 onsdigital/eq-questionnaire-launcher:latest +docker run -e SURVEY_RUNNER_SCHEMA_URL=http://host.docker.internal:5000 -e SDS_API_BASE_URL=http://host.docker.internal:5003 -e CIR_API_BASE_URL=http://host.docker.internal:5004 -it -p 8000:8000 onsdigital/eq-questionnaire-launcher:latest +``` + +##### [Mock Supplementary data service](https://github.com/ONSDigital/eq-runner-mock-sds) + +``` shell +docker run -it -p 5003:5003 onsdigital/eq-runner-mock-sds:latest +``` + +##### [Mock Collection Instrument Registry](https://github.com/ONSDigital/eq-runner-mock-cir) + +``` shell +docker run -it -p 5004:5004 onsdigital/eq-runner-mock-cir:latest ``` ##### Storage backends @@ -149,42 +221,45 @@ Or set the `GOOGLE_CLOUD_PROJECT` environment variable to your gcp project id. --- -## Frontend Tests -The frontend tests use NodeJS to run. You will need to have node version 14.X to run these tests. To do this, do the following commands: +## Integration Tests +There is a dev-convenience script that auto generates the lines of code for a user journey. See [README](scripts/README.md) for more information and how to run +the script. + +## Frontend Tests +The frontend tests use NodeJS to run. To handle different versions of NodeJS it is recommended to install `Node Version Manager` (`nvm`). It is similar to pyenv but for Node versions. +To install `nvm` use the command below (make sure to replace "v0.39.5" with the current latest version in [releases](https://github.com/nvm-sh/nvm/releases/): ``` shell -brew install nvm -nvm install -nvm use +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash ``` - -Install yarn with: +You will need to have the correct node version installed to run the tests. To do this, use the following commands: ``` shell -npm i -g yarn +nvm install +nvm use ``` Fetch npm dependencies: ``` shell -yarn +npm install ``` Available commands: | Command | Task | -| ---------------------- | --------------------------------------------------------------------------------------------------------- | -| `yarn test_functional` | Runs the functional tests through Webdriver (requires app running on localhost:5000 and generated pages). | -| `yarn generate_pages` | Generates the functional test pages. | -| `yarn lint` | Lints the JS, reporting errors/warnings. | -| `yarn format` | Format the json schemas. | +|------------------------| --------------------------------------------------------------------------------------------------------- | +| `make test-functional` | Runs the functional tests through Webdriver (requires app running on localhost:5000 and generated pages). | +| `make generate-pages` | Generates the functional test pages. | +| `make lint-js` | Lints the JS, reporting errors/warnings. | +| `make format-js` | Format the json schemas. | --- ### Development with functional tests -The tests are written using [WebdriverIO](https://webdriver.io/docs), [Chai](https://www.chaijs.com/), and [Mocha](https://mochajs.org/) +The tests are written using [WebdriverIO](https://webdriver.io/docs/gettingstarted), [Chai](https://www.chaijs.com/), and [Mocha](https://mochajs.org/) ### Functional test options @@ -198,21 +273,18 @@ RUNNER_ENV_FILE=.functional-tests.env make run This will set the correct environment variables for running the functional tests. -Then you can run: +Then you can run either: ``` shell make test-functional ``` - -This will delete the `tests/functional/generated_pages` directory and regenerate all the files in it from the schemas. - -You can also individually run the `generate_pages` and `test_functional` yarn scripts: +or ``` shell -yarn generate_pages -yarn test_functional +make test-functional-headless ``` +This will delete the `tests/functional/generated_pages` directory and regenerate all the files in it from the schemas. To generate the pages manually you can run the `generate_pages` scripts with the schema directory. Run it from the `tests/functional` directory as follows: @@ -220,22 +292,21 @@ To generate the pages manually you can run the `generate_pages` scripts with the ./generate_pages.py ../../schemas/test/en/ ./generated_pages -r "../../base_pages" ``` -To generate a spec file with the imports included, you can use the `generate_pages.py` script on a single schema with the `-s` argument. - +To generate a spec file with the imports included, you can pass the schema name as an argument without the file extension, e.g. `SCHEMA=test_address`: ``` shell -./generate_pages.py ../../schemas/test/en/test_multiple_piping.json ./temp_directory -r "../../base_pages" -s spec/test_multiple_piping.spec.js +make generate-spec SCHEMA= ``` If you have already built the generated pages, then the functional tests can be executed with: ``` shell -yarn test_functional +make test-functional ``` -This can be limited to a single spec using: +This can be limited to a single spec where argument needed is the remainder of the path after `./tests/functional/spec/` (which is included in the command): ``` shell -yarn test_functional --spec save_sign_out.spec.js +make test-functional-spec SPEC= ``` To run a single test, add `.only` into the name of any `describe` or `it` function: @@ -245,16 +316,22 @@ To run a single test, add `.only` into the name of any `describe` or `it` functi `it.only('Given this is a test', function() {...}` Test suites are configured in the `wdio.conf.js` file. -An individual test suite can be run using: +An individual test suite can be run using the suite names as the argument to this command. The suites that can be used with command below are: +* timeout_modal_expired +* timeout_modal_extended +* timeout_modal_extended_new_window +* features +* general +* components ``` shell -yarn test_functional --suite +make test-functional-suite SUITE= ``` To run the tests against a remote deployment you will need to specify the environment variable of EQ_FUNCTIONAL_TEST_ENV eg: ``` shell -EQ_FUNCTIONAL_TEST_ENV=https://staging-new-surveys.dev.eq.ons.digital/ yarn test_functional +EQ_FUNCTIONAL_TEST_ENV=https://staging-new-surveys.dev.eq.ons.digital/ npm run test_functional ``` --- @@ -302,15 +379,14 @@ The following environment variables must be set when deploying the app. The following environment variables are optional: | Variable Name | Default | Description | -| ---------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------- | +|------------------------------| ---------------- |----------------------------------------------------------------------------------------------------------------| | REGION | europe-west2 | The region that will be used for your Cloud Run service | | CONCURRENCY | 80 | The maximum number of requests that can be processed simultaneously by a given container instance | | MIN_INSTANCES | 1 | The minimum number of container instances that can be used for your Cloud Run service | | MAX_INSTANCES | 1 | The maximum number of container instances that can be used for your Cloud Run service | | CPU | 4 | The number of CPUs to allocate for each Cloud Run container instance | | MEMORY | 4G | The amount of memory to allocate for each Cloud Run container instance | -| GOOGLE_TAG_MANAGER_ID | | The Google Tag Manger ID - Specifies the GTM account | -| GOOGLE_TAG_MANAGER_AUTH | | The Google Tag Manger Auth - Ties the GTM container with the whole environment | +| GOOGLE_TAG_ID | | The Google Tag ID - Specifies the GTM account | | WEB_SERVER_TYPE | gunicorn-threads | Web server type used to run the application. This also determines the worker class which can be async/threaded | | WEB_SERVER_WORKERS | 7 | The number of worker processes | | WEB_SERVER_THREADS | 10 | The number of worker threads per worker | @@ -345,45 +421,50 @@ Once we have the translated .po files they can be added to the source code and u The following env variables can be used -| Variable Name | Default | Description | -| ----------------------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------- | -| EQ_SESSION_TIMEOUT_SECONDS | 2700 (45 mins) | The duration of the flask session | -| EQ_PROFILING | False | Enables or disables profiling (True/False) Default False/Disabled | -| EQ_GOOGLE_TAG_MANAGER_ID | | The Google Tag Manger ID - Specifies the GTM account | -| EQ_GOOGLE_TAG_MANAGER_AUTH | | The Google Tag Manger Auth - Ties the GTM container with the whole environment | -| EQ_ENABLE_HTML_MINIFY | True | Enable minification of html | -| EQ_ENABLE_SECURE_SESSION_COOKIE | True | Set secure session cookies | -| EQ_MAX_HTTP_POST_CONTENT_LENGTH | 65536 | The maximum http post content length that the system wil accept | -| EQ_MINIMIZE_ASSETS | True | Should JS and CSS be minimized | -| MAX_CONTENT_LENGTH | 65536 | max request payload size in bytes | -| EQ_APPLICATION_VERSION_PATH | .application-version | the location of a file containing the application version number | -| EQ_ENABLE_LIVE_RELOAD | False | Enable livereload of browser when scripts, styles or templates are updated | -| EQ_SECRETS_FILE | secrets.yml | The location of the secrets file | -| EQ_KEYS_FILE | keys.yml | The location of the keys file | -| EQ_SUBMISSION_BACKEND | | Which submission backend to use ( gcs, rabbitmq, log ) | -| EQ_GCS_SUBMISSION_BUCKET_ID | | The bucket name in GCP to store the submissions in | -| EQ_GCS_FEEDBACK_BUCKET_ID | | The bucket name in GCP to store the feedback in | -| EQ_RABBITMQ_HOST | | | -| EQ_RABBITMQ_HOST_SECONDARY | | | -| EQ_RABBITMQ_PORT | 5672 | | -| EQ_RABBITMQ_QUEUE_NAME | submit_q | The name of the submission queue | -| EQ_SERVER_SIDE_STORAGE_USER_ID_ITERATIONS | 10000 | | -| EQ_STORAGE_BACKEND | datastore | | -| EQ_DYNAMODB_ENDPOINT | | | -| EQ_REDIS_HOST | | Hostname of Redis instance used for ephemeral storage | -| EQ_REDIS_PORT | | Port number of Redis instance used for ephemeral storage | -| EQ_DYNAMODB_MAX_RETRIES | 5 | | -| EQ_DYNAMODB_MAX_POOL_CONNECTIONS | 30 | | -| EQ_QUESTIONNAIRE_STATE_TABLE_NAME | | | -| EQ_SESSION_TABLE_NAME | | | -| EQ_USED_JTI_CLAIM_TABLE_NAME | | | -| WEB_SERVER_TYPE | | Web server type used to run the application. This also determines the worker class which can be async/threaded | -| WEB_SERVER_WORKERS | | The number of worker processes | -| WEB_SERVER_THREADS | | The number of worker threads per worker | -| WEB_SERVER_UWSGI_ASYNC_CORES | | The number of cores to initialise when using "uwsgi-async" web server worker type | -| DATASTORE_USE_GRPC | False | Determines whether to use gRPC for Datastore. gRPC is currently only supported for threaded web servers | +| Variable Name | Default | Description | +|-------------------------------------------|------------------------------|----------------------------------------------------------------------------------------------------------------| +| EQ_SESSION_TIMEOUT_SECONDS | 2700 (45 mins) | The duration of the flask session | +| EQ_PROFILING | False | Enables or disables profiling (True/False) Default False/Disabled | +| EQ_GOOGLE_TAG_ID | | The Google Tag Manger ID - Specifies the GTM account | +| EQ_ENABLE_HTML_MINIFY | True | Enable minification of html | +| EQ_ENABLE_SECURE_SESSION_COOKIE | True | Set secure session cookies | +| EQ_MAX_HTTP_POST_CONTENT_LENGTH | 65536 | The maximum http post content length that the system wil accept | +| EQ_MINIMIZE_ASSETS | True | Should JS and CSS be minimized | +| MAX_CONTENT_LENGTH | 65536 | max request payload size in bytes | +| EQ_APPLICATION_VERSION_PATH | .application-version | the location of a file containing the application version number | +| EQ_ENABLE_LIVE_RELOAD | False | Enable livereload of browser when scripts, styles or templates are updated | +| EQ_SECRETS_FILE | secrets.yml | The location of the secrets file | +| EQ_KEYS_FILE | keys.yml | The location of the keys file | +| EQ_SUBMISSION_BACKEND | | Which submission backend to use ( gcs, rabbitmq, log ) | +| EQ_GCS_SUBMISSION_BUCKET_ID | | The bucket name in GCP to store the submissions in | +| EQ_GCS_FEEDBACK_BUCKET_ID | | The bucket name in GCP to store the feedback in | +| EQ_RABBITMQ_HOST | | | +| EQ_RABBITMQ_HOST_SECONDARY | | | +| EQ_RABBITMQ_PORT | 5672 | | +| EQ_RABBITMQ_QUEUE_NAME | submit_q | The name of the submission queue | +| EQ_SERVER_SIDE_STORAGE_USER_ID_ITERATIONS | 10000 | | +| EQ_STORAGE_BACKEND | datastore | | +| EQ_DYNAMODB_ENDPOINT | | | +| EQ_REDIS_HOST | | Hostname of Redis instance used for ephemeral storage | +| EQ_REDIS_PORT | | Port number of Redis instance used for ephemeral storage | +| EQ_DYNAMODB_MAX_RETRIES | 5 | | +| EQ_DYNAMODB_MAX_POOL_CONNECTIONS | 30 | | +| EQ_QUESTIONNAIRE_STATE_TABLE_NAME | | | +| EQ_SESSION_TABLE_NAME | | | +| EQ_USED_JTI_CLAIM_TABLE_NAME | | | +| WEB_SERVER_TYPE | | Web server type used to run the application. This also determines the worker class which can be async/threaded | +| WEB_SERVER_WORKERS | | The number of worker processes | +| WEB_SERVER_THREADS | | The number of worker threads per worker | +| WEB_SERVER_UWSGI_ASYNC_CORES | | The number of cores to initialise when using "uwsgi-async" web server worker type | +| DATASTORE_USE_GRPC | False | Determines whether to use gRPC for Datastore. gRPC is currently only supported for threaded web servers | | ACCOUNT_SERVICE_BASE_URL | `https://surveys.ons.gov.uk` | The base URL of the account service used to launch the survey | -| ONS_URL | `https://www.ons.gov.uk` | The URL of the ONS website where static content is sourced, e.g. accessibility info | +| ONS_URL | `https://www.ons.gov.uk` | The URL of the ONS website where static content is sourced, e.g. accessibility info | +| SDS_API_BASE_URL | | The base URL of the SDS API used for fetching supplementary data | +| CIR_API_BASE_URL | | The base URL of the CIR API used for fetching collection instruments | +| OIDC_TOKEN_BACKEND | gcp | The backend to use when fetching the Open ID Connect token | +| OIDC_TOKEN_LEEWAY_IN_SECONDS | 300 | The leeway to use when validating OIDC tokens | +| SDS_OAUTH2_CLIENT_ID | | The OAuth2 Client ID used when setting up IAP on the SDS | +| CIR_OAUTH2_CLIENT_ID | | The OAuth2 Client ID used when setting up IAP on the CIR | The following env variables can be used when running tests @@ -436,12 +517,34 @@ Refer to our [profiling document](doc/profiling.md). ## Updating / Installing dependencies ### Python -To add a new dependency, use `pipenv install [package-name]`, which not only installs the package but Pipenv will also go to the trouble of updating the Pipfile as well. +To add a new dependency, use: +``` shell +poetry add [package-name] +``` +This will add the required packages to your pyproject.toml and install them + +To update a dependency, use: +```shell +poetry update [package-name] +``` +This will resolve the required dependencies of the project and write the exact versions into poetry.lock + +Using the poetry up plugin we can update dependencies and bump their versions in the pyproject.toml file + +To update dependencies to the latest compatible version with respect to their version constraints specified in the pyproject.toml file: +```shell +poetry up +``` + +To update dependencies to their latest compatible version: +```shell +poetry up --latest +``` -NB: both the Pipfile and Pipfile.lock files are required in source control to accurately pin dependencies. +NB: both the pyproject.toml and poetry.lock files are required in source control to accurately pin dependencies. ### JavaScript -To add a new dependency, use `yarn add [package-name]` and `yarn` to install all the packages locally. +To add a new dependency, use `npm install [dev dependency] --save-dev` or `npm install [dependency]` then use `npm install` to install all the packages locally. --- @@ -450,8 +553,8 @@ To add a new dependency, use `yarn add [package-name]` and `yarn` to install all ### On [Design System](https://github.com/ONSdigital/design-system) Repo Checkout branch with new changes on -You will need to install the Design System dependencies to do this so run `yarn` in the terminal if you haven't -You will also need to install gulp +You will need to install the Design System dependencies. If you haven't installed Yarn, install it with `npm i -g yarn`. To install the dependencies run `yarn` in the terminal. If you haven't +you will also need to install gulp. Then in the terminal run: @@ -487,7 +590,7 @@ Run `make load-design-system-templates` in the terminal to make sure you have th Then edit the first line in the `templates/layout/_template.njk` file to remove the version number. Should now look like this: ``` shell -{% set release_version = "" %} +{% set release_version = '' %} ``` Then spin up launcher and runner with `make dev-compose-up` and `make run` diff --git a/app/authentication/auth_payload_versions.py b/app/authentication/auth_payload_versions.py new file mode 100644 index 0000000000..fe4d338d37 --- /dev/null +++ b/app/authentication/auth_payload_versions.py @@ -0,0 +1,5 @@ +from enum import Enum + + +class AuthPayloadVersion(Enum): + V2 = "v2" diff --git a/app/authentication/authenticator.py b/app/authentication/authenticator.py index e265d29ec5..ffcc5f3ec6 100644 --- a/app/authentication/authenticator.py +++ b/app/authentication/authenticator.py @@ -1,5 +1,6 @@ +from contextlib import contextmanager from datetime import datetime, timedelta, timezone -from typing import Any, Mapping, Optional, Union +from typing import Any, Generator, Mapping, MutableMapping from uuid import uuid4 from blinker import ANY @@ -7,13 +8,19 @@ from flask import session as cookie_session from flask_login import LoginManager, user_logged_out from sdc.crypto.decrypter import decrypt -from structlog import get_logger +from structlog import contextvars, get_logger from app.authentication.no_token_exception import NoTokenException from app.authentication.user import User +from app.data_models import QuestionnaireStore from app.data_models.session_data import SessionData from app.data_models.session_store import SessionStore -from app.globals import create_session_store, get_questionnaire_store, get_session_store +from app.globals import ( + create_session_store, + get_metadata, + get_questionnaire_store, + get_session_store, +) from app.keys import KEY_PURPOSE_AUTHENTICATION from app.settings import EQ_SESSION_ID, USER_IK @@ -23,15 +30,15 @@ @login_manager.user_loader -def user_loader(user_id: str) -> Optional[str]: +def user_loader(user_id: str) -> str | None: logger.debug("loading user", user_id=user_id) return load_user() @login_manager.request_loader def request_load_user( - request: Request, # pylint: disable=unused-argument -) -> Optional[User]: + request: Request, +) -> User | None: logger.debug("load user") extend_session = not ( @@ -87,7 +94,7 @@ def _is_session_valid(session_store: SessionStore) -> bool: ) -def load_user(extend_session: bool = True) -> Optional[User]: +def load_user(extend_session: bool = True) -> User | None: """ Checks for the present of the JWT in the users sessions :return: A user object if a JWT token is available in the session @@ -103,15 +110,25 @@ def load_user(extend_session: bool = True) -> Optional[User]: user_ik = cookie_session.get(USER_IK) user = User(user_id, user_ik) - if session_store.session_data and session_store.session_data.tx_id: - logger.bind(tx_id=session_store.session_data.tx_id) + if metadata := get_metadata(user): + contextvars.bind_contextvars(tx_id=metadata.tx_id) if extend_session: _extend_session_expiry(session_store) return user - logger.info("session does not exist") + logger.info( + "session does not exist", + user_ik_present=USER_IK in cookie_session, + eq_session_id_present=EQ_SESSION_ID in cookie_session, + session_store_exists=bool(session_store), + session_expiration=( + session_store.expiration_time.isoformat() + if session_store and session_store.expiration_time + else None + ), + ) cookie_session.pop(USER_IK, None) @@ -123,26 +140,16 @@ def _create_session_data_from_metadata(metadata: Mapping[str, Any]) -> SessionDa """ return SessionData( - tx_id=metadata.get("tx_id"), - schema_name=metadata.get("schema_name"), - period_str=metadata.get("period_str"), language_code=metadata.get("language_code"), - launch_language_code=metadata.get("language_code"), - survey_url=metadata.get("survey_url"), - ru_name=metadata.get("ru_name"), - ru_ref=metadata.get("ru_ref"), - response_id=metadata.get("response_id"), - case_id=metadata["case_id"], - case_ref=metadata.get("case_ref"), - trad_as=metadata.get("trad_as"), - account_service_base_url=metadata.get("account_service_url"), - account_service_log_out_url=metadata.get("account_service_log_out_url"), ) -def store_session(metadata: dict[str, Any]) -> None: +@contextmanager +def create_session_questionnaire_store( + metadata: MutableMapping, +) -> Generator[QuestionnaireStore, None, None]: """ - Store new session and metadata + Context to manage creating and saving new session and questionnaire store :param metadata: metadata parsed from jwt token """ # also clear the secure cookie data @@ -163,18 +170,19 @@ def store_session(metadata: dict[str, Any]) -> None: create_session_store(eq_session_id, user_id, user_ik, session_data) questionnaire_store = get_questionnaire_store(user_id, user_ik) + yield questionnaire_store questionnaire_store.set_metadata(metadata) questionnaire_store.save() logger.info("user authenticated") -def decrypt_token(encrypted_token: str) -> dict[str, Union[str, list, int]]: +def decrypt_token(encrypted_token: str | None) -> dict[str, Any]: if not encrypted_token: - raise NoTokenException("Please provide a token") + raise NoTokenException() logger.debug("decrypting token") - decrypted_token: dict[str, Union[str, list, int]] = decrypt( + decrypted_token: dict[str, Any] = decrypt( token=encrypted_token, key_store=current_app.eq["key_store"], # type: ignore key_purpose=KEY_PURPOSE_AUTHENTICATION, diff --git a/app/authentication/no_questionnaire_state_exception.py b/app/authentication/no_questionnaire_state_exception.py index 2affbdb90e..aadba31c59 100644 --- a/app/authentication/no_questionnaire_state_exception.py +++ b/app/authentication/no_questionnaire_state_exception.py @@ -1,8 +1,5 @@ -from typing import Union - - class NoQuestionnaireStateException(Exception): - def __init__(self, value: Union[str, int]) -> None: + def __init__(self, value: str | int) -> None: super().__init__() self.value = value diff --git a/app/authentication/no_token_exception.py b/app/authentication/no_token_exception.py index 0790d3a9df..8671397ee9 100644 --- a/app/authentication/no_token_exception.py +++ b/app/authentication/no_token_exception.py @@ -1,8 +1,5 @@ -from typing import Union - - class NoTokenException(Exception): - def __init__(self, value: Union[str, int]) -> None: + def __init__(self, value: str = "Please provide a token") -> None: super().__init__() self.value = value diff --git a/app/authentication/roles.py b/app/authentication/roles.py index 2e5a97bba4..ee1fa2fe6e 100644 --- a/app/authentication/roles.py +++ b/app/authentication/roles.py @@ -29,8 +29,10 @@ def dump(): def role_required_decorator(func: Callable) -> Callable: @wraps(func) def role_required_wrapper(*args: tuple, **kwargs: dict) -> Any: - metadata = get_metadata(current_user) - roles: list = (metadata.get("roles", []) or []) if metadata else [] + roles: list = [] + if metadata := get_metadata(current_user): + roles = metadata.roles or [] + if current_user.is_authenticated and role in roles: return func(*args, **kwargs) raise Forbidden diff --git a/app/authentication/user.py b/app/authentication/user.py index 59b5309728..9157fb4a73 100644 --- a/app/authentication/user.py +++ b/app/authentication/user.py @@ -1,12 +1,12 @@ -from typing import Optional - from flask_login import UserMixin class User(UserMixin): - def __init__(self, user_id: Optional[str], user_ik: Optional[str]) -> None: + USER_SESSION_ERROR_MESSAGE = "No user_id or user_ik found in session" + + def __init__(self, user_id: str | None, user_ik: str | None) -> None: if user_id and user_ik: self.user_id = user_id self.user_ik = user_ik else: - raise ValueError("No user_id or user_ik found in session") + raise ValueError(self.USER_SESSION_ERROR_MESSAGE) diff --git a/app/authentication/user_id_generator.py b/app/authentication/user_id_generator.py index 01d0fa8b00..a5d454a9b2 100644 --- a/app/authentication/user_id_generator.py +++ b/app/authentication/user_id_generator.py @@ -11,11 +11,14 @@ class UserIDGenerator: + USER_ID_SALT_ERROR_MESSAGE = "user_id_salt is required" + USER_IK_SALT_ERROR_MESSAGE = "user_ik_salt is required" + def __init__(self, iterations: int, user_id_salt: str, user_ik_salt: str) -> None: if user_id_salt is None: - raise ValueError("user_id_salt is required") + raise ValueError(self.USER_ID_SALT_ERROR_MESSAGE) if user_ik_salt is None: - raise ValueError("user_ik_salt is required") + raise ValueError(self.USER_IK_SALT_ERROR_MESSAGE) self._iterations = iterations self._user_id_salt = user_id_salt diff --git a/app/cloud_tasks/__init__.py b/app/cloud_tasks/__init__.py index 325f39ba16..5f92f8e9b8 100644 --- a/app/cloud_tasks/__init__.py +++ b/app/cloud_tasks/__init__.py @@ -1,3 +1,6 @@ -from .cloud_task_publishers import CloudTaskPublisher, LogCloudTaskPublisher +from app.cloud_tasks.cloud_task_publishers import ( + CloudTaskPublisher, + LogCloudTaskPublisher, +) __all__ = ["CloudTaskPublisher", "LogCloudTaskPublisher"] diff --git a/app/cloud_tasks/cloud_task_publishers.py b/app/cloud_tasks/cloud_task_publishers.py index 7778cdedeb..a8c02b1958 100644 --- a/app/cloud_tasks/cloud_task_publishers.py +++ b/app/cloud_tasks/cloud_task_publishers.py @@ -4,7 +4,7 @@ from google.cloud.tasks_v2.types.task import Task from structlog import get_logger -from .exceptions import CloudTaskCreationFailed +from app.cloud_tasks.exceptions import CloudTaskCreationFailed logger = get_logger(__name__) @@ -53,7 +53,6 @@ def create_task( function_name: str, fulfilment_request_transaction_id: str, ) -> Task: - parent = self._client.queue_path(self._project_id, "europe-west2", queue_name) try: task = self._create_task_with_retry(body, function_name, parent) diff --git a/app/data_models/__init__.py b/app/data_models/__init__.py index 331a6fdd4d..cbe2c9a282 100644 --- a/app/data_models/__init__.py +++ b/app/data_models/__init__.py @@ -1,14 +1,15 @@ -from .answer import Answer, AnswerValueTypes -from .fulfilment_request import FulfilmentRequest -from .progress_store import CompletionStatus -from .questionnaire_store import ( +from app.data_models.answer import Answer, AnswerValueTypes +from app.data_models.fulfilment_request import FulfilmentRequest +from app.data_models.progress import CompletionStatus +from app.data_models.questionnaire_store import ( AnswerStore, ListStore, ProgressStore, QuestionnaireStore, + SupplementaryDataStore, ) -from .session_data import SessionData -from .session_store import SessionStore +from app.data_models.session_data import SessionData +from app.data_models.session_store import SessionStore __all__ = [ "Answer", @@ -21,4 +22,5 @@ "QuestionnaireStore", "SessionData", "SessionStore", + "SupplementaryDataStore", ] diff --git a/app/data_models/answer.py b/app/data_models/answer.py index fa75ef50c2..d199021276 100644 --- a/app/data_models/answer.py +++ b/app/data_models/answer.py @@ -2,23 +2,26 @@ from dataclasses import asdict, dataclass, field from decimal import Decimal -from typing import Optional, TypedDict, Union, overload +from typing import TypedDict, overload from markupsafe import Markup, escape -DictAnswer = dict[str, Union[int, str]] +DictAnswer = dict[str, int | str] ListAnswer = list[str] -DictAnswerEscaped = dict[str, Union[int, Markup]] +ListDictAnswer = list[DictAnswer] +DictAnswerEscaped = dict[str, int | Markup] ListAnswerEscaped = list[Markup] +ListDictAnswerEscaped = list[DictAnswerEscaped] -AnswerValueTypes = Union[str, int, Decimal, DictAnswer, ListAnswer] -AnswerValueEscapedTypes = Union[ - Markup, - int, - Decimal, - DictAnswerEscaped, - ListAnswerEscaped, -] +AnswerValueTypes = str | int | Decimal | DictAnswer | ListAnswer | ListDictAnswer +AnswerValueEscapedTypes = ( + Markup + | int + | Decimal + | DictAnswerEscaped + | ListAnswerEscaped + | ListDictAnswerEscaped +) class AnswerDict(TypedDict, total=False): @@ -31,7 +34,7 @@ class AnswerDict(TypedDict, total=False): class Answer: answer_id: str value: AnswerValueTypes - list_item_id: Optional[str] = field(default=None) + list_item_id: str | None = field(default=None) @classmethod def from_dict(cls, answer_dict: AnswerDict) -> Answer: @@ -52,28 +55,32 @@ def to_dict(self) -> dict: @overload -def escape_answer_value(value: ListAnswer) -> ListAnswerEscaped: - ... # pragma: no cover +def escape_answer_value(value: ListAnswer) -> ListAnswerEscaped: ... # pragma: no cover @overload -def escape_answer_value(value: DictAnswer) -> DictAnswerEscaped: - ... # pragma: no cover +def escape_answer_value(value: DictAnswer) -> DictAnswerEscaped: ... # pragma: no cover @overload -def escape_answer_value(value: str) -> Markup: - ... # pragma: no cover +def escape_answer_value( + value: ListDictAnswer, +) -> ListDictAnswerEscaped: ... # pragma: no cover @overload -def escape_answer_value(value: Union[None, int, Decimal]) -> Union[None, int, Decimal]: - ... # pragma: no cover +def escape_answer_value(value: str) -> Markup: ... # pragma: no cover + + +@overload +def escape_answer_value( + value: None | int | Decimal, +) -> None | int | Decimal: ... # pragma: no cover def escape_answer_value( - value: Optional[AnswerValueTypes], -) -> Optional[AnswerValueEscapedTypes]: + value: AnswerValueTypes | None, +) -> AnswerValueEscapedTypes | None: if isinstance(value, list): return [escape(item) for item in value] diff --git a/app/data_models/answer_store.py b/app/data_models/answer_store.py index 06aa813c9d..10b83c41c1 100644 --- a/app/data_models/answer_store.py +++ b/app/data_models/answer_store.py @@ -1,10 +1,10 @@ from __future__ import annotations -from typing import Iterable, Iterator, Optional +from typing import Iterable, Iterator from app.data_models.answer import Answer, AnswerDict -AnswerKeyType = tuple[str, Optional[str]] +AnswerKeyType = tuple[str, str | None] class AnswerStore: @@ -21,13 +21,13 @@ class AnswerStore: } """ - def __init__(self, existing_answers: Optional[Iterable[AnswerDict]] = None): + def __init__(self, answers: Iterable[AnswerDict] | None = None): """Instantiate an answer_store. Args: - existing_answers: If a list of answer dictionaries is provided, this will be used to initialise the store. + answers: If a list of answer dictionaries is provided, this will be used to initialise the store. """ - self.answer_map = self._build_map(existing_answers or []) + self.answer_map = self._build_map(answers or []) self._is_dirty = False def __iter__(self) -> Iterator[Answer]: @@ -53,10 +53,11 @@ def _build_map( @staticmethod def _validate(answer: Answer) -> None: + answer_type_error_message = ( + f"Method only supports Answer argument type, found type: {type(answer)}" + ) if not isinstance(answer, Answer): - raise TypeError( - f"Method only supports Answer argument type, found type: {type(answer)}" - ) + raise TypeError(answer_type_error_message) @property def is_dirty(self) -> bool: @@ -81,8 +82,8 @@ def add_or_update(self, answer: Answer) -> bool: return False def get_answer( - self, answer_id: str, list_item_id: Optional[str] = None - ) -> Optional[Answer]: + self, answer_id: str, list_item_id: str | None = None + ) -> Answer | None: """Get a single answer from the store Args: @@ -95,7 +96,7 @@ def get_answer( return self.answer_map.get((answer_id, list_item_id)) def get_answers_by_answer_id( - self, answer_ids: Iterable[str], list_item_id: Optional[str] = None + self, answer_ids: Iterable[str], list_item_id: str | None = None ) -> list[Answer]: """Get multiple answers from the store using the answer_id @@ -121,9 +122,7 @@ def clear(self) -> None: """ self.answer_map.clear() - def remove_answer( - self, answer_id: str, *, list_item_id: Optional[str] = None - ) -> bool: + def remove_answer(self, answer_id: str, *, list_item_id: str | None = None) -> bool: """ Removes answer *in place* from the answer store. :return: True if answer removed else False @@ -137,16 +136,17 @@ def remove_answer( return False - def remove_all_answers_for_list_item_id(self, list_item_id: str) -> None: - """Remove all answers associated with a particular list_item_id. + def remove_all_answers_for_list_item_ids(self, *list_item_ids: str) -> None: + """Remove all answers associated with any of the list_item_ids passed in. This method iterates through the entire list of answers. *Not efficient.* """ - keys_to_delete = [] - for answer in self: - if answer.list_item_id == list_item_id: - keys_to_delete.append((answer.answer_id, answer.list_item_id)) + keys_to_delete = [ + (answer.answer_id, answer.list_item_id) + for answer in self + if answer.list_item_id and answer.list_item_id in list_item_ids + ] for key in keys_to_delete: del self.answer_map[key] diff --git a/app/data_models/app_models.py b/app/data_models/app_models.py index 0165488ba2..d9f0b5d7f6 100644 --- a/app/data_models/app_models.py +++ b/app/data_models/app_models.py @@ -1,5 +1,5 @@ from datetime import datetime, timezone -from typing import Any, Optional, Union +from typing import Any from marshmallow import Schema, fields, post_load, pre_dump @@ -11,8 +11,8 @@ def __init__( state_data: str, collection_exercise_sid: str, version: int, - submitted_at: Optional[datetime] = None, - expires_at: Optional[datetime] = None, + submitted_at: datetime | None = None, + expires_at: datetime | None = None, ): self.user_id = user_id self.state_data = state_data @@ -28,9 +28,9 @@ class EQSession: def __init__( self, eq_session_id: str, - user_id: Optional[str], + user_id: str | None, expires_at: datetime, - session_data: Optional[str], + session_data: str | None, ): self.eq_session_id = eq_session_id self.user_id = user_id @@ -52,9 +52,9 @@ class Timestamp(fields.Field): def _serialize( self, value: datetime, - *args: Optional[list], + *args: list | None, **kwargs: Any, - ) -> int: + ) -> int | None: if value: # Timezone aware datetime to timestamp return int(value.replace(tzinfo=timezone.utc).timestamp()) @@ -63,9 +63,9 @@ def _serialize( def _deserialize( self, value: float, - *args: Optional[list], + *args: list | None, **kwargs: Any, - ) -> datetime: + ) -> datetime | None: if value: # Timestamp to timezone aware datetime return datetime.fromtimestamp(value, tz=timezone.utc) @@ -79,9 +79,9 @@ class DateTimeSchemaMixin: @staticmethod @pre_dump def set_date( - data: Union[EQSession, QuestionnaireState], + data: EQSession | QuestionnaireState, **kwargs: Any, - ) -> Union[EQSession, QuestionnaireState]: + ) -> EQSession | QuestionnaireState: data.updated_at = datetime.now(tz=timezone.utc) return data diff --git a/app/data_models/data_stores.py b/app/data_models/data_stores.py new file mode 100644 index 0000000000..7c1f5e8dbf --- /dev/null +++ b/app/data_models/data_stores.py @@ -0,0 +1,20 @@ +from dataclasses import dataclass, field +from typing import MutableMapping + +from app.data_models.answer_store import AnswerStore +from app.data_models.list_store import ListStore +from app.data_models.metadata_proxy import MetadataProxy +from app.data_models.progress_store import ProgressStore +from app.data_models.supplementary_data_store import SupplementaryDataStore + + +@dataclass +class DataStores: + metadata: MetadataProxy | None = None + response_metadata: MutableMapping = field(default_factory=dict) + list_store: ListStore = field(default_factory=ListStore) + answer_store: AnswerStore = field(default_factory=AnswerStore) + progress_store: ProgressStore = field(default_factory=ProgressStore) + supplementary_data_store: SupplementaryDataStore = field( + default_factory=SupplementaryDataStore + ) diff --git a/app/data_models/list_store.py b/app/data_models/list_store.py index 869e95e67e..54db73ff8c 100644 --- a/app/data_models/list_store.py +++ b/app/data_models/list_store.py @@ -3,7 +3,7 @@ import random from functools import cached_property from string import ascii_letters -from typing import Iterable, Iterator, Optional, TypedDict +from typing import Iterable, Iterator, TypedDict, overload from structlog import get_logger @@ -27,9 +27,9 @@ class ListModel: def __init__( self, name: str, - items: Optional[list[str]] = None, - primary_person: Optional[str] = None, - same_name_items: Optional[list[str]] = None, + items: list[str] | None = None, + primary_person: str | None = None, + same_name_items: list[str] | None = None, ): self.name = name self.items = items or [] @@ -44,7 +44,13 @@ def __eq__(self, other: object) -> bool: def __iter__(self) -> Iterator[str]: yield from self.items - def __getitem__(self, list_item_index: int) -> str: + @overload + def __getitem__(self, list_item_index: int) -> str: ... # pragma: no cover + + @overload + def __getitem__(self, list_item_index: slice) -> list[str]: ... # pragma: no cover + + def __getitem__(self, list_item_index: slice | int) -> str | list[str]: return self.items[list_item_index] def __len__(self) -> int: @@ -76,9 +82,10 @@ def first(self) -> str: try: return self.items[0] except IndexError as e: - raise IndexError( + empty_list_error_message = ( f"unable to access first item in list, list '{self.name}' is empty" - ) from e + ) + raise IndexError(empty_list_error_message) from e @property def count(self) -> int: @@ -121,10 +128,10 @@ class ListStore: ``` """ - def __init__(self, existing_items: Optional[Iterable[ListModelDictType]] = None): - existing_items = existing_items or [] + def __init__(self, items: Iterable[ListModelDictType] | None = None): + items = items or [] - self._lists = self._build_map(existing_items) + self._lists = self._build_map(items) self._is_dirty = False @@ -151,7 +158,7 @@ def _build_map(list_models: Iterable[ListModelDictType]) -> dict[str, ListModel] } def get(self, item: str) -> ListModel: - return self.__getitem__(item) + return self[item] def list_item_position(self, for_list: str, list_item_id: str) -> int: return self[for_list].index(list_item_id) + 1 @@ -170,6 +177,11 @@ def _list_item_ids(self) -> list[str]: return ids + def get_list_name_for_list_item_id(self, list_item_id: str) -> str | None: + for list_name in self._lists: + if list_item_id in self[list_name].items: + return list_name + @property def is_dirty(self) -> bool: return self._is_dirty @@ -215,6 +227,10 @@ def add_list_item(self, list_name: str, primary_person: bool = False) -> str: return list_item_id + def delete_list(self, list_name: str) -> None: + """Deletes the entire list""" + del self._lists[list_name] + def serialize(self) -> list[ListModelDictType]: return [list_model.serialize() for list_model in self] @@ -223,4 +239,4 @@ def deserialize(cls, serialized: Iterable[ListModelDictType]) -> ListStore: if not serialized: return cls() - return cls(existing_items=serialized) + return cls(items=serialized) diff --git a/app/data_models/metadata_proxy.py b/app/data_models/metadata_proxy.py new file mode 100644 index 0000000000..9efba54bd0 --- /dev/null +++ b/app/data_models/metadata_proxy.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +from copy import deepcopy +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Mapping + +from werkzeug.datastructures import ImmutableDict + +from app.authentication.auth_payload_versions import AuthPayloadVersion +from app.utilities.make_immutable import make_immutable + + +class NoMetadataException(Exception): + pass + + +# "version" is excluded here as it is handled independently +TOP_LEVEL_METADATA_KEYS = [ + "tx_id", + "account_service_url", + "case_id", + "collection_exercise_sid", + "response_id", + "response_expires_at", + "language_code", + "schema_name", + "schema_url", + "cir_instrument_id", + "channel", + "region_code", + "roles", +] + + +@dataclass(frozen=True) +class SurveyMetadata: + data: ImmutableDict + receipting_keys: tuple | None = None + + def __getitem__(self, key: str) -> Any: + return self.data.get(key) + + +@dataclass(frozen=True) +class MetadataProxy: + tx_id: str + account_service_url: str + case_id: str + collection_exercise_sid: str + response_id: str + response_expires_at: datetime + survey_metadata: SurveyMetadata | None = None + schema_url: str | None = None + schema_name: str | None = None + cir_instrument_id: str | None = None + language_code: str | None = None + channel: str | None = None + region_code: str | None = None + version: AuthPayloadVersion | None = None + roles: list | None = None + + def __getitem__(self, key: str) -> Any | None: + if self.survey_metadata and key in self.survey_metadata.data: + return self.survey_metadata[key] + + return getattr(self, key, None) + + @classmethod + def from_dict(cls, metadata: Mapping) -> MetadataProxy: + _metadata = deepcopy(dict(metadata)) + version = ( + AuthPayloadVersion(_metadata.pop("version")) + if "version" in _metadata + else None + ) + + survey_metadata = None + if serialized_metadata := cls.serialize(_metadata.pop("survey_metadata", {})): + survey_metadata = SurveyMetadata(**serialized_metadata) + + top_level_data = { + key: _metadata.pop(key, None) for key in TOP_LEVEL_METADATA_KEYS + } + + return cls( + **top_level_data, + version=version, + survey_metadata=survey_metadata, + ) + + @classmethod + def serialize(cls, data: Any) -> Any: + return make_immutable(data) diff --git a/app/data_models/progress.py b/app/data_models/progress.py index e42229eff8..99d8fdbf84 100644 --- a/app/data_models/progress.py +++ b/app/data_models/progress.py @@ -1,29 +1,37 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Mapping, Optional, TypedDict +from enum import StrEnum +from typing import Mapping, TypedDict -class ProgressDictType(TypedDict, total=False): +class CompletionStatus(StrEnum): + COMPLETED = "COMPLETED" + IN_PROGRESS = "IN_PROGRESS" + NOT_STARTED = "NOT_STARTED" + INDIVIDUAL_RESPONSE_REQUESTED = "INDIVIDUAL_RESPONSE_REQUESTED" + + +class ProgressDict(TypedDict, total=False): section_id: str block_ids: list[str] - status: str - list_item_id: str + status: CompletionStatus + list_item_id: str | None @dataclass class Progress: section_id: str block_ids: list[str] - status: str - list_item_id: Optional[str] = None + status: CompletionStatus + list_item_id: str | None = None @classmethod - def from_dict(cls, progress_dict: ProgressDictType) -> Progress: + def from_dict(cls, progress_dict: ProgressDict) -> Progress: return cls( section_id=progress_dict["section_id"], block_ids=progress_dict["block_ids"], - status=progress_dict["status"], + status=CompletionStatus(progress_dict["status"]), list_item_id=progress_dict.get("list_item_id"), ) diff --git a/app/data_models/progress_store.py b/app/data_models/progress_store.py index 29eef569a6..7985108b1f 100644 --- a/app/data_models/progress_store.py +++ b/app/data_models/progress_store.py @@ -1,53 +1,52 @@ -from dataclasses import astuple, dataclass -from typing import Iterable, Iterator, MutableMapping, Optional +from typing import Iterable, MutableMapping -from app.data_models.progress import Progress, ProgressDictType -from app.questionnaire.location import Location - -SectionKeyType = tuple[str, Optional[str]] - - -@dataclass -class CompletionStatus: - COMPLETED: str = "COMPLETED" - IN_PROGRESS: str = "IN_PROGRESS" - NOT_STARTED: str = "NOT_STARTED" - INDIVIDUAL_RESPONSE_REQUESTED: str = "INDIVIDUAL_RESPONSE_REQUESTED" - - def __iter__(self) -> Iterator[tuple[str]]: - return iter(astuple(self)) +from app.data_models.progress import CompletionStatus, Progress, ProgressDict +from app.questionnaire.location import Location, SectionKey +from app.utilities.types import LocationType class ProgressStore: """ - An object that stores and updates references to sections and blocks + An object that stores and updates references to sections and list items that have been started. """ def __init__( - self, in_progress_sections: Optional[Iterable[ProgressDictType]] = None + self, + progress: Iterable[ProgressDict] | None = None, ) -> None: """ - Instantiate a ProgressStore object that tracks the status of sections and its completed blocks + Instantiate a ProgressStore object that tracks the progress status of Sections & Repeating Sections, + and their completed blocks, as well as Repeating Blocks for List Items. + - Standard Sections are keyed by Section ID, and a None List Item ID + - Repeating Sections (dynamic Sections created for List Items that have been added using a List Collector) + are keyed by their Section ID, and the List Item ID of the item it is the section for. + - Repeating Blocks for List Items are keyed by the Section ID for the Section in which their List Collector + appears, and the List Item ID. Repeating Blocks progress is only tracked if the List Collector + that created the List Item has Repeating Blocks, and progress of the Repeating Blocks for a List Item + indicates if all required Repeating Blocks from the List Collector have been completed for the List Item. Args: - in_progress_sections: A list of hierarchical dict containing the section status and completed blocks + progress: A list of hierarchical dict containing the completion status + and completed blocks of Sections, Repeating Sections and List Items """ self._is_dirty: bool = False self._is_routing_backwards: bool = False - self._progress: MutableMapping[SectionKeyType, Progress] = self._build_map( - in_progress_sections or [] + self._progress: MutableMapping[SectionKey, Progress] = self._build_map( + progress or [] ) - def __contains__(self, section_key: SectionKeyType) -> bool: + def __contains__(self, section_key: SectionKey) -> bool: return section_key in self._progress @staticmethod - def _build_map(section_progress_list: Iterable[ProgressDictType]) -> MutableMapping: + def _build_map( + section_list: Iterable[ProgressDict], + ) -> MutableMapping: """ - Builds the progress_store's data structure from a list of progress dictionaries. + Builds the ProgressStore's data structure from a list of section dictionaries. The `section_key` is tuple consisting of `section_id` and the `list_item_id`. - The `section_progress` is a mutableMapping created from the Progress object. + The `section` is a mutableMapping created from the Progress object. Example structure: { @@ -61,11 +60,10 @@ def _build_map(section_progress_list: Iterable[ProgressDictType]) -> MutableMapp """ return { - ( - section_progress["section_id"], - section_progress.get("list_item_id"), - ): Progress.from_dict(section_progress) - for section_progress in section_progress_list + SectionKey( + section["section_id"], section.get("list_item_id") + ): Progress.from_dict(section) + for section in section_list } @property @@ -76,10 +74,12 @@ def is_dirty(self) -> bool: def is_routing_backwards(self) -> bool: return self._is_routing_backwards - def is_section_complete( - self, section_id: str, list_item_id: Optional[str] = None - ) -> bool: - return (section_id, list_item_id) in self.section_keys( + def is_section_complete(self, section_key: SectionKey) -> bool: + """ + Return True if the CompletionStatus of the Section or List Item specified by the given section_id and + list_item_id is COMPLETED or INDIVIDUAL_RESPONSE_REQUESTED, else False. + """ + return section_key in self.section_keys( statuses={ CompletionStatus.COMPLETED, CompletionStatus.INDIVIDUAL_RESPONSE_REQUESTED, @@ -88,11 +88,14 @@ def is_section_complete( def section_keys( self, - statuses: Optional[Iterable[str]] = None, - section_ids: Optional[Iterable[str]] = None, - ) -> list[SectionKeyType]: + statuses: Iterable[CompletionStatus] | None = None, + section_ids: Iterable[str] | None = None, + ) -> list[SectionKey]: + """ + Return the Keys of the Section and Repeating Blocks progresses stored in this ProgressStore. + """ if not statuses: - statuses = {*CompletionStatus()} + statuses = CompletionStatus section_keys = [ section_key @@ -104,82 +107,103 @@ def section_keys( return section_keys return [ - section_key - for section_key in section_keys - if any(section_id in section_key for section_id in section_ids) + progress_key + for progress_key in section_keys + if any(section_id in progress_key for section_id in section_ids) ] def update_section_status( - self, section_status: str, section_id: str, list_item_id: Optional[str] = None - ) -> None: - section_key = (section_id, list_item_id) + self, status: CompletionStatus, section_key: SectionKey + ) -> bool: + """ + Updates the status of the Section or Repeating Blocks for a list item specified by the key based on the given section id and list item id. + """ + updated = False if section_key in self._progress: - self._progress[section_key].status = section_status - self._is_dirty = True + if self._progress[section_key].status != status: + updated = True + self._progress[section_key].status = status + self._is_dirty = True - elif ( - section_status == CompletionStatus.INDIVIDUAL_RESPONSE_REQUESTED - and section_key not in self._progress - ): + elif status == CompletionStatus.INDIVIDUAL_RESPONSE_REQUESTED: self._progress[section_key] = Progress( - section_id=section_id, - list_item_id=list_item_id, - block_ids=[], - status=section_status, + block_ids=[], status=status, **section_key.to_dict() ) self._is_dirty = True - def get_section_status( - self, section_id: str, list_item_id: Optional[str] = None - ) -> str: - section_key = (section_id, list_item_id) + return updated + + def get_section_status(self, section_key: SectionKey) -> CompletionStatus: + """ + Return the CompletionStatus of the Section or Repeating Blocks for a list item, + specified by the given section_id and list_item_id in SectionKey. + Returns NOT_STARTED if the progress does not exist + """ if section_key in self._progress: return self._progress[section_key].status return CompletionStatus.NOT_STARTED - def get_completed_block_ids( - self, section_id: str, list_item_id: Optional[str] = None - ) -> list[str]: - section_key = (section_id, list_item_id) + def get_block_status( + self, *, block_id: str, section_key: SectionKey + ) -> CompletionStatus: + """ + Return the completion status of the block specified by the given block_id, + if it is part of the progress of the given Section or Repeating Blocks for list item + specified by the given section_id or list_item_id + """ + blocks = self.get_completed_block_ids(section_key) + if block_id in blocks: + return CompletionStatus.COMPLETED + + return CompletionStatus.NOT_STARTED + + def get_completed_block_ids(self, section_key: SectionKey) -> list[str]: + """ + Return the block ids recorded as part of the progress for the Section or Repeating Blocks + for list item specified by the given section_id and list_item_id in SectionKey + """ if section_key in self._progress: return self._progress[section_key].block_ids return [] - def add_completed_location(self, location: Location) -> None: - section_id = location.section_id - list_item_id = location.list_item_id - - completed_block_ids = self.get_completed_block_ids(section_id, list_item_id) + def add_completed_location(self, location: LocationType) -> None: + """ + Adds the block from the given Location, to the progress specified by the + section id and list item id within the Location. + """ + completed_block_ids = self.get_completed_block_ids(location.section_key) if location.block_id not in completed_block_ids: completed_block_ids.append(location.block_id) # type: ignore - - section_key = (section_id, list_item_id) - - if section_key in self._progress: - self._progress[section_key].block_ids = completed_block_ids + progress_key = location.section_key + if progress_key in self._progress: + self._progress[progress_key].block_ids = completed_block_ids else: - self._progress[section_key] = Progress( - section_id=section_id, - list_item_id=list_item_id, + self._progress[progress_key] = Progress( + section_id=location.section_id, + list_item_id=location.list_item_id, block_ids=completed_block_ids, status=CompletionStatus.IN_PROGRESS, ) self._is_dirty = True - def remove_completed_location(self, location: Location) -> bool: - section_key = (location.section_id, location.list_item_id) + def remove_completed_location(self, location: LocationType) -> bool: + """ + Removes the block in the given Location, from the progress specified by the + section id and list item id within the Location if it exists in the store. + """ + progress_key = location.section_key if ( - section_key in self._progress - and location.block_id in self._progress[section_key].block_ids + progress_key in self._progress + and location.block_id in self._progress[progress_key].block_ids ): - self._progress[section_key].block_ids.remove(location.block_id) + self._progress[progress_key].block_ids.remove(location.block_id) - if not self._progress[section_key].block_ids: - self._progress[section_key].status = CompletionStatus.IN_PROGRESS + if not self._progress[progress_key].block_ids: + self._progress[progress_key].status = CompletionStatus.IN_PROGRESS self._is_dirty = True return True @@ -192,15 +216,14 @@ def remove_progress_for_list_item_id(self, list_item_id: str) -> None: *Not efficient.* """ - - section_keys_to_delete = [ - (section_id, progress_list_item_id) + progress_keys_to_delete = [ + SectionKey(section_id, progress_list_item_id) for section_id, progress_list_item_id in self._progress if progress_list_item_id == list_item_id ] - for section_key in section_keys_to_delete: - del self._progress[section_key] + for progress_key in progress_keys_to_delete: + del self._progress[progress_key] self._is_dirty = True @@ -216,9 +239,12 @@ def clear(self) -> None: self._is_dirty = True def started_section_keys( - self, section_ids: Optional[Iterable[str]] = None - ) -> list[SectionKeyType]: + self, section_ids: Iterable[str] | None = None + ) -> list[SectionKey]: return self.section_keys( statuses={CompletionStatus.COMPLETED, CompletionStatus.IN_PROGRESS}, section_ids=section_ids, ) + + def is_block_complete(self, *, block_id: str, section_key: SectionKey) -> bool: + return block_id in self.get_completed_block_ids(section_key) diff --git a/app/data_models/questionnaire_store.py b/app/data_models/questionnaire_store.py index 2c5552d87d..3e5625eeb0 100644 --- a/app/data_models/questionnaire_store.py +++ b/app/data_models/questionnaire_store.py @@ -1,12 +1,14 @@ from __future__ import annotations from datetime import datetime -from types import MappingProxyType -from typing import TYPE_CHECKING, Any, Mapping, Optional +from typing import TYPE_CHECKING, MutableMapping from app.data_models.answer_store import AnswerStore +from app.data_models.data_stores import DataStores from app.data_models.list_store import ListStore +from app.data_models.metadata_proxy import MetadataProxy from app.data_models.progress_store import ProgressStore +from app.data_models.supplementary_data_store import SupplementaryDataStore from app.questionnaire.rules.utils import parse_iso_8601_datetime from app.utilities.json import json_dumps, json_loads @@ -20,21 +22,17 @@ class QuestionnaireStore: LATEST_VERSION = 1 def __init__( - self, storage: EncryptedQuestionnaireStorage, version: Optional[int] = None + self, storage: EncryptedQuestionnaireStorage, version: int | None = None ): self._storage = storage if version is None: version = self.get_latest_version_number() self.version = version - self._metadata: dict[str, Any] = {} - # self.metadata is a read-only view over self._metadata - self.metadata: Mapping[str, Any] = MappingProxyType(self._metadata) - self.response_metadata: Mapping[str, Any] = {} - self.list_store = ListStore() - self.answer_store = AnswerStore() - self.progress_store = ProgressStore() - self.submitted_at: Optional[datetime] - self.collection_exercise_sid: Optional[str] + self._metadata: MutableMapping = {} + self._stores = DataStores() + self.data_stores = self._stores + self.submitted_at: datetime | None + self.collection_exercise_sid: str | None ( raw_data, @@ -51,52 +49,54 @@ def __init__( def get_latest_version_number(self) -> int: return self.LATEST_VERSION - def set_metadata(self, to_set: dict[str, Any]) -> QuestionnaireStore: + def set_metadata(self, to_set: MutableMapping) -> QuestionnaireStore: """ Set metadata. This should only be used where absolutely necessary. Metadata should normally be read only. """ self._metadata = to_set - self.metadata = MappingProxyType(self._metadata) + self._stores.metadata = MetadataProxy.from_dict(self._metadata) return self def _deserialize(self, data: str) -> None: json_data = json_loads(data) - self.progress_store = ProgressStore(json_data.get("PROGRESS")) + self._stores.progress_store = ProgressStore(json_data.get("PROGRESS")) self.set_metadata(json_data.get("METADATA", {})) - self.answer_store = AnswerStore(json_data.get("ANSWERS")) - self.list_store = ListStore.deserialize(json_data.get("LISTS")) - self.response_metadata = json_data.get("RESPONSE_METADATA", {}) + self._stores.supplementary_data_store = SupplementaryDataStore.deserialize( + json_data.get("SUPPLEMENTARY_DATA", {}) + ) + self._stores.answer_store = AnswerStore(json_data.get("ANSWERS")) + self._stores.list_store = ListStore.deserialize(json_data.get("LISTS")) + self._stores.response_metadata = json_data.get("RESPONSE_METADATA", {}) def serialize(self) -> str: data = { "METADATA": self._metadata, - "ANSWERS": list(self.answer_store), - "LISTS": self.list_store.serialize(), - "PROGRESS": self.progress_store.serialize(), - "RESPONSE_METADATA": self.response_metadata, + "ANSWERS": list(self._stores.answer_store), + "SUPPLEMENTARY_DATA": self._stores.supplementary_data_store.serialize(), + "LISTS": self._stores.list_store.serialize(), + "PROGRESS": self._stores.progress_store.serialize(), + "RESPONSE_METADATA": self._stores.response_metadata, } return json_dumps(data) def delete(self) -> None: self._storage.delete() self._metadata.clear() - self.response_metadata = {} - self.answer_store.clear() - self.progress_store.clear() + self._stores.response_metadata = {} + self._stores.answer_store.clear() + self._stores.progress_store.clear() def save(self) -> None: data = self.serialize() collection_exercise_sid = ( self.collection_exercise_sid or self._metadata["collection_exercise_sid"] ) - response_expires_at = self._metadata.get("response_expires_at") + response_expires_at = self._metadata["response_expires_at"] self._storage.save( data=data, collection_exercise_sid=collection_exercise_sid, submitted_at=self.submitted_at, - expires_at=parse_iso_8601_datetime(response_expires_at) - if response_expires_at - else None, + expires_at=parse_iso_8601_datetime(response_expires_at), ) diff --git a/app/data_models/relationship_store.py b/app/data_models/relationship_store.py index 8beae8cd8a..26ef35b927 100644 --- a/app/data_models/relationship_store.py +++ b/app/data_models/relationship_store.py @@ -1,5 +1,5 @@ from dataclasses import asdict, dataclass -from typing import Iterable, Iterator, Optional, TypedDict, cast +from typing import Iterable, Iterator, TypedDict, cast class RelationshipDict(TypedDict, total=False): @@ -27,9 +27,7 @@ class RelationshipStore: Stores and updates relationships. """ - def __init__( - self, relationships: Optional[Iterable[RelationshipDict]] = None - ) -> None: + def __init__(self, relationships: Iterable[RelationshipDict] | None = None) -> None: self._is_dirty = False self._relationships = self._build_map(relationships or []) @@ -60,7 +58,7 @@ def serialize(self) -> list[RelationshipDict]: def get_relationship( self, list_item_id: str, to_list_item_id: str - ) -> Optional[Relationship]: + ) -> Relationship | None: key = (list_item_id, to_list_item_id) return self._relationships.get(key) @@ -83,16 +81,15 @@ def remove_all_relationships_for_list_item_id(self, list_item_id: str) -> None: This method iterates through the entire list of relationships. """ - keys_to_delete = [] - - for relationship in self: - if list_item_id in ( + keys_to_delete = [ + (relationship.list_item_id, relationship.to_list_item_id) + for relationship in self + if list_item_id + in ( relationship.to_list_item_id, relationship.list_item_id, - ): - keys_to_delete.append( - (relationship.list_item_id, relationship.to_list_item_id) - ) + ) + ] for key in keys_to_delete: del self._relationships[key] diff --git a/app/data_models/session_data.py b/app/data_models/session_data.py index 0457b2149b..31341c96f0 100644 --- a/app/data_models/session_data.py +++ b/app/data_models/session_data.py @@ -1,42 +1,14 @@ -from typing import Any, Optional +from typing import Any class SessionData: def __init__( self, - tx_id: Optional[str], - schema_name: Optional[str], - period_str: Optional[str], - language_code: Optional[str], - launch_language_code: Optional[str], - survey_url: Optional[str], - ru_name: Optional[str], - ru_ref: Optional[str], - response_id: Optional[str], - case_id: Optional[str], - case_ref: Optional[str] = None, - account_service_base_url: Optional[str] = None, - account_service_log_out_url: Optional[str] = None, - trad_as: Optional[str] = None, - display_address: Optional[str] = None, + language_code: str | None = None, confirmation_email_count: int = 0, feedback_count: int = 0, **_: Any, - ): # pylint: disable=too-many-locals - self.tx_id = tx_id - self.schema_name = schema_name - self.period_str = period_str + ): self.language_code = language_code - self.launch_language_code = launch_language_code - self.survey_url = survey_url - self.ru_name = ru_name - self.ru_ref = ru_ref - self.response_id = response_id - self.case_id = case_id - self.case_ref = case_ref - self.trad_as = trad_as - self.account_service_base_url = account_service_base_url - self.account_service_log_out_url = account_service_log_out_url - self.display_address = display_address self.confirmation_email_count = confirmation_email_count self.feedback_count = feedback_count diff --git a/app/data_models/session_store.py b/app/data_models/session_store.py index 04271b7fd2..ee73c90416 100644 --- a/app/data_models/session_store.py +++ b/app/data_models/session_store.py @@ -1,7 +1,6 @@ from __future__ import annotations from datetime import datetime -from typing import Optional from flask import current_app from jwcrypto.common import base64url_decode @@ -17,19 +16,19 @@ class SessionStore: def __init__( - self, user_ik: str, pepper: str, eq_session_id: Optional[str] = None + self, user_ik: str, pepper: str, eq_session_id: str | None = None ) -> None: self.eq_session_id = eq_session_id - self.user_id: Optional[str] = None + self.user_id: str | None = None self.user_ik = user_ik - self.session_data: Optional[SessionData] = None - self._eq_session: Optional[EQSession] = None + self.session_data: SessionData | None = None + self._eq_session: EQSession | None = None self.pepper = pepper if eq_session_id: self._load() @property - def expiration_time(self) -> Optional[datetime]: + def expiration_time(self) -> datetime | None: """ Checking if expires_at is available can be removed soon after deployment, it is only needed to cater for in-flight sessions. @@ -97,7 +96,7 @@ def _load(self) -> None: logger.debug( "finding eq_session_id in database", eq_session_id=self.eq_session_id ) - self._eq_session: Optional[EQSession] = current_app.eq["storage"].get(EQSession, self.eq_session_id) # type: ignore + self._eq_session: EQSession | None = current_app.eq["storage"].get(EQSession, self.eq_session_id) # type: ignore if self._eq_session and self._eq_session.session_data: self.user_id = self._eq_session.user_id diff --git a/app/data_models/supplementary_data_store.py b/app/data_models/supplementary_data_store.py new file mode 100644 index 0000000000..79ccb04392 --- /dev/null +++ b/app/data_models/supplementary_data_store.py @@ -0,0 +1,175 @@ +from __future__ import annotations + +from functools import cached_property +from typing import Iterable, Mapping, MutableMapping + +from werkzeug.datastructures import ImmutableDict + +from app.utilities.make_immutable import make_immutable +from app.utilities.types import ( + SupplementaryDataKeyType, + SupplementaryDataListMapping, + SupplementaryDataValueType, +) + + +class InvalidSupplementaryDataSelector(Exception): + pass + + +class SupplementaryDataStore: + """ + An object that stores supplementary data + """ + + def __init__( + self, + supplementary_data: MutableMapping | None = None, + list_mappings: Mapping[str, list[SupplementaryDataListMapping]] | None = None, + ): + """ + Initialised with the "data" value from the sds api response + and list mappings of the form + { + list_name: [ + {"identifier": identifier-1, "list_item_id": list_item_id-1 }, + {"identifier": identifier-2, "list_item_id": list_item_id-2 } + ] + } + """ + self._raw_data = supplementary_data or {} + self._list_mappings = list_mappings or {} + # use shallow copy of the data, as items will be popped off + self._data_map = self._build_map({**self._raw_data}) + + @cached_property + def raw_data(self) -> ImmutableDict: + data: ImmutableDict = make_immutable(self._raw_data) + return data + + @cached_property + def list_mappings(self) -> ImmutableDict[str, list[ImmutableDict]]: + mappings: ImmutableDict[str, list[ImmutableDict]] = make_immutable( + self._list_mappings + ) + return mappings + + @cached_property + def list_lookup(self) -> dict[str, dict[str | int, str]]: + """Create a lookup for easily finding the list_item_id for a given identifier""" + return { + list_name: { + mapping["identifier"]: mapping["list_item_id"] for mapping in list_data + } + for list_name, list_data in self._list_mappings.items() + } + + def _build_map( + self, data: MutableMapping + ) -> dict[SupplementaryDataKeyType, SupplementaryDataValueType]: + """ + The raw data will be of the form + { + "some_key": "some_value" + "items": { + "some_list": [ + {"identifier": ... }, + {"identifier": ... } + ] + } + } + each list item has an identifier which will link to a list-item-id in self.list_lookup + for example: {"some_list": {identifier-1: list_item_id-1, identifier-2: list_item_id-2 }} + + resulting map based off list mappings has the form + { + ("some_key", None): "some_value" + ("some_list", list_item_id-1): {"identifier": identifier-1, ...} + ("some_list", list_item_id-2): {"identifier": identifier-2, ...} + } + """ + list_items = data.pop("items", {}) + resulting_map: dict[SupplementaryDataKeyType, SupplementaryDataValueType] = { + (key, None): value for key, value in data.items() + } + for list_name, list_data in list_items.items(): + for item in list_data: + identifier = item["identifier"] + list_item_id = self.list_lookup[list_name][identifier] + resulting_map[(list_name, list_item_id)] = item + return resulting_map + + def get_data( + self, + *, + identifier: str, + selectors: Iterable[str] | None = None, + list_item_id: str | None = None, + ) -> SupplementaryDataValueType: + """ + Used to retrieve supplementary data in a similar style to AnswerStore + the identifier is the top level key for static data, and the name of the list for list items + selectors are used to reference nested data + + For example if you wanted the identifier for the first item in "some_list" + it would be get_data(identifier="some_list", selectors=["identifier"], list_item_id=list_item_id-1) + """ + if self.is_data_repeating(identifier) and not list_item_id: + values = [] + for _list_item_id in self.list_lookup.get(identifier, {}).values(): + value = self._resolve_value( + identifier=identifier, + selectors=selectors, + list_item_id=_list_item_id, + ) + if value is not None: + values.append(value) + return values + + return self._resolve_value( + identifier=identifier, selectors=selectors, list_item_id=list_item_id + ) + + def _resolve_value( + self, + *, + identifier: str, + selectors: Iterable[str] | None, + list_item_id: str | None, + ) -> dict | str | list | None: + value = self._data_map.get((identifier, list_item_id)) + # for nested data, index with each selector, or return None if there is no data to index + for selector in selectors or []: + if value is None: + return None + if not isinstance(value, Mapping): + # if value is not None, and also not index able, raise an error + invalid_selector_error_message = ( + f"Cannot use the selector `{selector}` on non-nested data" + ) + raise InvalidSupplementaryDataSelector(invalid_selector_error_message) + value = value.get(selector) + + return value + + def is_data_repeating(self, identifier: str) -> bool: + """ + Returns true if the identifier is for one of the lists + """ + return identifier in self._list_mappings + + def serialize(self) -> dict: + return { + "data": self._raw_data, + "list_mappings": self._list_mappings, + } + + @classmethod + def deserialize(cls, serialized_data: Mapping) -> SupplementaryDataStore: + if not serialized_data: + return cls() + + return cls( + supplementary_data=serialized_data["data"], + list_mappings=serialized_data["list_mappings"], + ) diff --git a/app/forms/__init__.py b/app/forms/__init__.py index edbeed9d72..2ec81a73fe 100644 --- a/app/forms/__init__.py +++ b/app/forms/__init__.py @@ -1,3 +1,3 @@ -from .error_messages import error_messages +from app.forms.error_messages import error_messages __all__ = ["error_messages"] diff --git a/app/forms/address_form.py b/app/forms/address_form.py index 15d8dea278..ffc344713f 100644 --- a/app/forms/address_form.py +++ b/app/forms/address_form.py @@ -24,8 +24,6 @@ class AddressForm(Form): @cached_property def data(self) -> dict[str, Any]: - # pylint: disable=no-member - # wtforms Form parents are not discoverable in the 2.3.3 implementation data_: dict[str, Any] = super().data return data_ diff --git a/app/forms/duration_form.py b/app/forms/duration_form.py index 62dd671373..e10038fa93 100644 --- a/app/forms/duration_form.py +++ b/app/forms/duration_form.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Callable, Mapping, Optional, Type +from typing import Callable, Mapping from wtforms import Form @@ -10,10 +10,9 @@ # pylint: disable=no-member -# wtforms Form parents are not discoverable in the 2.3.3 implementation class DurationForm(Form): def validate( - self, extra_validators: Optional[dict[str, list[Callable]]] = None + self, extra_validators: dict[str, list[Callable]] | None = None ) -> bool: super().validate(extra_validators) @@ -44,8 +43,8 @@ def _set_error(self, key: str) -> None: list(self._fields.values())[0].errors = [self.answer_errors[key]] @property - def data(self) -> Optional[dict[str, Optional[str]]]: - data: dict[str, Optional[str]] = super().data + def data(self) -> dict[str, str | None] | None: + data: dict[str, str | None] = super().data if all(value is None for value in data.values()): return None return data @@ -53,7 +52,7 @@ def data(self) -> Optional[dict[str, Optional[str]]]: def get_duration_form( answer: Mapping, error_messages: ErrorMessageType -) -> Type[DurationForm]: +) -> type[DurationForm]: class CustomDurationForm(DurationForm): mandatory = answer["mandatory"] units = answer["units"] diff --git a/app/forms/email_form.py b/app/forms/email_form.py index fbe898e4d5..080565ac71 100644 --- a/app/forms/email_form.py +++ b/app/forms/email_form.py @@ -7,8 +7,6 @@ from app.forms.validators import EmailTLDCheck -# pylint: disable=no-member -# wtforms Form parents are not discoverable in the 2.3.3 implementation class EmailForm(FlaskForm): email = StringField( validators=[ diff --git a/app/forms/error_messages.py b/app/forms/error_messages.py index 765affc190..8814067489 100644 --- a/app/forms/error_messages.py +++ b/app/forms/error_messages.py @@ -67,4 +67,7 @@ "SINGLE_DATE_PERIOD_TOO_EARLY": lazy_gettext("Enter a date after %(min)s"), "SINGLE_DATE_PERIOD_TOO_LATE": lazy_gettext("Enter a date before %(max)s"), "MUTUALLY_EXCLUSIVE": lazy_gettext("Remove an answer"), + "INVALID_YEAR_FORMAT": lazy_gettext( + "Enter the year in a valid format. For example, 2023." + ), } diff --git a/app/forms/field_handlers/__init__.py b/app/forms/field_handlers/__init__.py index 6f32ee4d20..53d5d96413 100644 --- a/app/forms/field_handlers/__init__.py +++ b/app/forms/field_handlers/__init__.py @@ -1,5 +1,3 @@ -from typing import Optional - from werkzeug.datastructures import ImmutableDict from app.forms.field_handlers.address_handler import AddressHandler @@ -49,7 +47,7 @@ def get_field_handler( rule_evaluator: RuleEvaluator, error_messages: ImmutableDict, disable_validation: bool = False, - question_title: Optional[str] = None, + question_title: str | None = None, ) -> FieldHandler: return FIELD_HANDLER_MAPPINGS[answer_schema["type"]]( answer_schema=answer_schema, diff --git a/app/forms/field_handlers/address_handler.py b/app/forms/field_handlers/address_handler.py index 5c3baa3ec3..2ccd5c3985 100644 --- a/app/forms/field_handlers/address_handler.py +++ b/app/forms/field_handlers/address_handler.py @@ -1,6 +1,7 @@ from functools import cached_property from wtforms import FormField +from wtforms.fields.core import UnboundField from wtforms.validators import InputRequired from app.forms.address_form import AddressValidatorTypes, get_address_form @@ -28,7 +29,7 @@ def validators(self) -> AddressValidatorTypes: return validate_with - def get_field(self) -> FormField: + def get_field(self) -> UnboundField | FormField: return FormField( get_address_form(self.validators), label=self.label, diff --git a/app/forms/field_handlers/date_handlers.py b/app/forms/field_handlers/date_handlers.py index cecd104d5f..e5882c5e53 100644 --- a/app/forms/field_handlers/date_handlers.py +++ b/app/forms/field_handlers/date_handlers.py @@ -1,8 +1,9 @@ from datetime import datetime, timezone from functools import cached_property -from typing import Any, Optional, Union +from typing import Any from dateutil.relativedelta import relativedelta +from wtforms.fields.core import UnboundField from app.forms.field_handlers.field_handler import FieldHandler from app.forms.fields import DateField, MonthYearDateField, YearDateField @@ -14,10 +15,7 @@ format_message_with_title, ) from app.questionnaire.rules.utils import parse_datetime - -DateValidatorTypes = list[ - Union[OptionalForm, DateRequired, DateCheck, SingleDatePeriodCheck] -] +from app.utilities.types import DateValidatorType class DateHandler(FieldHandler): @@ -26,9 +24,8 @@ class DateHandler(FieldHandler): DISPLAY_FORMAT = "d MMMM yyyy" @cached_property - def validators(self) -> DateValidatorTypes: - - validate_with: DateValidatorTypes = [OptionalForm()] + def validators(self) -> list[DateValidatorType]: + validate_with: list[DateValidatorType] = [OptionalForm()] if self.answer_schema["mandatory"] is True: validate_with = [ @@ -53,11 +50,13 @@ def validators(self) -> DateValidatorTypes: validate_with.append(min_max_validator) return validate_with - def get_field(self) -> DateField: - return DateField(self.validators, label=self.label, description=self.guidance) + def get_field(self) -> UnboundField | DateField: + return DateField( + validators=self.validators, label=self.label, description=self.guidance + ) def get_min_max_validator( - self, minimum_date: Optional[datetime], maximum_date: Optional[datetime] + self, minimum_date: datetime | None, maximum_date: datetime | None ) -> SingleDatePeriodCheck: messages = self.answer_schema.get("validation", {}).get("messages") @@ -68,7 +67,7 @@ def get_min_max_validator( maximum_date=maximum_date, ) - def get_referenced_date(self, key: str) -> Optional[datetime]: + def get_referenced_date(self, key: str) -> datetime | None: """ Gets value of the referenced date type, whether it is a value, id of an answer or a meta date. @@ -84,8 +83,8 @@ def get_referenced_date(self, key: str) -> Optional[datetime]: @staticmethod def transform_date_by_offset( - date_to_offset: Optional[datetime], offset: dict[str, int] - ) -> Optional[datetime]: + date_to_offset: datetime | None, offset: dict[str, int] + ) -> datetime | None: """ Adds/subtracts offset from a date and returns the new offset value @@ -103,7 +102,7 @@ def transform_date_by_offset( return date_to_offset - def get_date_value(self, key: str) -> Optional[datetime]: + def get_date_value(self, key: str) -> datetime | None: """ Gets attributes within a minimum or maximum of a date field and validates that the entered date is valid. @@ -124,13 +123,13 @@ class MonthYearDateHandler(DateHandler): DATE_FORMAT = "yyyy-mm" DISPLAY_FORMAT = "MMMM yyyy" - def get_field(self) -> MonthYearDateField: + def get_field(self) -> UnboundField | MonthYearDateField: return MonthYearDateField( - self.validators, label=self.label, description=self.guidance + validators=self.validators, label=self.label, description=self.guidance ) def get_min_max_validator( - self, minimum_date: Optional[datetime], maximum_date: Optional[datetime] + self, minimum_date: datetime | None, maximum_date: datetime | None ) -> SingleDatePeriodCheck: messages = self.answer_schema.get("validation", {}).get("messages") @@ -153,13 +152,13 @@ class YearDateHandler(DateHandler): DATE_FORMAT = "yyyy" DISPLAY_FORMAT = "yyyy" - def get_field(self) -> YearDateField: + def get_field(self) -> UnboundField | YearDateField: return YearDateField( - self.validators, label=self.label, description=self.guidance + validators=self.validators, label=self.label, description=self.guidance ) def get_min_max_validator( - self, minimum_date: Optional[datetime], maximum_date: Optional[datetime] + self, minimum_date: datetime | None, maximum_date: datetime | None ) -> SingleDatePeriodCheck: messages = self.answer_schema.get("validation", {}).get("messages") diff --git a/app/forms/field_handlers/dropdown_handler.py b/app/forms/field_handlers/dropdown_handler.py index ec969293dc..eae1f794b3 100644 --- a/app/forms/field_handlers/dropdown_handler.py +++ b/app/forms/field_handlers/dropdown_handler.py @@ -2,13 +2,11 @@ from flask_babel import lazy_gettext from wtforms import SelectField +from wtforms.fields.core import UnboundField -from app.forms.field_handlers.select_handlers import ( - Choice, - ChoiceWithDetailAnswer, - SelectHandlerBase, -) +from app.forms.field_handlers.select_handlers import SelectHandlerBase from app.questionnaire.questionnaire_schema import InvalidSchemaConfigurationException +from app.utilities.types import Choice, ChoiceWithDetailAnswer class DropdownHandler(SelectHandlerBase): @@ -21,7 +19,7 @@ def choices(self) -> Sequence[Choice]: self._build_dynamic_choices() + self._build_static_choices() ) if not _choices: - raise InvalidSchemaConfigurationException("No dynamic or static choices") + raise InvalidSchemaConfigurationException() return [ Choice("", self._get_placeholder_text()), @@ -31,7 +29,7 @@ def choices(self) -> Sequence[Choice]: def _get_placeholder_text(self) -> str: return self.answer_schema.get("placeholder", self.DEFAULT_PLACEHOLDER) - def get_field(self) -> SelectField: + def get_field(self) -> UnboundField | SelectField: return SelectField( label=self.label, description=self.guidance, diff --git a/app/forms/field_handlers/duration_handler.py b/app/forms/field_handlers/duration_handler.py index e8024900a9..8c691d1d41 100644 --- a/app/forms/field_handlers/duration_handler.py +++ b/app/forms/field_handlers/duration_handler.py @@ -1,4 +1,5 @@ from wtforms import FormField +from wtforms.fields.core import UnboundField from app.forms.duration_form import get_duration_form from app.forms.field_handlers.field_handler import FieldHandler @@ -7,7 +8,7 @@ class DurationHandler(FieldHandler): MANDATORY_MESSAGE_KEY = "MANDATORY_DURATION" - def get_field(self) -> FormField: + def get_field(self) -> UnboundField | FormField: return FormField( get_duration_form(self.answer_schema, dict(self.error_messages)), label=self.label, diff --git a/app/forms/field_handlers/field_handler.py b/app/forms/field_handlers/field_handler.py index b416aa5ee5..4cb0d736ce 100644 --- a/app/forms/field_handlers/field_handler.py +++ b/app/forms/field_handlers/field_handler.py @@ -1,6 +1,6 @@ from abc import ABC from functools import cached_property -from typing import Any, Mapping, Optional, Union +from typing import Any, Mapping from wtforms import Field, validators from wtforms.validators import Optional as OptionalValidator @@ -24,7 +24,7 @@ def __init__( rule_evaluator: RuleEvaluator, error_messages: Mapping[str, str], disable_validation: bool = False, - question_title: Optional[str] = None, + question_title: str | None = None, ): self.answer_schema = answer_schema self.value_source_resolver = value_source_resolver @@ -44,7 +44,7 @@ def validators(self) -> list[validators.Optional]: return [] @cached_property - def label(self) -> Optional[str]: + def label(self) -> str | None: return self.answer_schema.get("label") @cached_property @@ -57,7 +57,7 @@ def get_validation_message(self, message_key: str) -> str: or self.error_messages[message_key] ) - def get_mandatory_validator(self) -> Union[ResponseRequired, OptionalValidator]: + def get_mandatory_validator(self) -> ResponseRequired | OptionalValidator: if self.answer_schema["mandatory"] is True: mandatory_message = self.get_validation_message(self.MANDATORY_MESSAGE_KEY) @@ -70,12 +70,12 @@ def get_mandatory_validator(self) -> Union[ResponseRequired, OptionalValidator]: def get_schema_value( self, schema_element: dict - ) -> Union[ValueSourceEscapedTypes, ValueSourceTypes]: + ) -> ValueSourceEscapedTypes | ValueSourceTypes: if isinstance(schema_element["value"], dict): return self.value_source_resolver.resolve(schema_element["value"]) - schema_element_value: Union[ - ValueSourceEscapedTypes, ValueSourceTypes - ] = schema_element["value"] + schema_element_value: ValueSourceEscapedTypes | ValueSourceTypes = ( + schema_element["value"] + ) return schema_element_value def get_field(self) -> Field: diff --git a/app/forms/field_handlers/mobile_number_handler.py b/app/forms/field_handlers/mobile_number_handler.py index ecf95ddfdf..fc78691a52 100644 --- a/app/forms/field_handlers/mobile_number_handler.py +++ b/app/forms/field_handlers/mobile_number_handler.py @@ -1,12 +1,12 @@ from functools import cached_property -from typing import Union from wtforms import StringField +from wtforms.fields.core import UnboundField from app.forms.field_handlers.field_handler import FieldHandler from app.forms.validators import MobileNumberCheck, ResponseRequired -MobileNumberValidatorTypes = list[Union[ResponseRequired, MobileNumberCheck]] +MobileNumberValidatorTypes = list[ResponseRequired | MobileNumberCheck] class MobileNumberHandler(FieldHandler): @@ -21,7 +21,7 @@ def validators(self) -> MobileNumberValidatorTypes: return validate_with - def get_field(self) -> StringField: + def get_field(self) -> UnboundField | StringField: return StringField( label=self.label, description=self.guidance, validators=self.validators ) diff --git a/app/forms/field_handlers/number_handler.py b/app/forms/field_handlers/number_handler.py index 8142c7c983..78ff7a3e83 100644 --- a/app/forms/field_handlers/number_handler.py +++ b/app/forms/field_handlers/number_handler.py @@ -1,7 +1,8 @@ from functools import cached_property -from typing import Any, Type, Union +from typing import Any from wtforms import DecimalField, IntegerField +from wtforms.fields.core import UnboundField from app.forms.field_handlers.field_handler import FieldHandler from app.forms.fields import DecimalFieldWithSeparator, IntegerFieldWithSeparator @@ -14,7 +15,7 @@ from app.settings import MAX_NUMBER NumberValidatorTypes = list[ - Union[ResponseRequired, NumberCheck, NumberRange, DecimalPlaces] + ResponseRequired | NumberCheck | NumberRange | DecimalPlaces ] @@ -57,21 +58,29 @@ def max_decimals(self) -> int: @property def _field_type( self, - ) -> Type[Union[DecimalFieldWithSeparator, IntegerFieldWithSeparator]]: + ) -> type[DecimalFieldWithSeparator | IntegerFieldWithSeparator]: return ( DecimalFieldWithSeparator if self.max_decimals > 0 else IntegerFieldWithSeparator ) - def get_field(self) -> Union[DecimalField, IntegerField]: + def get_field(self) -> UnboundField | DecimalField | IntegerField: + additional_args = ( + {"places": self.max_decimals} + if self._field_type == DecimalFieldWithSeparator + else {} + ) return self._field_type( - label=self.label, validators=self.validators, description=self.guidance + label=self.label, + validators=self.validators, + description=self.guidance, + **additional_args, ) def _get_number_field_validators( self, - ) -> list[Union[NumberCheck, NumberRange, DecimalPlaces]]: + ) -> list[NumberCheck | NumberRange | DecimalPlaces]: answer_errors = dict(self.error_messages) for error_key in self.validation_messages.keys(): diff --git a/app/forms/field_handlers/select_handlers.py b/app/forms/field_handlers/select_handlers.py index 647110505c..bd3533df7b 100644 --- a/app/forms/field_handlers/select_handlers.py +++ b/app/forms/field_handlers/select_handlers.py @@ -1,5 +1,6 @@ -from collections import namedtuple -from typing import Any, Optional, Sequence, Union +from typing import Any, Sequence + +from wtforms.fields.core import UnboundField from app.forms.field_handlers.field_handler import FieldHandler from app.forms.fields import ( @@ -8,13 +9,7 @@ ) from app.questionnaire.dynamic_answer_options import DynamicAnswerOptions from app.questionnaire.questionnaire_schema import InvalidSchemaConfigurationException - -Choice = namedtuple("Choice", "value label") -ChoiceWithDetailAnswer = namedtuple( - "ChoiceWithDetailAnswer", "value label detail_answer_id" -) - -ChoiceType = Union[Choice, ChoiceWithDetailAnswer] +from app.utilities.types import ChoiceType, ChoiceWithDetailAnswer class SelectHandlerBase(FieldHandler): @@ -22,7 +17,7 @@ class SelectHandlerBase(FieldHandler): def choices(self) -> Sequence[ChoiceType]: _choices = self._build_dynamic_choices() + self._build_static_choices() if not _choices: - raise InvalidSchemaConfigurationException("No dynamic or static choices") + raise InvalidSchemaConfigurationException() return _choices @property @@ -61,7 +56,7 @@ class SelectHandler(SelectHandlerBase): MANDATORY_MESSAGE_KEY = "MANDATORY_RADIO" @staticmethod - def coerce_str_unless_none(value: Optional[str]) -> Optional[str]: + def coerce_str_unless_none(value: str | None) -> str | None: """ Coerces a value using str() unless that value is None :param value: Any value that can be coerced using str() or None @@ -75,7 +70,7 @@ def coerce_str_unless_none(value: Optional[str]) -> Optional[str]: # not providing an answer and them selecting the 'None' option otherwise. # https://github.com/ONSdigital/eq-survey-runner/issues/1013 # See related WTForms PR: https://github.com/wtforms/wtforms/pull/288 - def get_field(self) -> SelectFieldWithDetailAnswer: + def get_field(self) -> UnboundField | SelectFieldWithDetailAnswer: return SelectFieldWithDetailAnswer( label=self.label, description=self.guidance, @@ -88,7 +83,7 @@ def get_field(self) -> SelectFieldWithDetailAnswer: class SelectMultipleHandler(SelectHandler): MANDATORY_MESSAGE_KEY = "MANDATORY_CHECKBOX" - def get_field(self) -> MultipleSelectFieldWithDetailAnswer: + def get_field(self) -> UnboundField | MultipleSelectFieldWithDetailAnswer: return MultipleSelectFieldWithDetailAnswer( label=self.label, description=self.guidance, diff --git a/app/forms/field_handlers/string_handler.py b/app/forms/field_handlers/string_handler.py index 661a645c41..2f1d6d45cd 100644 --- a/app/forms/field_handlers/string_handler.py +++ b/app/forms/field_handlers/string_handler.py @@ -1,12 +1,12 @@ from functools import cached_property -from typing import Union from wtforms import StringField, validators +from wtforms.fields.core import UnboundField from wtforms.validators import Length from app.forms.field_handlers.field_handler import FieldHandler -StringValidatorTypes = list[Union[validators.Optional, validators.Length]] +StringValidatorTypes = list[validators.Optional | validators.Length] class StringHandler(FieldHandler): @@ -33,7 +33,7 @@ def max_length(self) -> int: max_length: int = self.answer_schema.get("max_length", self.MAX_LENGTH) return max_length - def get_field(self) -> StringField: + def get_field(self) -> UnboundField | StringField: return StringField( label=self.label, description=self.guidance, validators=self.validators ) diff --git a/app/forms/field_handlers/text_area_handler.py b/app/forms/field_handlers/text_area_handler.py index 1d9eedfd82..d0885b5abe 100644 --- a/app/forms/field_handlers/text_area_handler.py +++ b/app/forms/field_handlers/text_area_handler.py @@ -1,13 +1,13 @@ from functools import cached_property -from typing import Union from wtforms import validators +from wtforms.fields.core import UnboundField from wtforms.validators import Length from app.forms.field_handlers.field_handler import FieldHandler from app.forms.fields import MaxTextAreaField -TextAreaValidatorTypes = list[Union[validators.Optional, validators.Length]] +TextAreaValidatorTypes = list[validators.Optional | validators.Length] class TextAreaHandler(FieldHandler): @@ -32,7 +32,7 @@ def get_length_validator(self) -> Length: return validators.length(-1, self.max_length, message=length_message) - def get_field(self) -> MaxTextAreaField: + def get_field(self) -> UnboundField | MaxTextAreaField: return MaxTextAreaField( label=self.label, description=self.guidance, diff --git a/app/forms/fields/__init__.py b/app/forms/fields/__init__.py index 240938d377..65c1e9c936 100644 --- a/app/forms/fields/__init__.py +++ b/app/forms/fields/__init__.py @@ -12,9 +12,11 @@ from app.forms.fields.multiple_select_field_with_detail_answer import ( MultipleSelectFieldWithDetailAnswer, ) - -from .select_field_with_detail_answer import SelectField, SelectFieldWithDetailAnswer -from .year_date_field import YearDateField +from app.forms.fields.select_field_with_detail_answer import ( + SelectField, + SelectFieldWithDetailAnswer, +) +from app.forms.fields.year_date_field import YearDateField __all__ = [ "DateField", diff --git a/app/forms/fields/date_field.py b/app/forms/fields/date_field.py index afe1f0f481..85754fe9ee 100644 --- a/app/forms/fields/date_field.py +++ b/app/forms/fields/date_field.py @@ -1,13 +1,17 @@ import logging from functools import cached_property +from typing import Any, Callable, Sequence +from werkzeug.datastructures import MultiDict from wtforms import Form, FormField, StringField -from wtforms.utils import unset_value +from wtforms.utils import UnsetValue, unset_value + +from app.utilities.types import DateValidatorType logger = logging.getLogger(__name__) -def get_form_class(validators): +def get_form_class(validators: Sequence[DateValidatorType]) -> type[Form]: class DateForm(Form): # Validation is only ever added to the 1 field that shows in all 3 variants # This is to prevent an error message for each input box @@ -16,9 +20,7 @@ class DateForm(Form): day = StringField() @cached_property - def data(self): - # pylint: disable=no-member - # wtforms Form parents are not discoverable in the 2.3.3 implementation + def data(self) -> str | None: data = super().data try: @@ -30,11 +32,24 @@ def data(self): class DateField(FormField): - def __init__(self, validators, **kwargs): + def __init__( + self, + *, + validators: Sequence[DateValidatorType], + **kwargs: Any, + ) -> None: form_class = get_form_class(validators) - super().__init__(form_class, **kwargs) - - def process(self, formdata, data=unset_value, extra_filters=None): + super().__init__( + form_class, + **kwargs, + ) + + def process( + self, + formdata: MultiDict | None = None, + data: str | UnsetValue = unset_value, + extra_filters: Sequence[Callable] | None = None, + ) -> None: if data is not unset_value: substrings = data.split("-") data = {"year": substrings[0], "month": substrings[1], "day": substrings[2]} diff --git a/app/forms/fields/decimal_field_with_separator.py b/app/forms/fields/decimal_field_with_separator.py index 3790f2fbba..24e574653a 100644 --- a/app/forms/fields/decimal_field_with_separator.py +++ b/app/forms/fields/decimal_field_with_separator.py @@ -1,9 +1,9 @@ from decimal import Decimal, InvalidOperation +from typing import Any, Sequence -from babel import numbers from wtforms import DecimalField -from app.settings import DEFAULT_LOCALE +from app.helpers.form_helpers import sanitise_number class DecimalFieldWithSeparator(DecimalField): @@ -16,16 +16,13 @@ class DecimalFieldWithSeparator(DecimalField): DecimalPlace validators """ - def __init__(self, **kwargs): + def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) - self.data = None - - def process_formdata(self, valuelist): + self.data: Decimal | None = None + def process_formdata(self, valuelist: Sequence[str] | None = None) -> None: if valuelist: try: - self.data = Decimal( - valuelist[0].replace(numbers.get_group_symbol(DEFAULT_LOCALE), "") - ) + self.data = Decimal(sanitise_number(valuelist[0])) except (ValueError, TypeError, InvalidOperation): pass diff --git a/app/forms/fields/integer_field_with_separator.py b/app/forms/fields/integer_field_with_separator.py index 812e807a5c..e4af166f22 100644 --- a/app/forms/fields/integer_field_with_separator.py +++ b/app/forms/fields/integer_field_with_separator.py @@ -1,7 +1,8 @@ -from babel import numbers +from typing import Any, Sequence + from wtforms import IntegerField -from app.settings import DEFAULT_LOCALE +from app.helpers.form_helpers import sanitise_number class IntegerFieldWithSeparator(IntegerField): @@ -14,16 +15,13 @@ class IntegerFieldWithSeparator(IntegerField): DecimalPlace validators """ - def __init__(self, **kwargs): + def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) - self.data = None - - def process_formdata(self, valuelist): + self.data: int | None = None + def process_formdata(self, valuelist: Sequence[str] | None = None) -> None: if valuelist: try: - self.data = int( - valuelist[0].replace(numbers.get_group_symbol(DEFAULT_LOCALE), "") - ) + self.data = int(sanitise_number(valuelist[0])) except ValueError: pass diff --git a/app/forms/fields/max_text_area_field.py b/app/forms/fields/max_text_area_field.py index 563351c897..a32b8f318d 100644 --- a/app/forms/fields/max_text_area_field.py +++ b/app/forms/fields/max_text_area_field.py @@ -1,8 +1,16 @@ +from typing import Any + from wtforms import TextAreaField class MaxTextAreaField(TextAreaField): - def __init__(self, label="", validators=None, rows=None, maxlength=None, **kwargs): - super().__init__(label, validators, **kwargs) + def __init__( + self, + *, + rows: int, + maxlength: int, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) self.rows = rows self.maxlength = maxlength diff --git a/app/forms/fields/month_year_date_field.py b/app/forms/fields/month_year_date_field.py index 4bee2feb8e..d9afb6adc4 100644 --- a/app/forms/fields/month_year_date_field.py +++ b/app/forms/fields/month_year_date_field.py @@ -1,21 +1,23 @@ import logging from functools import cached_property +from typing import Any, Callable, Sequence +from werkzeug.datastructures import MultiDict from wtforms import Form, FormField, StringField -from wtforms.utils import unset_value +from wtforms.utils import UnsetValue, unset_value + +from app.utilities.types import DateValidatorType logger = logging.getLogger(__name__) -def get_form_class(validators): +def get_form_class(validators: Sequence[DateValidatorType]) -> type[Form]: class YearMonthDateForm(Form): year = StringField(validators=validators) month = StringField() @cached_property - def data(self): - # pylint: disable=no-member - # wtforms Form parents are not discoverable in the 2.3.3 implementation + def data(self) -> str | None: data = super().data try: @@ -27,11 +29,24 @@ def data(self): class MonthYearDateField(FormField): - def __init__(self, validators, **kwargs): + def __init__( + self, + *, + validators: Sequence[DateValidatorType], + **kwargs: Any, + ) -> None: form_class = get_form_class(validators) - super().__init__(form_class, **kwargs) - - def process(self, formdata, data=unset_value, extra_filters=None): + super().__init__( + form_class, + **kwargs, + ) + + def process( + self, + formdata: MultiDict | None = None, + data: str | UnsetValue = unset_value, + extra_filters: Sequence[Callable] | None = None, + ) -> None: if data is not unset_value: substrings = data.split("-") data = {"year": substrings[0], "month": substrings[1]} diff --git a/app/forms/fields/multiple_select_field_with_detail_answer.py b/app/forms/fields/multiple_select_field_with_detail_answer.py index 9b1db7385a..4276285041 100644 --- a/app/forms/fields/multiple_select_field_with_detail_answer.py +++ b/app/forms/fields/multiple_select_field_with_detail_answer.py @@ -1,4 +1,8 @@ -from wtforms import SelectMultipleField +from typing import Any, Generator, Sequence + +from wtforms import SelectFieldBase, SelectMultipleField + +from app.utilities.types import ChoiceType, ChoiceWidgetRenderType class MultipleSelectFieldWithDetailAnswer(SelectMultipleField): @@ -7,13 +11,24 @@ class MultipleSelectFieldWithDetailAnswer(SelectMultipleField): This saves us having to later map options with their detail_answer. """ - def __init__(self, **kwargs): - super().__init__(**kwargs) - - def __iter__(self): - opts = dict( - widget=self.option_widget, name=self.name, _form=None, _meta=self.meta + def __init__( + self, + *, + choices: Sequence[ChoiceType], + **kwargs: Any, + ) -> None: + super().__init__( + choices=choices, + **kwargs, ) + + def __iter__(self) -> Generator[SelectFieldBase._Option, None, None]: + opts = { + "widget": self.option_widget, + "name": self.name, + "_form": None, + "_meta": self.meta, + } for i, (value, label, checked, detail_answer_id) in enumerate( self.iter_choices() ): @@ -23,7 +38,7 @@ def __iter__(self): opt.checked = checked yield opt - def iter_choices(self): + def iter_choices(self) -> Generator[ChoiceWidgetRenderType, None, None]: for value, label, detail_answer_id in self.choices: selected = self.data is not None and self.coerce(value) in self.data yield value, label, selected, detail_answer_id diff --git a/app/forms/fields/select_field_with_detail_answer.py b/app/forms/fields/select_field_with_detail_answer.py index 87040c935f..a52384d7d1 100644 --- a/app/forms/fields/select_field_with_detail_answer.py +++ b/app/forms/fields/select_field_with_detail_answer.py @@ -1,6 +1,10 @@ -from wtforms import SelectField +from typing import Any, Generator, Sequence + +from wtforms import SelectField, SelectFieldBase from wtforms.validators import ValidationError +from app.utilities.types import ChoiceType, ChoiceWidgetRenderType + class SelectFieldWithDetailAnswer(SelectField): """ @@ -8,13 +12,24 @@ class SelectFieldWithDetailAnswer(SelectField): This saves us having to later map options with their detail_answer. """ - def __init__(self, **kwargs): - super().__init__(**kwargs) - - def __iter__(self): - opts = dict( - widget=self.option_widget, name=self.name, _form=None, _meta=self.meta + def __init__( + self, + *, + choices: Sequence[ChoiceType], + **kwargs: Any, + ) -> None: + super().__init__( + choices=choices, + **kwargs, ) + + def __iter__(self) -> Generator[SelectFieldBase._Option, None, None]: + opts = { + "widget": self.option_widget, + "name": self.name, + "_form": None, + "_meta": self.meta, + } for i, (value, label, checked, detail_answer_id) in enumerate( self.iter_choices() ): @@ -24,11 +39,11 @@ def __iter__(self): opt.checked = checked yield opt - def iter_choices(self): + def iter_choices(self) -> Generator[ChoiceWidgetRenderType, None, None]: for value, label, detail_answer_id in self.choices: yield value, label, self.coerce(value) == self.data, detail_answer_id - def pre_validate(self, _): + def pre_validate(self, _: Any) -> None: for _, _, match, _ in self.iter_choices(): if match: break diff --git a/app/forms/fields/year_date_field.py b/app/forms/fields/year_date_field.py index f78915ef8c..7c67405518 100644 --- a/app/forms/fields/year_date_field.py +++ b/app/forms/fields/year_date_field.py @@ -1,20 +1,22 @@ import logging from functools import cached_property +from typing import Any, Callable, Sequence +from werkzeug.datastructures import MultiDict from wtforms import Form, FormField, StringField -from wtforms.utils import unset_value +from wtforms.utils import UnsetValue, unset_value + +from app.utilities.types import DateValidatorType logger = logging.getLogger(__name__) -def get_form_class(validators): +def get_form_class(validators: Sequence[DateValidatorType]) -> type[Form]: class YearDateForm(Form): year = StringField(validators=validators) @cached_property - def data(self): - # pylint: disable=no-member - # wtforms Form parents are not discoverable in the 2.3.3 implementation + def data(self) -> str | None: data = super().data try: @@ -26,11 +28,24 @@ def data(self): class YearDateField(FormField): - def __init__(self, validators, **kwargs): + def __init__( + self, + *, + validators: Sequence[DateValidatorType], + **kwargs: Any, + ): form_class = get_form_class(validators) - super().__init__(form_class, **kwargs) - - def process(self, formdata, data=None, extra_filters=None): + super().__init__( + form_class, + **kwargs, + ) + + def process( + self, + formdata: MultiDict | None = None, + data: str | UnsetValue = unset_value, + extra_filters: Sequence[Callable] | None = None, + ) -> None: if data is not unset_value: substrings = data.split("-") data = {"year": substrings[0]} diff --git a/app/forms/questionnaire_form.py b/app/forms/questionnaire_form.py index 596cb669e3..58a20e33d5 100644 --- a/app/forms/questionnaire_form.py +++ b/app/forms/questionnaire_form.py @@ -5,21 +5,27 @@ from collections.abc import Callable from datetime import datetime, timedelta, timezone from decimal import Decimal -from typing import Any, Mapping, Optional, Sequence, Union +from typing import Any, Mapping, Sequence from dateutil.relativedelta import relativedelta from flask_wtf import FlaskForm from werkzeug.datastructures import ImmutableMultiDict, MultiDict from wtforms import validators -from app.data_models import AnswerStore, AnswerValueTypes, ListStore +from app.data_models.data_stores import DataStores from app.forms import error_messages from app.forms.field_handlers import DateHandler, FieldHandler, get_field_handler from app.forms.validators import DateRangeCheck, MutuallyExclusiveCheck, SumCheck -from app.questionnaire import Location, QuestionnaireSchema, QuestionSchema +from app.questionnaire import Location, QuestionnaireSchema, QuestionSchemaType +from app.questionnaire.dependencies import ( + get_routing_path_block_ids_by_section_for_calculation_summary_dependencies, +) +from app.questionnaire.path_finder import PathFinder from app.questionnaire.relationship_location import RelationshipLocation from app.questionnaire.rules.rule_evaluator import RuleEvaluator from app.questionnaire.value_source_resolver import ValueSourceResolver +from app.utilities.mappings import get_flattened_mapping_values +from app.utilities.types import LocationType, SectionKey logger = logging.getLogger(__name__) @@ -27,39 +33,42 @@ QuestionnaireExtraValidators = Mapping[str, Sequence[Callable]] Period = Mapping[str, int] PeriodLimits = Mapping[str, Any] -Error = Union[Mapping, Sequence] +Error = Mapping | Sequence Errors = Mapping[str, Error] ErrorList = Sequence[tuple[str, str]] # pylint: disable=too-many-locals class QuestionnaireForm(FlaskForm): + PERIOD_RANGE_ERROR_MESSAGE = "Period range must have a start and end date" + def __init__( self, schema: QuestionnaireSchema, - question_schema: QuestionSchema, - answer_store: AnswerStore, - list_store: ListStore, - metadata: Mapping[str, Any], - response_metadata: Mapping[str, Any], - location: Union[None, Location, RelationshipLocation], - **kwargs: Union[MultiDict[str, Any], Mapping[str, Any], None], + question_schema: QuestionSchemaType, + data_stores: DataStores, + location: None | Location | RelationshipLocation, + **kwargs: MultiDict | Mapping | None, ): self.schema = schema self.question = question_schema - self.answer_store = answer_store - self.list_store = list_store - self.metadata = metadata - self.response_metadata = response_metadata self.location = location self.question_errors: dict[str, str] = {} self.options_with_detail_answer: dict = {} self.question_title = self.question.get("title", "") + self.data_stores = data_stores + + self.value_source_resolver = ValueSourceResolver( + schema=self.schema, + data_stores=data_stores, + location=self.location, + list_item_id=self.location.list_item_id if self.location else None, + ) super().__init__(**kwargs) def validate( - self, extra_validators: Optional[QuestionnaireExtraValidators] = None + self, extra_validators: QuestionnaireExtraValidators | None = None ) -> bool: """ Validate this form as usual and check for any form-level validation errors based on question type @@ -92,7 +101,7 @@ def validate( and valid_mutually_exclusive_form ) - def validate_date_range_question(self, question: QuestionSchema) -> bool: + def validate_date_range_question(self, question: QuestionSchemaType) -> bool: date_from = question["answers"][0] date_to = question["answers"][1] if self._has_min_and_max_single_dates(date_from, date_to): @@ -130,15 +139,17 @@ def validate_date_range_question(self, question: QuestionSchema) -> bool: return True - def validate_calculated_question(self, question: QuestionSchema) -> bool: + def validate_calculated_question(self, question: QuestionSchemaType) -> bool: for calculation in question["calculations"]: - result = self._get_target_total_and_currency(calculation, question) - if result: - target_total, currency = result + if result := self._get_target_total_and_currency(calculation, question): if self.answers_all_valid( calculation["answers_to_calculate"] ) and self._validate_calculated_question( - calculation, question, target_total, currency + calculation=calculation, + question=question, + target_total=result["target_total"], + currency=result["currency"], + decimal_places=result["decimal_places"], ): # Remove any previous question errors if it passes this OR before returning True if question["id"] in self.question_errors: @@ -147,23 +158,24 @@ def validate_calculated_question(self, question: QuestionSchema) -> bool: return False - def validate_mutually_exclusive_question(self, question: QuestionSchema) -> bool: + def validate_mutually_exclusive_question( + self, question: QuestionSchemaType + ) -> bool: is_mandatory: bool = question["mandatory"] messages = ( question["validation"].get("messages") if "validation" in question else None ) answers = (getattr(self, answer["id"]).data for answer in question["answers"]) - is_only_checkboxes = all( - answer["type"] == "Checkbox" for answer in question["answers"] + is_only_checkboxes_or_radios = all( + answer["type"] in {"Checkbox", "Radio"} for answer in question["answers"] ) - validator = MutuallyExclusiveCheck( messages=messages, question_title=self.question_title, ) try: - validator(answers, is_mandatory, is_only_checkboxes) + validator(answers, is_mandatory, is_only_checkboxes_or_radios) except validators.ValidationError as e: self.question_errors[question["id"]] = str(e) @@ -173,30 +185,37 @@ def validate_mutually_exclusive_question(self, question: QuestionSchema) -> bool def _get_target_total_and_currency( self, calculation: Calculation, - question: QuestionSchema, - ) -> Optional[tuple[Union[Calculation, AnswerValueTypes], Optional[str]]]: - - calculation_value: Union[Calculation, AnswerValueTypes] - currency: Optional[str] + question: QuestionSchemaType, + ) -> dict: + decimal_places = self.schema.get_decimal_limit( + calculation["answers_to_calculate"] + ) + currency = question.get("currency") if "value" in calculation: - calculation_value = calculation["value"] - currency = question.get("currency") - return calculation_value, currency + if isinstance(calculation["value"], dict): + target_total = self.value_source_resolver.resolve(calculation["value"]) + else: + target_total = calculation["value"] + else: + target_answer = self.schema.get_answers_by_answer_id( + calculation["answer_id"] + )[0] + target_total = self.data_stores.answer_store.get_answer( + calculation["answer_id"] + ).value # type: ignore # expect not None - target_answer = self.schema.get_answers_by_answer_id(calculation["answer_id"])[ - 0 - ] - calculation_value = self.answer_store.get_answer( - calculation["answer_id"] - ).value # type: ignore # expect not None - currency = target_answer.get("currency") + currency = target_answer.get("currency") - return calculation_value, currency + return { + "currency": currency, + "target_total": target_total, + "decimal_places": decimal_places, + } def validate_date_range_with_period_limits_and_single_date_limits( self, - question_id: Union[str, Sequence[Mapping]], + question_id: str | Sequence[Mapping], period_limits: PeriodLimits, period_range: timedelta, ) -> None: @@ -208,7 +227,7 @@ def validate_date_range_with_period_limits_and_single_date_limits( if period_min and period_range < self._get_offset_value(period_min): exception = f"The schema has invalid period_limits for {question_id}" - raise Exception(exception) + raise ValueError(exception) @staticmethod def validate_date_range_with_single_date_limits( @@ -219,7 +238,7 @@ def validate_date_range_with_single_date_limits( exception = f"The schema has invalid date answer limits for {question_id}" if period_range < timedelta(0): - raise Exception(exception) + raise ValueError(exception) def _validate_date_range_question( self, @@ -227,7 +246,7 @@ def _validate_date_range_question( period_from_id: str, period_to_id: str, messages: Mapping[str, str], - period_limits: Optional[PeriodLimits], + period_limits: PeriodLimits | None, ) -> bool: period_from = getattr(self, period_from_id) period_to = getattr(self, period_to_id) @@ -249,9 +268,10 @@ def _validate_date_range_question( def _validate_calculated_question( self, calculation: Calculation, - question: QuestionSchema, + question: QuestionSchemaType, target_total: Any, - currency: Optional[str], + currency: str | None, + decimal_places: int | None, ) -> bool: messages = None if "validation" in question: @@ -273,7 +293,13 @@ def _validate_calculated_question( # Validate grouped answers meet calculation_type criteria try: - validator(self, calculation["conditions"], calculation_total, target_total) + validator( + self, + conditions=calculation["conditions"], + total=calculation_total, + target_total=target_total, + decimal_limit=decimal_places, + ) except validators.ValidationError as e: self.question_errors[question["id"]] = str(e) return False @@ -285,13 +311,9 @@ def _get_period_range_for_single_date( date_from: Mapping[str, dict], date_to: Mapping[str, dict], ) -> timedelta: - list_item_id = self.location.list_item_id if self.location else None value_source_resolver = ValueSourceResolver( - answer_store=self.answer_store, - list_store=self.list_store, - metadata=self.metadata, - response_metadata=self.response_metadata, + data_stores=self.data_stores, schema=self.schema, location=self.location, list_item_id=list_item_id, @@ -299,11 +321,8 @@ def _get_period_range_for_single_date( ) rule_evaluator = RuleEvaluator( + data_stores=self.data_stores, schema=self.schema, - answer_store=self.answer_store, - list_store=self.list_store, - metadata=self.metadata, - response_metadata=self.response_metadata, location=self.location, ) @@ -320,7 +339,7 @@ def _get_period_range_for_single_date( ) if not min_period_date or not max_period_date: - raise ValueError("Period range must have a start and end date") + raise ValueError(QuestionnaireForm.PERIOD_RANGE_ERROR_MESSAGE) # Work out the largest possible range, for date range question period_range = max_period_date - min_period_date @@ -353,8 +372,8 @@ def _get_offset_value(period_object: Mapping[str, int]) -> timedelta: @staticmethod def _get_period_limits( - limits: Optional[PeriodLimits], - ) -> tuple[Optional[dict[str, Any]], Optional[dict[str, Any]]]: + limits: PeriodLimits | None, + ) -> tuple[dict[str, Any] | None, dict[str, Any] | None]: minimum, maximum = None, None if limits: if "minimum" in limits: @@ -364,8 +383,23 @@ def _get_period_limits( return minimum, maximum def _get_formatted_calculation_values( - self, answers_list: Sequence[str] + self, answers_sequence: Sequence[str] ) -> list[str]: + answers_list: list[str] = [] + block_id = self.location.block_id if self.location else None + if block_id and block_id in self.schema.dynamic_answers_parent_block_ids: + list_name = self.schema.get_list_name_for_dynamic_answer(block_id) + list_item_ids = self.data_stores.list_store[list_name] + for answer_id in answers_sequence: + if self.schema.is_answer_dynamic(answer_id): + answers_list.extend( + f"{answer_id}-{list_item_id}" for list_item_id in list_item_ids + ) + else: + answers_list.append(answer_id) + else: + answers_list = list(answers_sequence) + return [ self.get_data(answer_id).replace(" ", "").replace(",", "") for answer_id in answers_list @@ -373,14 +407,12 @@ def _get_formatted_calculation_values( @staticmethod def _get_calculation_total( - calculation_type: Callable, values: Sequence[Union[float, int, Decimal, str]] + calculation_type: Callable, values: Sequence[float | int | Decimal | str] ) -> Decimal: result: Decimal = calculation_type(Decimal(value or 0) for value in values) return result def answers_all_valid(self, answer_id_list: Sequence[str]) -> bool: - # pylint: disable=no-member - # wtforms Form parents are not discoverable in the 2.3.3 implementation return not set(answer_id_list) & set(self.errors) def map_errors(self) -> list[tuple[str, str]]: @@ -393,8 +425,6 @@ def map_errors(self) -> list[tuple[str, str]]: self.question_errors[self.question["id"]], ) ] - # pylint: disable=no-member - # wtforms Form parents are not discoverable in the 2.3.3 implementation for answer in self.question["answers"]: if answer["id"] in self.errors: ordered_errors += map_subfield_errors(self.errors, answer["id"]) @@ -419,7 +449,7 @@ def get_data(self, answer_id: str) -> str: def _option_value_in_data( answer: Mapping[str, str], option: Mapping[str, Any], - data: Union[MultiDict[str, Any], Mapping[str, Any]], + data: MultiDict[str, Any] | Mapping[str, Any], ) -> bool: data_to_inspect = data.to_dict(flat=False) if isinstance(data, MultiDict) else data @@ -427,39 +457,58 @@ def _option_value_in_data( def get_answer_fields( - question: QuestionSchema, - data: Union[None, MultiDict[str, Any], Mapping[str, Any]], + question: QuestionSchemaType, + data: MultiDict[str, Any] | Mapping[str, Any] | None, schema: QuestionnaireSchema, - answer_store: AnswerStore, - list_store: ListStore, - metadata: Mapping[str, Any], - response_metadata: Mapping[str, Any], - location: Union[Location, RelationshipLocation, None], + data_stores: DataStores, + location: LocationType | None, ) -> dict[str, FieldHandler]: list_item_id = location.list_item_id if location else None - value_source_resolver = ValueSourceResolver( - answer_store=answer_store, - list_store=list_store, - metadata=metadata, - schema=schema, - location=location, - list_item_id=list_item_id, - escape_answer_values=False, - response_metadata=response_metadata, - ) + + block_ids_by_section: dict[SectionKey, tuple[str, ...]] = {} + + if location and data_stores.progress_store: + block_ids_by_section = ( + get_routing_path_block_ids_by_section_for_calculation_summary_dependencies( + location=location, + progress_store=data_stores.progress_store, + path_finder=PathFinder(schema=schema, data_stores=data_stores), + data=question, + ignore_keys=["when"], + schema=schema, + ) + ) + block_ids = None + if block_ids_by_section: + block_ids = get_flattened_mapping_values(block_ids_by_section) + + def _get_value_source_resolver(list_item: str | None = None) -> ValueSourceResolver: + return ValueSourceResolver( + data_stores=data_stores, + schema=schema, + location=location, + list_item_id=list_item, + escape_answer_values=False, + routing_path_block_ids=block_ids, + assess_routing_path=False, + ) rule_evaluator = RuleEvaluator( schema=schema, - answer_store=answer_store, - list_store=list_store, - metadata=metadata, - response_metadata=response_metadata, + data_stores=data_stores, location=location, ) answer_fields = {} question_title = question.get("title") + + value_source_resolved_for_location = _get_value_source_resolver(list_item_id) for answer in question.get("answers", []): + if "list_item_id" in answer: + value_source_resolver = _get_value_source_resolver(answer["list_item_id"]) + else: + value_source_resolver = value_source_resolved_for_location + for option in answer.get("options", []): if "detail_answer" in option: if data: @@ -526,32 +575,33 @@ def _get_error_id(id_: str) -> str: def _clear_detail_answer_field( - form_data: MultiDict, question_schema: QuestionSchema + form_data: ImmutableMultiDict | MultiDict, question_schema: QuestionSchemaType ) -> MultiDict[str, Any]: """ Clears the detail answer field if the parent option is not selected """ - for answer in question_schema["answers"]: + mutable_form_data = ( + MultiDict(form_data) if isinstance(form_data, ImmutableMultiDict) else form_data + ) + + for answer in question_schema.get("answers", []): for option in answer.get("options", []): if "detail_answer" in option and option["value"] not in form_data.getlist( answer["id"] ): - if isinstance(form_data, ImmutableMultiDict): - form_data = MultiDict(form_data) - form_data[option["detail_answer"]["id"]] = "" - return form_data + mutable_form_data[option["detail_answer"]["id"]] = "" + + return mutable_form_data def generate_form( + *, schema: QuestionnaireSchema, - question_schema: QuestionSchema, - answer_store: AnswerStore, - list_store: ListStore, - metadata: Mapping[str, Any], - response_metadata: Mapping[str, Any], - location: Union[None, Location, RelationshipLocation] = None, - data: Optional[dict[str, Any]] = None, - form_data: Optional[MultiDict[str, Any]] = None, + question_schema: QuestionSchemaType, + data_stores: DataStores, + location: LocationType | None = None, + data: dict[str, Any] | None = None, + form_data: MultiDict | None = None, ) -> QuestionnaireForm: class DynamicForm(QuestionnaireForm): pass @@ -564,10 +614,7 @@ class DynamicForm(QuestionnaireForm): question_schema, input_data, schema, - answer_store, - list_store, - metadata, - response_metadata, + data_stores, location, ) @@ -577,10 +624,7 @@ class DynamicForm(QuestionnaireForm): return DynamicForm( schema, question_schema, - answer_store, - list_store, - metadata, - response_metadata, + data_stores, location, data=data, formdata=form_data, diff --git a/app/forms/validators.py b/app/forms/validators.py index c1e591c0fe..51ca7c4d80 100644 --- a/app/forms/validators.py +++ b/app/forms/validators.py @@ -1,9 +1,10 @@ from __future__ import annotations +import math import re from datetime import datetime, timezone from decimal import Decimal, InvalidOperation -from typing import TYPE_CHECKING, Iterable, List, Mapping, Optional, Sequence, Union +from typing import TYPE_CHECKING, Iterable, Mapping, Sequence import flask_babel from babel import numbers @@ -19,9 +20,14 @@ DecimalFieldWithSeparator, IntegerFieldWithSeparator, ) -from app.jinja_filters import format_number, get_formatted_currency +from app.helpers.form_helpers import ( + format_message_with_title, + format_playback_value, + sanitise_mobile_number, + sanitise_number, +) +from app.questionnaire.questionnaire_store_updater import QuestionnaireStoreUpdater from app.questionnaire.rules.utils import parse_datetime -from app.utilities import safe_content if TYPE_CHECKING: from app.forms.questionnaire_form import QuestionnaireForm # pragma: no cover @@ -33,30 +39,27 @@ ) email_regex = re.compile(r"^.+@([^.@][^@\s]+)$") -OptionalMessage = Optional[Mapping[str, str]] -NumType = Union[int, Decimal] +OptionalMessage = Mapping[str, str] | None +NumType = int | Decimal PeriodType = Mapping[str, int] class NumberCheck: - def __init__(self, message: Optional[str] = None): + def __init__(self, message: str | None = None): self.message = message or error_messages["INVALID_NUMBER"] def __call__( self, form: FlaskForm, - field: Union[DecimalFieldWithSeparator, IntegerFieldWithSeparator], + field: DecimalFieldWithSeparator | IntegerFieldWithSeparator, ) -> None: try: - Decimal( - field.raw_data[0].replace( - numbers.get_group_symbol(flask_babel.get_locale()), "" - ) - ) + # number is sanitised to guard against inputs like `,NaN_` etc + number = Decimal(sanitise_number(number=field.raw_data[0])) except (ValueError, TypeError, InvalidOperation, AttributeError) as exc: raise validators.StopValidation(self.message) from exc - if "e" in field.raw_data[0].lower(): + if "e" in field.raw_data[0].lower() or math.isnan(number): raise validators.StopValidation(self.message) @@ -69,7 +72,7 @@ class ResponseRequired: an option for DataRequired or InputRequired validators in wtforms. """ - field_flags = ("required",) + field_flags = {"required": True} def __init__(self, message: str, strip_whitespace: bool = True): self.message = message @@ -105,12 +108,12 @@ class NumberRange: def __init__( self, - minimum: Optional[NumType] = None, + minimum: NumType | None = None, minimum_exclusive: bool = False, - maximum: Optional[NumType] = None, + maximum: NumType | None = None, maximum_exclusive: bool = False, messages: OptionalMessage = None, - currency: Optional[str] = None, + currency: str | None = None, ): self.minimum = minimum self.maximum = maximum @@ -122,41 +125,56 @@ def __init__( def __call__( self, form: "QuestionnaireForm", - field: Union[DecimalFieldWithSeparator, IntegerFieldWithSeparator], + field: DecimalFieldWithSeparator | IntegerFieldWithSeparator, ) -> None: - value: Union[int, Decimal] = field.data + value: int | Decimal | None = field.data + if value is not None: - error_message = self.validate_minimum(value) or self.validate_maximum(value) - if error_message: + decimal_limit = ( + field.places if isinstance(field, DecimalFieldWithSeparator) else None + ) + + if error_message := self.validate_minimum( + value=value, decimal_limit=decimal_limit + ) or self.validate_maximum(value=value, decimal_limit=decimal_limit): raise validators.ValidationError(error_message) - def validate_minimum(self, value: NumType) -> Optional[str]: + def validate_minimum( + self, *, value: NumType, decimal_limit: int | None + ) -> str | None: if self.minimum is None: return None + minimum_value = format_playback_value( + value=self.minimum, + currency=self.currency, + decimal_limit=decimal_limit, + ) + if self.minimum_exclusive and value <= self.minimum: - return self.messages["NUMBER_TOO_SMALL_EXCLUSIVE"] % dict( - min=format_playback_value(self.minimum, self.currency) - ) + return self.messages["NUMBER_TOO_SMALL_EXCLUSIVE"] % {"min": minimum_value} + if value < self.minimum: - return self.messages["NUMBER_TOO_SMALL"] % dict( - min=format_playback_value(self.minimum, self.currency) - ) + return self.messages["NUMBER_TOO_SMALL"] % {"min": minimum_value} return None - def validate_maximum(self, value: NumType) -> Optional[str]: + def validate_maximum( + self, *, value: NumType, decimal_limit: int | None + ) -> str | None: if self.maximum is None: return None + maximum_value = format_playback_value( + value=self.maximum, + currency=self.currency, + decimal_limit=decimal_limit, + ) + if self.maximum_exclusive and value >= self.maximum: - return self.messages["NUMBER_TOO_LARGE_EXCLUSIVE"] % dict( - max=format_playback_value(self.maximum, self.currency) - ) + return self.messages["NUMBER_TOO_LARGE_EXCLUSIVE"] % {"max": maximum_value} if value > self.maximum: - return self.messages["NUMBER_TOO_LARGE"] % dict( - max=format_playback_value(self.maximum, self.currency) - ) + return self.messages["NUMBER_TOO_LARGE"] % {"max": maximum_value} return None @@ -177,18 +195,14 @@ def __init__(self, max_decimals: int = 0, messages: OptionalMessage = None): def __call__( self, form: "QuestionnaireForm", field: DecimalFieldWithSeparator ) -> None: - data = ( - field.raw_data[0] - .replace(numbers.get_group_symbol(flask_babel.get_locale()), "") - .replace(" ", "") - ) + data = sanitise_number(field.raw_data[0]) decimal_symbol = numbers.get_decimal_symbol(flask_babel.get_locale()) if data and decimal_symbol in data: if self.max_decimals == 0: raise validators.ValidationError(self.messages["INVALID_INTEGER"]) if len(data.split(decimal_symbol)[1]) > self.max_decimals: raise validators.ValidationError( - self.messages["INVALID_DECIMAL"] % dict(max=self.max_decimals) + self.messages["INVALID_DECIMAL"] % {"max": self.max_decimals} ) @@ -198,7 +212,7 @@ class OptionalForm: Will not stop the validation chain if any one of the fields is populated. """ - field_flags = ("optional",) + field_flags = {"optional": True} def __call__(self, form: Sequence["QuestionnaireForm"], field: Field) -> None: empty_form = True @@ -224,9 +238,9 @@ def __call__(self, form: Sequence["QuestionnaireForm"], field: Field) -> None: class DateRequired: - field_flags = ("required",) + field_flags = {"required": True} - def __init__(self, message: Optional[str] = None): + def __init__(self, message: str | None = None): self.message = message or error_messages["MANDATORY_DATE"] def __call__(self, form: "QuestionnaireForm", field: DateField) -> None: @@ -245,13 +259,16 @@ def __call__(self, form: "QuestionnaireForm", field: DateField) -> None: class DateCheck: - def __init__(self, message: Optional[str] = None): + def __init__(self, message: str | None = None): self.message = message or error_messages["INVALID_DATE"] def __call__(self, form: "QuestionnaireForm", field: StringField) -> None: if not form.data: raise validators.StopValidation(self.message) + if hasattr(form, "year") and len(form["year"].data) < 4: + raise validators.StopValidation(error_messages["INVALID_YEAR_FORMAT"]) + try: if hasattr(form, "day"): datetime.strptime(form.data, "%Y-%m-%d").replace(tzinfo=timezone.utc) @@ -268,8 +285,8 @@ def __init__( self, messages: OptionalMessage = None, date_format: str = "d MMMM yyyy", - minimum_date: Optional[datetime] = None, - maximum_date: Optional[datetime] = None, + minimum_date: datetime | None = None, + maximum_date: datetime | None = None, ): self.messages = {**error_messages, **(messages or {})} self.minimum_date = minimum_date @@ -282,21 +299,21 @@ def __call__(self, form: "QuestionnaireForm", field: StringField) -> None: if self.minimum_date and date and date < self.minimum_date: raise validators.ValidationError( self.messages["SINGLE_DATE_PERIOD_TOO_EARLY"] - % dict( - min=self._format_playback_date( + % { + "min": self._format_playback_date( self.minimum_date + relativedelta(days=-1), self.date_format ) - ) + } ) if self.maximum_date and date and date > self.maximum_date: raise validators.ValidationError( self.messages["SINGLE_DATE_PERIOD_TOO_LATE"] - % dict( - max=self._format_playback_date( + % { + "max": self._format_playback_date( self.maximum_date + relativedelta(days=+1), self.date_format ) - ) + } ) @staticmethod @@ -309,8 +326,8 @@ class DateRangeCheck: def __init__( self, messages: OptionalMessage = None, - period_min: Optional[dict[str, int]] = None, - period_max: Optional[dict[str, int]] = None, + period_min: dict[str, int] | None = None, + period_max: dict[str, int] | None = None, ): self.messages = {**error_messages, **(messages or {})} self.period_min = period_min @@ -335,7 +352,7 @@ def __call__( ): raise validators.ValidationError( self.messages["DATE_PERIOD_TOO_SMALL"] - % dict(min=self._build_range_length_error(self.period_min)) + % {"min": self._build_range_length_error(self.period_min)} ) if self.period_max: @@ -345,7 +362,7 @@ def __call__( ): raise validators.ValidationError( self.messages["DATE_PERIOD_TOO_LARGE"] - % dict(max=self._build_range_length_error(self.period_max)) + % {"max": self._build_range_length_error(self.period_max)} ) @staticmethod @@ -360,7 +377,9 @@ def _return_relative_delta(period_object: PeriodType) -> relativedelta: def _is_first_relative_delta_largest( relativedelta1: relativedelta, relativedelta2: relativedelta ) -> bool: - epoch = datetime.min # generic epoch for comparison purposes only + epoch = datetime.min.replace( + tzinfo=timezone.utc + ) # generic epoch for comparison purposes only date1 = epoch + relativedelta1 date2 = epoch + relativedelta2 return date1 > date2 @@ -391,27 +410,25 @@ def _build_range_length_error(period_object: PeriodType) -> str: class SumCheck: - def __init__( - self, messages: OptionalMessage = None, currency: Optional[str] = None - ): + MULTIPLE_CONDITION_ERROR_MESSAGE = "There are multiple conditions, but equals is not one of them. We only support <= and >=" + + def __init__(self, messages: OptionalMessage = None, currency: str | None = None): self.messages = {**error_messages, **(messages or {})} self.currency = currency def __call__( self, form: QuestionnaireForm, - conditions: List[str], - total: Union[Decimal, int], - target_total: Union[Decimal, float], + conditions: list[str], + total: Decimal | int, + target_total: Decimal | float | int, + decimal_limit: int | None = None, ) -> None: if len(conditions) > 1: try: conditions.remove("equals") except ValueError as exc: - raise Exception( - "There are multiple conditions, but equals is not one of them. " - "We only support <= and >=" - ) from exc + raise ValueError(SumCheck.MULTIPLE_CONDITION_ERROR_MESSAGE) from exc condition = f"{conditions[0]} or equals" else: @@ -420,16 +437,27 @@ def __call__( is_valid, message = self._is_valid(condition, total, target_total) if not is_valid: + decimal_limit = decimal_limit or ( + None + if isinstance(target_total, int) + else str(target_total)[::-1].find(".") + ) raise validators.ValidationError( self.messages[message] - % dict(total=format_playback_value(target_total, self.currency)) + % { + "total": format_playback_value( + value=target_total, + currency=self.currency, + decimal_limit=decimal_limit, + ) + } ) @staticmethod def _is_valid( condition: str, - total: Union[Decimal, float], - target_total: Union[Decimal, float], + total: Decimal | float, + target_total: Decimal | float, ) -> tuple[bool, str]: if condition == "equals": return total == target_total, "TOTAL_SUM_NOT_EQUALS" @@ -442,19 +470,10 @@ def _is_valid( if condition == "less than or equals": return total <= target_total, "TOTAL_SUM_NOT_LESS_THAN_OR_EQUALS" - -def format_playback_value( - value: Union[float, Decimal], currency: Optional[str] = None -) -> str: - if currency: - return get_formatted_currency(value, currency) - - formatted_number: str = format_number(value) - return formatted_number - - -def format_message_with_title(error_message: str, question_title: str) -> str: - return error_message % {"question_title": safe_content(question_title)} + unimplemented_condition_error_message = ( + f"Condition '{condition}' is not implemented" + ) + raise NotImplementedError(unimplemented_condition_error_message) class MutuallyExclusiveCheck: @@ -463,26 +482,30 @@ def __init__(self, question_title: str, messages: OptionalMessage = None): self.question_title = question_title def __call__( - self, answer_values: Iterable, is_mandatory: bool, is_only_checkboxes: bool + self, + answer_values: Iterable, + is_mandatory: bool, + is_only_checkboxes_or_radios: bool, ) -> None: - total_answered = sum(1 for value in answer_values if value) + total_answered = sum( + value not in QuestionnaireStoreUpdater.EMPTY_ANSWER_VALUES + for value in answer_values + ) + if total_answered > 1: raise validators.ValidationError(self.messages["MUTUALLY_EXCLUSIVE"]) if is_mandatory and total_answered < 1: message = format_message_with_title( - self.messages["MANDATORY_CHECKBOX"] - if is_only_checkboxes - else self.messages["MANDATORY_QUESTION"], + ( + self.messages["MANDATORY_CHECKBOX"] + if is_only_checkboxes_or_radios + else self.messages["MANDATORY_QUESTION"] + ), self.question_title, ) raise validators.ValidationError(message) -def sanitise_mobile_number(data: str) -> str: - data = re.sub(r"[\s.,\t\-{}\[\]()/]", "", data) - return re.sub(r"^(0{1,2}44|\+44|0)", "", data) - - class MobileNumberCheck: def __init__(self, message: OptionalMessage = None): self.message = message or error_messages["INVALID_MOBILE_NUMBER"] @@ -495,7 +518,7 @@ def __call__(self, form: "QuestionnaireForm", field: StringField) -> None: class EmailTLDCheck: - def __init__(self, message: Optional[str] = None): + def __init__(self, message: str | None = None): self.message = message or error_messages["INVALID_EMAIL_FORMAT"] def __call__(self, form: "QuestionnaireForm", field: StringField) -> None: diff --git a/app/globals.py b/app/globals.py index a5f0f5732c..1a297fde95 100644 --- a/app/globals.py +++ b/app/globals.py @@ -1,5 +1,4 @@ from datetime import datetime, timedelta, timezone -from typing import Any, Mapping, Optional, Union from flask import current_app, g from flask import session as cookie_session @@ -7,7 +6,7 @@ from app.authentication.user import User from app.data_models import QuestionnaireStore -from app.data_models.answer_store import AnswerStore +from app.data_models.metadata_proxy import MetadataProxy from app.data_models.session_data import SessionData from app.data_models.session_store import SessionStore from app.questionnaire import QuestionnaireSchema @@ -26,13 +25,12 @@ def get_questionnaire_store(user_id: str, user_ik: str) -> QuestionnaireStore: "EQ_SERVER_SIDE_STORAGE_ENCRYPTION_USER_PEPPER" ) storage = EncryptedQuestionnaireStorage(user_id, user_ik, pepper) - # pylint: disable=assigning-non-slot store = g._questionnaire_store = QuestionnaireStore(storage) return store -def get_session_store() -> Optional[SessionStore]: +def get_session_store() -> SessionStore | None: if USER_IK not in cookie_session or EQ_SESSION_ID not in cookie_session: return None @@ -44,7 +42,6 @@ def get_session_store() -> Optional[SessionStore]: pepper = secret_store.get_secret_by_name( "EQ_SERVER_SIDE_STORAGE_ENCRYPTION_USER_PEPPER" ) - # pylint: disable=assigning-non-slot store = g._session_store = SessionStore( cookie_session[USER_IK], pepper, cookie_session[EQ_SESSION_ID] ) @@ -83,7 +80,7 @@ def create_session_store( seconds=session_timeout_in_seconds ) - # pylint: disable=protected-access, assigning-non-slot + # pylint: disable=protected-access g._session_store = ( SessionStore(user_ik, pepper) .create(eq_session_id, user_id, session_data, expires_at) @@ -91,18 +88,13 @@ def create_session_store( ) -def get_metadata(user: User) -> Union[None, Mapping[str, Any]]: +def get_metadata(user: User) -> MetadataProxy | None: if user.is_anonymous: logger.debug("anonymous user requesting metadata get instance") return None questionnaire_store = get_questionnaire_store(user.user_id, user.user_ik) - return questionnaire_store.metadata - - -def get_answer_store(user: User) -> AnswerStore: - questionnaire_store = get_questionnaire_store(user.user_id, user.user_ik) - return questionnaire_store.answer_store + return questionnaire_store.data_stores.metadata def get_view_submitted_response_expiration_time(submitted_at: datetime) -> datetime: diff --git a/app/helpers/__init__.py b/app/helpers/__init__.py index 0452bc1b3b..e5b58295ec 100644 --- a/app/helpers/__init__.py +++ b/app/helpers/__init__.py @@ -1,9 +1,9 @@ -from .address_lookup_api_helper import get_address_lookup_api_auth_token -from .header_helpers import get_span_and_trace -from .url_safe_serializer import url_safe_serializer +from app.helpers.address_lookup_api_helper import get_address_lookup_api_auth_token +from app.helpers.header_helpers import get_span_and_trace +from app.helpers.url_safe_serializer import url_safe_serializer __all__ = [ + "get_address_lookup_api_auth_token", "get_span_and_trace", "url_safe_serializer", - "get_address_lookup_api_auth_token", ] diff --git a/app/helpers/address_lookup_api_helper.py b/app/helpers/address_lookup_api_helper.py index 41103a1083..98f72a488d 100644 --- a/app/helpers/address_lookup_api_helper.py +++ b/app/helpers/address_lookup_api_helper.py @@ -10,7 +10,7 @@ def get_jwk_from_secret(secret: str) -> jwk.JWK: return jwk.JWK(kty="oct", k=base64url_encode(secret.encode("utf-8"))) -def get_address_lookup_api_auth_token() -> str: +def get_address_lookup_api_auth_token() -> str | None: if current_app.config["ADDRESS_LOOKUP_API_AUTH_ENABLED"]: secret = current_app.eq["secret_store"].get_secret_by_name( # type: ignore "ADDRESS_LOOKUP_API_AUTH_TOKEN_SECRET" diff --git a/app/helpers/form_helpers.py b/app/helpers/form_helpers.py new file mode 100644 index 0000000000..ffde8e3359 --- /dev/null +++ b/app/helpers/form_helpers.py @@ -0,0 +1,44 @@ +import re +from decimal import Decimal + +import flask_babel +from babel import numbers + +from app.jinja_filters import format_number +from app.utilities import safe_content +from app.utilities.decimal_places import get_formatted_currency + + +def sanitise_number(number: str) -> str: + return ( + number.replace(numbers.get_group_symbol(flask_babel.get_locale()), "") + .replace("_", "") + .replace(" ", "") + ) + + +def sanitise_mobile_number(data: str) -> str: + data = re.sub(r"[\s.,\t\-{}\[\]()/]", "", data) + return re.sub(r"^(0{1,2}44|\+44|0)", "", data) + + +def format_playback_value( + value: float | Decimal, + currency: str | None = None, + decimal_limit: int | None = None, +) -> str: + """ + For playback of values we set the decimal limit based on the number of decimal places + in the given value rather than any limit that has been set in the schema as we do for other methods + """ + if currency: + return get_formatted_currency( + value=value, currency=currency, decimal_limit=decimal_limit + ) + + formatted_number: str = format_number(value) + return formatted_number + + +def format_message_with_title(error_message: str, question_title: str) -> str: + return error_message % {"question_title": safe_content(question_title)} diff --git a/app/helpers/header_helpers.py b/app/helpers/header_helpers.py index a4aeb8654e..1525a6b901 100644 --- a/app/helpers/header_helpers.py +++ b/app/helpers/header_helpers.py @@ -1,11 +1,9 @@ -from typing import Union - from werkzeug.datastructures import EnvironHeaders def get_span_and_trace( headers: EnvironHeaders, -) -> Union[tuple[None, None], tuple[str, str]]: +) -> tuple[None, None] | tuple[str, str]: try: trace, span = headers.get("X-Cloud-Trace-Context").split("/") # type: ignore except (ValueError, AttributeError): diff --git a/app/helpers/language_helper.py b/app/helpers/language_helper.py index 0d5ab20947..0d08b9d460 100644 --- a/app/helpers/language_helper.py +++ b/app/helpers/language_helper.py @@ -1,11 +1,13 @@ -from typing import Optional, Union from urllib.parse import urlencode from flask import g, request +from flask import session as cookie_session +from flask_login import current_user -from app.globals import get_session_store +from app.data_models.metadata_proxy import MetadataProxy +from app.globals import get_metadata, get_session_store from app.questionnaire.questionnaire_schema import DEFAULT_LANGUAGE_CODE -from app.utilities.schema import get_allowed_languages +from app.utilities.schema import get_allowed_languages, load_schema_from_metadata LANGUAGE_TEXT = { "en": "English", @@ -13,33 +15,50 @@ } -def handle_language() -> None: +def handle_language(metadata: MetadataProxy | None = None) -> None: session_store = get_session_store() - if session_store and (session_data := session_store.session_data): - launch_language = session_data.launch_language_code or DEFAULT_LANGUAGE_CODE - # pylint: disable=assigning-non-slot - g.allowed_languages = get_allowed_languages( - session_data.schema_name, launch_language - ) + + if session_store and session_store.session_data: + if not metadata: + metadata = get_metadata(current_user) + + schema_name = metadata.schema_name if metadata else None + language_code = metadata.language_code if metadata else None + + launch_language = language_code or DEFAULT_LANGUAGE_CODE + g.allowed_languages = get_allowed_languages(schema_name, launch_language) request_language = request.args.get("language_code") if request_language and request_language in g.allowed_languages: + if metadata: + schema = load_schema_from_metadata( + metadata=metadata, language_code=request_language + ) + if schema.json["title"] != cookie_session.get("title"): + cookie_session["title"] = schema.json["title"] + + cookie_session["language_code"] = request_language session_store.session_data.language_code = request_language session_store.save() -def get_languages_context(current_language: str) -> Optional[dict[str, list[dict]]]: - context = [] +def get_languages_context(current_language: str) -> dict[str, list[dict]] | None: allowed_languages = g.get("allowed_languages") if allowed_languages and len(allowed_languages) > 1: - for language in allowed_languages: - context.append(_get_language_context(language, current_language)) + context: list[dict[str, str | bool]] = [ + _get_language_context(language, current_language) + for language in allowed_languages + ] return {"languages": context} + + if (language := cookie_session.get("language_code")) and language in LANGUAGE_TEXT: + return {"languages": [_get_language_context(language, language)]} + return None def _get_language_context( language_code: str, current_language: str -) -> dict[str, Union[str, bool]]: +) -> dict[str, str | bool]: return { "ISOCode": language_code, "url": _get_query_string_with_language(language_code), diff --git a/app/helpers/metadata_helpers.py b/app/helpers/metadata_helpers.py new file mode 100644 index 0000000000..e4eba3bcb7 --- /dev/null +++ b/app/helpers/metadata_helpers.py @@ -0,0 +1,3 @@ +def get_ru_ref_without_check_letter(ru_ref: str) -> str: + """Return the ru_ref without the check-letter""" + return ru_ref[:11] diff --git a/app/helpers/schema_helpers.py b/app/helpers/schema_helpers.py index 1680b6c8f7..2da547e55e 100644 --- a/app/helpers/schema_helpers.py +++ b/app/helpers/schema_helpers.py @@ -1,13 +1,20 @@ from functools import wraps -from typing import Any, Callable +from typing import Callable, Concatenate, ParamSpec, TypeVar +from flask_login import current_user from werkzeug.exceptions import Unauthorized -from app.globals import get_session_store -from app.utilities.schema import load_schema_from_session_data +from app.globals import get_metadata, get_session_store +from app.questionnaire.questionnaire_schema import QuestionnaireSchema +from app.utilities.schema import load_schema_from_metadata +T = TypeVar("T") +P = ParamSpec("P") -def with_schema(function: Callable) -> Any: + +def with_schema( + function: Callable[Concatenate[QuestionnaireSchema, P], T], +) -> Callable[P, T]: """Adds the survey schema as the first argument to the function being wrapped. Use on flask request handlers or methods called by flask request handlers. @@ -22,13 +29,20 @@ def get_block(routing_path, schema, *args): """ @wraps(function) - def wrapped_function(*args: Any, **kwargs: Any) -> Any: + def wrapped_function(*args: P.args, **kwargs: P.kwargs) -> T: session_store = get_session_store() - if not session_store: + if ( + not session_store + or not session_store.session_data + or not (metadata := get_metadata(current_user)) + ): raise Unauthorized - session_data = session_store.session_data - schema = load_schema_from_session_data(session_data) + language_code = session_store.session_data.language_code + + schema = load_schema_from_metadata( + metadata=metadata, language_code=language_code + ) return function(schema, *args, **kwargs) return wrapped_function diff --git a/app/helpers/template_helpers.py b/app/helpers/template_helpers.py index f189d61876..4488cf8b46 100644 --- a/app/helpers/template_helpers.py +++ b/app/helpers/template_helpers.py @@ -1,25 +1,37 @@ from functools import cached_property, lru_cache -from typing import Any, Mapping, Optional, Type, Union +from typing import Any from flask import current_app from flask import render_template as flask_render_template from flask import request from flask import session as cookie_session from flask import url_for -from flask_babel import LazyString, get_locale, lazy_gettext +from flask_babel import get_locale, lazy_gettext from flask_login import current_user -from app.globals import get_session_store +from app.globals import get_metadata, get_session_store from app.helpers.language_helper import get_languages_context +from app.questionnaire import QuestionnaireSchema from app.settings import ACCOUNT_SERVICE_BASE_URL from app.survey_config import ( BusinessSurveyConfig, - CensusNISRASurveyConfig, - CensusSurveyConfig, - NorthernIrelandBusinessSurveyConfig, + DBTBusinessSurveyConfig, + DBTDSITBusinessSurveyConfig, + DBTDSITNIBusinessSurveyConfig, + DBTNIBusinessSurveyConfig, + DESNZBusinessSurveyConfig, + DESNZNIBusinessSurveyConfig, + NIBusinessSurveyConfig, + ONSNHSSocialSurveyConfig, + ORRBusinessSurveyConfig, + SocialSurveyConfig, SurveyConfig, - WelshCensusSurveyConfig, + UKHSAONSSocialSurveyConfig, ) +from app.survey_config.survey_type import SurveyType +from app.utilities.schema import load_schema_from_metadata + +DATA_LAYER_KEYS = {"title", "survey_id", "form_type"} class ContextHelper: @@ -34,30 +46,29 @@ def __init__( self._is_post_submission = is_post_submission self._include_csrf_token = include_csrf_token self._survey_config = survey_config - self._survey_title = cookie_session.get( - "survey_title", self._survey_config.survey_title - ) + self._survey_title = cookie_session.get("title", lazy_gettext("ONS Surveys")) self._sign_out_url = url_for("session.get_sign_out") self._cdn_url = ( f'{current_app.config["CDN_URL"]}{current_app.config["CDN_ASSETS_PATH"]}' ) self._address_lookup_api = current_app.config["ADDRESS_LOOKUP_API_URL"] - self._google_tag_manager_id = current_app.config.get("EQ_GOOGLE_TAG_MANAGER_ID") - self._google_tag_manager_auth = current_app.config.get( - "EQ_GOOGLE_TAG_MANAGER_AUTH" + self._google_tag_id = current_app.config.get("EQ_GOOGLE_TAG_ID") + self._survey_type = cookie_session.get("theme") + self._preview_enabled = ( + self._survey_config.schema.preview_enabled + if self._survey_config.schema + else False ) @property def context(self) -> dict[str, Any]: - return { + context = { "sign_out_button_text": self._survey_config.sign_out_button_text, "account_service_my_account_url": self._survey_config.account_service_my_account_url, "account_service_log_out_url": self._survey_config.account_service_log_out_url, "account_service_todo_url": self._survey_config.account_service_todo_url, "contact_us_url": self._survey_config.contact_us_url, "thank_you_url": url_for("post_submission.get_thank_you"), - "cookie_settings_url": self._survey_config.cookie_settings_url, - "page_header": self.page_header_context, "service_links": self.service_links_context, "footer": self.footer_context, "languages": get_languages_context(self._language), @@ -66,41 +77,58 @@ def context(self) -> dict[str, Any]: "survey_title": self._survey_title, "cdn_url": self._cdn_url, "address_lookup_api_url": self._address_lookup_api, - "data_layer": self._survey_config.data_layer, + "data_layer": self.data_layer_context, "include_csrf_token": self._include_csrf_token, - "google_tag_manager_id": self._google_tag_manager_id, - "google_tag_manager_auth": self._google_tag_manager_auth, + "google_tag_id": self._google_tag_id, + "survey_type": self._survey_type, + "preview_enabled": self._preview_enabled, + "masthead_logo": self._survey_config.masthead_logo, + "masthead_logo_mobile": self._survey_config.masthead_logo_mobile, } + if self._survey_type: + context["cookie_settings_url"] = self._survey_config.cookie_settings_url + context["cookie_domain"] = self._survey_config.cookie_domain + + return context + @property - def service_links_context(self) -> Optional[dict[str, list[dict]]]: + def service_links_context( + self, + ) -> dict[str, dict[str, str] | list[dict]] | None: + ru_ref = ( + metadata["ru_ref"] if (metadata := get_metadata(current_user)) else None + ) + if service_links := self._survey_config.get_service_links( sign_out_url=self._sign_out_url, is_authenticated=current_user.is_authenticated, + cookie_has_theme=bool(self._survey_type), + ru_ref=ru_ref, ): - return {"itemsList": service_links} + return { + "toggleServicesButton": { + "text": lazy_gettext("Menu"), + "ariaLabel": "Toggle services menu", + }, + "itemsList": service_links, + } return None @property - def page_header_context(self) -> dict[str, Union[bool, str, LazyString]]: - context: dict[str, Union[bool, str, LazyString]] = { - "logo": f"{self._survey_config.page_header_logo}", - "logoAlt": f"{self._survey_config.page_header_logo_alt}", + def data_layer_context( + self, + ) -> dict[str, str]: + tx_id_context = ( + {"tx_id": metadata.tx_id} + if (metadata := get_metadata(current_user)) + else {} + ) + schema_context = { + key: value for key in DATA_LAYER_KEYS if (value := cookie_session.get(key)) } - - if self._survey_title: - context["title"] = self._survey_title - if self._survey_config.title_logo: - context["titleLogo"] = self._survey_config.title_logo - if self._survey_config.title_logo_alt: - context["titleLogoAlt"] = self._survey_config.title_logo_alt - if self._survey_config.custom_header_logo: - context["customHeaderLogo"] = self._survey_config.custom_header_logo - if self._survey_config.mobile_logo: - context["mobileLogo"] = self._survey_config.mobile_logo - - return context + return tx_id_context | schema_context @property def footer_context(self) -> dict[str, Any]: @@ -117,25 +145,20 @@ def footer_context(self) -> dict[str, Any]: if self._footer_warning: context["footerWarning"] = self._footer_warning - if self._survey_config.footer_links: - context["rows"] = [{"itemsList": self._survey_config.footer_links}] - - if self._survey_config.footer_legal_links: - context["legal"] = [{"itemsList": self._survey_config.footer_legal_links}] + if footer_links := self._survey_config.get_footer_links( + cookie_has_theme=bool(self._survey_type), + ): + context["rows"] = [{"itemsList": footer_links}] - if ( - self._survey_config.powered_by_logo - or self._survey_config.powered_by_logo_alt + if footer_legal_links := self._survey_config.get_footer_legal_links( + cookie_has_theme=bool(self._survey_type), ): - context["poweredBy"] = { - "logo": self._survey_config.powered_by_logo, - "alt": self._survey_config.powered_by_logo_alt, - } + context["legal"] = [{"itemsList": footer_legal_links}] return context @cached_property - def _footer_warning(self) -> Optional[str]: + def _footer_warning(self) -> str | None: if self._is_post_submission: footer_warning: str = lazy_gettext( "Make sure you leave this page or close your browser if using a shared device" @@ -145,47 +168,68 @@ def _footer_warning(self) -> Optional[str]: @lru_cache -def survey_config_mapping(*, theme: str, language: str, base_url: str) -> SurveyConfig: - survey_type_to_config: dict[str, Type[SurveyConfig]] = { - "default": BusinessSurveyConfig, - "business": BusinessSurveyConfig, - "health": SurveyConfig, - "social": SurveyConfig, - "northernireland": NorthernIrelandBusinessSurveyConfig, - "census": (WelshCensusSurveyConfig if language == "cy" else CensusSurveyConfig), - "census-nisra": CensusNISRASurveyConfig, +def survey_config_mapping( + *, theme: SurveyType, language: str, base_url: str, schema: QuestionnaireSchema +) -> SurveyConfig: + survey_type_to_config: dict[SurveyType, type[SurveyConfig]] = { + SurveyType.DEFAULT: BusinessSurveyConfig, + SurveyType.BUSINESS: BusinessSurveyConfig, + SurveyType.HEALTH: SocialSurveyConfig, + SurveyType.SOCIAL: SocialSurveyConfig, + SurveyType.NORTHERN_IRELAND: NIBusinessSurveyConfig, + SurveyType.DBT: DBTBusinessSurveyConfig, + SurveyType.DBT_NI: DBTNIBusinessSurveyConfig, + SurveyType.DBT_DSIT: DBTDSITBusinessSurveyConfig, + SurveyType.DBT_DSIT_NI: DBTDSITNIBusinessSurveyConfig, + SurveyType.DESNZ: DESNZBusinessSurveyConfig, + SurveyType.DESNZ_NI: DESNZNIBusinessSurveyConfig, + SurveyType.ORR: ORRBusinessSurveyConfig, + SurveyType.UKHSA_ONS: UKHSAONSSocialSurveyConfig, + SurveyType.ONS_NHS: ONSNHSSocialSurveyConfig, } return survey_type_to_config[theme]( - base_url=base_url, + base_url=base_url, schema=schema, language_code=language ) def get_survey_config( *, - theme: Optional[str] = None, - language: Optional[str] = None, + base_url: str | None = None, + theme: SurveyType | None = None, + language: str | None = None, + schema: QuestionnaireSchema | None = None, ) -> SurveyConfig: # The fallback to assigning SURVEY_TYPE to theme is only being added until # business feedback on the differentiation between theme and SURVEY_TYPE. language = language or get_locale().language - theme = theme or get_survey_type() - base_url = ( + + if metadata := get_metadata(current_user): + schema = load_schema_from_metadata(metadata=metadata, language_code=language) + + survey_theme = theme or get_survey_type() + + base_url = base_url or ( cookie_session.get("account_service_base_url") or ACCOUNT_SERVICE_BASE_URL ) return survey_config_mapping( - theme=theme, + theme=survey_theme, language=language, base_url=base_url, + schema=schema, ) -def render_template(template: str, **kwargs: Union[str, Mapping]) -> str: +def render_template(template: str, **kwargs: Any) -> str: + session_expires_at = None language = get_locale().language - survey_config = get_survey_config( - language=language, - ) + if session_store := get_session_store(): + if session_expiry := session_store.expiration_time: + session_expires_at = session_expiry.isoformat() + + survey_config = get_survey_config() + is_post_submission = request.blueprint == "post_submission" include_csrf_token = bool( request.url_rule @@ -199,12 +243,6 @@ def render_template(template: str, **kwargs: Union[str, Mapping]) -> str: template = f"{template.lower()}.html" - session_expires_at = ( - session_store.expiration_time.isoformat() - if (session_store := get_session_store()) and session_store.expiration_time - else None - ) - return flask_render_template( template, csp_nonce=request.csp_nonce, # type: ignore @@ -214,5 +252,6 @@ def render_template(template: str, **kwargs: Union[str, Mapping]) -> str: ) -def get_survey_type() -> str: - return cookie_session.get("theme", current_app.config["SURVEY_TYPE"]) +def get_survey_type() -> SurveyType: + survey_type = cookie_session.get("theme", current_app.config["SURVEY_TYPE"]) + return SurveyType(survey_type) diff --git a/app/helpers/url_safe_serializer.py b/app/helpers/url_safe_serializer.py index a8c0634337..7eac6a2deb 100644 --- a/app/helpers/url_safe_serializer.py +++ b/app/helpers/url_safe_serializer.py @@ -6,4 +6,5 @@ def url_safe_serializer() -> URLSafeSerializer: - return URLSafeSerializer(current_app.secret_key, salt=cookie_session[EQ_SESSION_ID]) + # Type Ignore: Secret key is validated on app start up, so it must exist at this point + return URLSafeSerializer(current_app.secret_key, salt=cookie_session[EQ_SESSION_ID]) # type: ignore diff --git a/app/jinja_filters.py b/app/jinja_filters.py index c857f8bcf0..7ccc6e4636 100644 --- a/app/jinja_filters.py +++ b/app/jinja_filters.py @@ -1,92 +1,109 @@ # coding: utf-8 + import re +from datetime import datetime +from decimal import Decimal +from typing import Any, Callable, Literal, Mapping, TypeAlias import flask import flask_babel -from babel import numbers, units -from flask import current_app -from jinja2 import pass_eval_context +from babel import numbers +from flask import current_app, g +from jinja2 import nodes, pass_eval_context from markupsafe import Markup, escape +from wtforms import SelectFieldBase +from app.questionnaire.questionnaire_schema import ( + QuestionnaireSchema, + is_summary_with_calculation, +) from app.questionnaire.rules.utils import parse_datetime from app.settings import MAX_NUMBER +from app.utilities.decimal_places import ( + custom_format_decimal, + custom_format_unit, + get_formatted_currency, +) blueprint = flask.Blueprint("filters", __name__) +FormType = Mapping[str, Mapping[str, Any]] +AnswerType = Mapping[str, Any] +UnitLengthType: TypeAlias = Literal["short", "long", "narrow"] -def mark_safe(context, value): - return Markup(value) if context.autoescape else value +def mark_safe(context: nodes.EvalContext, value: str) -> Markup | str: + return Markup(value) if context.autoescape else value # noqa: S704 -def strip_tags(value): - return escape(Markup(value).striptags()) +def strip_tags(value: str) -> Markup: + return escape(Markup(value).striptags()) # noqa: S704 @blueprint.app_template_filter() -def format_number(value): - if value or value == 0: - return numbers.format_decimal(value, locale=flask_babel.get_locale()) +def format_number(value: int | Decimal | float) -> str: + locale = flask_babel.get_locale() - return "" + formatted_number: str = custom_format_decimal(value, locale) + return formatted_number -def get_formatted_address(address_fields): +def get_formatted_address(address_fields: dict[str, str]) -> str: address_fields.pop("uprn", None) return "
".join(address_field for address_field in address_fields.values()) -def get_formatted_currency(value, currency="GBP") -> str: - if value or value == 0: - return numbers.format_currency( - number=value, currency=currency, locale=flask_babel.get_locale() - ) - - return "" - - @blueprint.app_template_filter() -def get_currency_symbol(currency="GBP"): - return numbers.get_currency_symbol(currency, locale=flask_babel.get_locale()) +def get_currency_symbol(currency: str = "GBP") -> str: + currency_symbol: str = numbers.get_currency_symbol( + currency, locale=flask_babel.get_locale() + ) + return currency_symbol @blueprint.app_template_filter() -def format_percentage(value): +def format_percentage(value: int | float | Decimal) -> str: return f"{value}%" -def format_unit(unit, value, length="short"): - return units.format_unit( +def format_unit( + unit: str, + value: int | float | Decimal, + length: UnitLengthType = "short", +) -> str: + formatted_unit: str = custom_format_unit( value=value, measurement_unit=unit, length=length, locale=flask_babel.get_locale(), ) + return formatted_unit -def format_unit_input_label(unit, unit_length="short"): + +def format_unit_input_label(unit: str, unit_length: UnitLengthType = "short") -> str: """ - This function is used to only get the unit of measurement text. If the unit_length + This function is used to only get the unit of measurement text. If the unit_length is long then only the plural form of the word is returned (e.g., Hours, Years, etc). :param (str) unit unit of measurement :param (str) unit_length length of unit text, can be one of short/long/narrow """ + unit_label: str + if unit_length == "long": - return units.format_unit( - value=2, - measurement_unit=unit, - length=unit_length, - locale=flask_babel.get_locale(), - ).replace("2 ", "") - return units.format_unit( - value="", - measurement_unit=unit, - length=unit_length, - locale=flask_babel.get_locale(), - ).strip() + unit_label = format_unit(value=2, unit=unit, length=unit_length).replace( + "2 ", "" + ) + else: + # Type ignore: We pass an empty string as the value so that we just return the unit label + unit_label = format_unit( + value="", unit=unit, length=unit_length # type: ignore + ).strip() + + return unit_label -def format_duration(value): +def format_duration(value: Mapping[str, int]) -> str: parts = [] if "years" in value and (value["years"] > 0 or len(value) == 1): @@ -104,14 +121,14 @@ def format_duration(value): return " ".join(parts) -def get_format_multilined_string(value): +def get_format_multilined_string(value: str) -> str: escaped_value = escape(value) new_line_regex = r"(?:\r\n|\r|\n)+" value_with_line_break_tag = re.sub(new_line_regex, "
", escaped_value) return f"{value_with_line_break_tag}" -def get_format_date(value): +def get_format_date(value: Markup) -> str: """Format a datetime string. :param (jinja2.nodes.EvalContext) context: Evaluation context. @@ -132,9 +149,9 @@ def get_format_date(value): return f"{date}" -@pass_eval_context # type: ignore +@pass_eval_context @blueprint.app_template_filter() -def format_datetime(context, date_time): +def format_datetime(context: nodes.EvalContext, date_time: datetime) -> str | Markup: # flask babel on formatting will automatically convert based on the time zone specified in setup.py formatted_date = flask_babel.format_date(date_time, format="d MMMM yyyy") formatted_time = flask_babel.format_time(date_time, format="HH:mm") @@ -148,46 +165,53 @@ def format_datetime(context, date_time): return mark_safe(context, result) -def get_format_date_range(start_date, end_date): - return flask_babel.gettext( +def get_format_date_range(start_date: Markup, end_date: Markup) -> Markup: + date_range: Markup + date_range = flask_babel.gettext( "%(from_date)s to %(to_date)s", from_date=get_format_date(start_date), to_date=get_format_date(end_date), ) + return date_range @blueprint.app_context_processor -def format_unit_processor(): - return dict(format_unit=format_unit) +def format_unit_processor() -> dict[ + str, + Callable[[str, int | float | Decimal, UnitLengthType], str], +]: + return {"format_unit": format_unit} @blueprint.app_context_processor -def format_unit_input_label_processor(): - return dict(format_unit_input_label=format_unit_input_label) +def format_unit_input_label_processor() -> dict[str, Callable]: + return {"format_unit_input_label": format_unit_input_label} @blueprint.app_context_processor -def get_currency_symbol_processor(): - return dict(get_currency_symbol=get_currency_symbol) +def get_currency_symbol_processor() -> dict[str, Callable]: + return {"get_currency_symbol": get_currency_symbol} -@blueprint.app_template_filter() # type: ignore -def setAttribute(dictionary, key, value): +@blueprint.app_template_filter() +def setAttribute(dictionary: dict[str, str], key: str, value: str) -> dict[str, str]: dictionary[key] = value return dictionary -@blueprint.app_template_filter() # type: ignore -def setAttributes(dictionary, attributes): +@blueprint.app_template_filter() +def setAttributes( + dictionary: dict[str, str], attributes: dict[str, str] +) -> dict[str, str]: for key in attributes: dictionary[key] = attributes[key] return dictionary @blueprint.app_template_filter() -def should_wrap_with_fieldset(question): +def should_wrap_with_fieldset(question: dict[str, list]) -> bool: # Logic for when to wrap with a fieldset comes from - # https://ons-design-system.netlify.app/patterns/question/ + # https://service-manual.ons.gov.uk/design-system/components/fieldset if question["type"] == "DateRange": return False @@ -212,21 +236,41 @@ def should_wrap_with_fieldset(question): @blueprint.app_context_processor -def should_wrap_with_fieldset_processor(): +def should_wrap_with_fieldset_processor() -> dict[str, Callable]: return {"should_wrap_with_fieldset": should_wrap_with_fieldset} +def get_min_max_value_width( + min_max: Literal["minimum", "maximum"], answer: AnswerType, default_value: int +) -> int: + """ + This function gets the minimum and maximum value accepted for a question. + Which then allows us to use that value to set the width of the textbox to suit that min and max. + + If the min or max for the answer is a value source but not an "answers" source, such as a calculated or grand calculated summary, + use the length of the default value for the min and max width, as the actual min and max width cannot currently be determined + """ + min_max_value = answer.get(min_max, {}) + if min_max_value and isinstance(answer[min_max]["value"], Mapping): + if answer[min_max]["value"].get("source") == "answers": + schema: QuestionnaireSchema = g.get("schema") + identifier = answer[min_max]["value"]["identifier"] + return schema.min_and_max_map[identifier][min_max] + return len(str(default_value)) + + # Factor out the decimals as it's accounted for in get_width_for_number + result = int(min_max_value.get("value", default_value)) + return len(str(result)) + + @blueprint.app_template_filter() -def get_width_for_number(answer): +def get_width_for_number(answer: AnswerType) -> int | None: allowable_widths = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 30, 40, 50] - min_value = answer.get("minimum", {}).get("value", 0) - max_value = answer.get("maximum", {}).get("value", MAX_NUMBER) - - min_value_width = len(str(min_value)) - max_value_width = len(str(max_value)) + min_value_width = get_min_max_value_width("minimum", answer, 0) + max_value_width = get_min_max_value_width("maximum", answer, MAX_NUMBER) - width = min_value_width if min_value_width > max_value_width else max_value_width + width = max(min_value_width, max_value_width) width += answer.get("decimal_places", 0) @@ -236,19 +280,25 @@ def get_width_for_number(answer): @blueprint.app_context_processor -def get_width_for_number_processor(): +def get_width_for_number_processor() -> dict[str, Callable]: return {"get_width_for_number": get_width_for_number} class LabelConfig: - def __init__(self, _for, text, description=None): + def __init__(self, _for: str, text: str, description: str | None = None) -> None: self._for = _for self.text = text self.description = description class SelectConfig: - def __init__(self, option, index, answer, form=None): + def __init__( + self, + option: SelectFieldBase._Option, + index: int, + answer: AnswerType, + form: FormType | None = None, + ) -> None: self.id = option.id self.name = option.name self.value = option.data @@ -275,7 +325,12 @@ def __init__(self, option, index, answer, form=None): class RelationshipRadioConfig(SelectConfig): - def __init__(self, option, index, answer): + def __init__( + self, + option: SelectFieldBase._Option, + index: int, + answer: AnswerType, + ) -> None: super().__init__(option, index, answer) if self._answer_option: @@ -295,7 +350,11 @@ def __init__(self, option, index, answer): class OtherConfig: - def __init__(self, detail_answer_field, detail_answer_schema): + def __init__( + self, + detail_answer_field: SelectFieldBase._Option, + detail_answer_schema: Mapping[str, str], + ) -> None: self.id = detail_answer_field.id self.name = detail_answer_field.name @@ -311,16 +370,14 @@ def __init__(self, detail_answer_field, detail_answer_schema): ] else: self.otherType = "input" - self.value = escape( - detail_answer_field._value() - ) # pylint: disable=protected-access + self.value = escape(detail_answer_field._value()) if answer_type == "Number": self.width = get_width_for_number(detail_answer_schema) -@blueprint.app_template_filter() # type: ignore -def map_select_config(form, answer): +@blueprint.app_template_filter() +def map_select_config(form: FormType, answer: AnswerType) -> list[SelectConfig]: options = form["fields"][answer["id"]] return [ @@ -330,12 +387,14 @@ def map_select_config(form, answer): @blueprint.app_context_processor -def map_select_config_processor(): - return dict(map_select_config=map_select_config) +def map_select_config_processor() -> dict[str, Callable]: + return {"map_select_config": map_select_config} -@blueprint.app_template_filter() # type: ignore -def map_relationships_config(form, answer): +@blueprint.app_template_filter() +def map_relationships_config( + form: Mapping[str, str], answer: Mapping[str, int | slice] +) -> list[RelationshipRadioConfig]: options = form["fields"][answer["id"]] return [ @@ -344,43 +403,61 @@ def map_relationships_config(form, answer): @blueprint.app_context_processor -def map_relationships_config_processor(): - return dict(map_relationships_config=map_relationships_config) +def map_relationships_config_processor() -> dict[str, Callable]: + return {"map_relationships_config": map_relationships_config} class DropdownConfig: - def __init__(self, option, select): + def __init__( + self, option: SelectFieldBase._Option, select: SelectFieldBase._Option + ) -> None: self.value, self.text = option.value, option.label self.selected = select.data == self.value self.disabled = self.value == "" and select.flags.required @blueprint.app_template_filter() -def map_dropdown_config(select): +def map_dropdown_config(select: SelectFieldBase._Option) -> list[DropdownConfig]: return [DropdownConfig(choice, select) for choice in select.choices] @blueprint.app_context_processor -def map_dropdown_config_processor(): - return dict(map_dropdown_config=map_dropdown_config) +def map_dropdown_config_processor() -> dict[str, Callable]: + return {"map_dropdown_config": map_dropdown_config} class SummaryAction: - def __init__(self, answer, answer_title, edit_link_text, edit_link_aria_label): + def __init__( + self, + answer: SelectFieldBase._Option, + item_title: str, + edit_link_text: str, + item_name: str | None = None, + ) -> None: self.text = edit_link_text - self.ariaLabel = edit_link_aria_label + " " + answer_title + if item_name: + self.visuallyHiddenText = flask_babel.lazy_gettext( + "Change answer for {item_name}: {question_title_or_answer_label}" + ).format(item_name=item_name, question_title_or_answer_label=item_title) + else: + self.visuallyHiddenText = flask_babel.lazy_gettext( + "Change your answer for: {question_title_or_answer_label}" + ).format(question_title_or_answer_label=item_title) + self.url = answer["link"] self.attributes = { "data-qa": answer["id"] + "-edit", "data-ga": "click", - "data-ga-category": "Summary", - "data-ga-action": "Edit click", + "data-ga-category": "Link", + "data-ga-action": "Edit", + "data-ga-label": "Edit", + "data-ga-page": "Summary", } class SummaryRowItemValue: - def __init__(self, text, other=None): + def __init__(self, text: str, other: str | None = None) -> None: self.text = text if other or other == 0: @@ -388,32 +465,23 @@ def __init__(self, text, other=None): class SummaryRowItem: - def __init__( # noqa: C901, R0912 pylint: disable=too-complex, too-many-branches + def __init__( # noqa: C901 pylint: disable=too-complex, too-many-branches self, - question, - answer, - multiple_answers, - answers_are_editable, - no_answer_provided, - edit_link_text, - edit_link_aria_label, - summary_type, - ): - - if "type" in answer: - answer_type = answer["type"] - else: - answer_type = "calculated" - + question: SelectFieldBase._Option, + answer: SelectFieldBase._Option, + answers_are_editable: bool, + no_answer_provided: str, + edit_link_text: str, + summary_type: str, + use_answer_label: bool = False, + item_name: str | None = None, + ) -> None: + answer_type = answer.get("type", "calculated") if ( - ( - multiple_answers - or answer_type == "relationship" - or summary_type == "CalculatedSummary" - ) - and "label" in answer - and answer["label"] - ): + answer_type == "relationship" + or is_summary_with_calculation(summary_type) + or use_answer_label + ) and answer.get("label"): self.rowTitle = answer["label"] self.rowTitleAttributes = {"data-qa": answer["id"] + "-label"} else: @@ -437,8 +505,15 @@ def __init__( # noqa: C901, R0912 pylint: disable=too-complex, too-many-branche for option in value ] elif answer_type == "currency": + decimal_places = answer.get("decimal_places") self.valueList = [ - SummaryRowItemValue(get_formatted_currency(value, answer["currency"])) + SummaryRowItemValue( + get_formatted_currency( + value=value, + currency=answer["currency"], + decimal_limit=decimal_places, + ) + ) ] elif answer_type in ["date", "monthyeardate", "yeardate"]: if question["type"] == "DateRange": @@ -471,29 +546,27 @@ def __init__( # noqa: C901, R0912 pylint: disable=too-complex, too-many-branche if answers_are_editable: self.actions = [ - SummaryAction( - answer, self.rowTitle, edit_link_text, edit_link_aria_label - ) + SummaryAction(answer, self.rowTitle, edit_link_text, item_name) ] class SummaryRow: def __init__( self, - question, - summary_type, - answers_are_editable, - no_answer_provided, - edit_link_text, - edit_link_aria_label, - ): + question: SelectFieldBase._Option, + summary_type: SelectFieldBase._Option, + answers_are_editable: bool, + no_answer_provided: str, + edit_link_text: str, + use_answer_label: bool = False, + item_name: str | None = None, + ) -> None: self.rowTitle = strip_tags(question["title"]) self.id = question["id"] self.rowItems = [] + use_answer_label = use_answer_label or len(question["answers"]) > 1 - multiple_answers = len(question["answers"]) > 1 - - if summary_type == "CalculatedSummary" and not answers_are_editable: + if is_summary_with_calculation(summary_type) and not answers_are_editable: self.total = True for answer in question["answers"]: @@ -501,107 +574,204 @@ def __init__( SummaryRowItem( question, answer, - multiple_answers, answers_are_editable, no_answer_provided, edit_link_text, - edit_link_aria_label, summary_type, + use_answer_label, + item_name, ) ) -@blueprint.app_template_filter() # type: ignore +@blueprint.app_template_filter() def map_summary_item_config( - group, - summary_type, - answers_are_editable, - no_answer_provided, - edit_link_text, - edit_link_aria_label, - calculated_question, -): - rows = [ - SummaryRow( - block["question"], - summary_type, - answers_are_editable, - no_answer_provided, - edit_link_text, - edit_link_aria_label, - ) - for block in group["blocks"] - ] + group: dict[str, list | dict], + summary_type: str, + answers_are_editable: bool, + no_answer_provided: str, + edit_link_text: str, + edit_link_aria_label: str, + calculated_question: dict[str, list] | None, + remove_link_text: str | None = None, + remove_link_aria_label: str | None = None, +) -> list[dict[str, list] | SummaryRow]: + rows: list[dict[str, list] | SummaryRow] = [] + + for block in group["blocks"]: + if block.get("question"): + rows.append( + SummaryRow( + block["question"], + summary_type, + answers_are_editable, + no_answer_provided, + edit_link_text, + ) + ) + elif block.get("calculated_summary"): + rows.append( + SummaryRow( + block["calculated_summary"], + summary_type, + answers_are_editable, + no_answer_provided, + edit_link_text, + ) + ) + else: + list_collector_rows = map_list_collector_config( + list_items=block["list"]["list_items"], + editable=block["list"]["editable"], + edit_link_text=edit_link_text, + edit_link_aria_label=edit_link_aria_label, + remove_link_text=remove_link_text, + remove_link_aria_label=remove_link_aria_label, + related_answers=block.get("related_answers"), + item_label=block.get("item_label"), + item_anchor=block.get("item_anchor"), + answers_are_editable=answers_are_editable, + ) - if summary_type == "CalculatedSummary": - rows.append( - SummaryRow(calculated_question, summary_type, False, None, None, None) - ) + rows.extend(list_collector_rows) + + if is_summary_with_calculation(summary_type): + rows.append(SummaryRow(calculated_question, summary_type, False, "", "")) return rows @blueprint.app_context_processor -def map_summary_item_config_processor(): - return dict(map_summary_item_config=map_summary_item_config) +def map_summary_item_config_processor() -> dict[str, Callable]: + return {"map_summary_item_config": map_summary_item_config} + + +@blueprint.app_context_processor +def map_list_config_processor() -> dict[str, Callable]: + return {"map_list_config": map_list_config} + + +@blueprint.app_template_filter() +def map_list_config(list_values: list[dict]) -> list[dict]: + items_list: list[dict] = [] + + for index, value in enumerate(list_values, 1): + item: dict = {"text": value["item_title"]} + if value["is_complete"]: + item["iconType"] = "check" -@blueprint.app_template_filter() # type: ignore + item["attributes"] = { + "data-qa": f"list-item-{index}-label", + } + + items_list.append(item) + + return items_list + + +# pylint: disable=too-many-locals +@blueprint.app_template_filter() def map_list_collector_config( - list_items, - icon, - edit_link_text=None, - edit_link_aria_label=None, - remove_link_text=None, - remove_link_aria_label=None, -): - rows = [] - - for index, list_item in enumerate(list_items, start=1): - item_name = list_item.get("item_title") + list_items: list[dict[str, str | int]], + editable: bool = True, + render_icon: bool = False, + edit_link_text: str = "", + edit_link_aria_label: str = "", + remove_link_text: str | None = None, + remove_link_aria_label: str | None = None, + related_answers: dict | None = None, + item_label: str | None = None, + item_anchor: str | None = None, + answers_are_editable: bool = True, +) -> list[dict[str, list] | SummaryRow]: + rows: list[dict[str, list] | SummaryRow] = [] + + for index, list_item in enumerate(list_items, 1): + item_name = str(list_item.get("item_title")) actions = [] - - if edit_link_text: - actions.append( - { - "text": edit_link_text, - "ariaLabel": edit_link_aria_label.format(item_name=item_name), - "url": list_item.get("edit_link"), - "attributes": {"data-qa": f"list-item-change-{index}-link"}, - } + edit_link_hidden_text = None + remove_link_hidden_text = None + + if edit_link_text and editable: + url = ( + f'{list_item.get("edit_link")}{item_anchor}' + if item_anchor + else list_item.get("edit_link") ) - if not list_item.get("primary_person") and remove_link_text: + edit_link = { + "text": edit_link_text, + "visuallyHiddenText": edit_link_hidden_text, + "url": url, + "attributes": {"data-qa": f"list-item-change-{index}-link"}, + } + + if edit_link_aria_label: + edit_link_hidden_text = edit_link_aria_label.format(item_name=item_name) + edit_link["visuallyHiddenText"] = edit_link_hidden_text + + actions.append(edit_link) + + if not list_item.get("primary_person") and remove_link_text and editable: + if remove_link_aria_label: + remove_link_hidden_text = remove_link_aria_label.format( + item_name=item_name + ) + actions.append( { "text": remove_link_text, - "ariaLabel": remove_link_aria_label.format(item_name=item_name), + "visuallyHiddenText": remove_link_hidden_text, "url": list_item.get("remove_link"), "attributes": {"data-qa": f"list-item-remove-{index}-link"}, } ) - rows.append( - { - "rowItems": [ - { - "iconType": icon, - "actions": actions, - "rowTitle": item_name, - "id": list_item.get("list_item_id"), - "rowTitleAttributes": { - "data-qa": f"list-item-{index}-label", - "data-list-item-id": list_item.get("list_item_id"), - }, - } - ] - } + icon = ( + "check" + if render_icon + and list_item.get("repeating_blocks") + and list_item.get("is_complete") + else None ) + row_item = { + "iconType": icon, + "iconVisuallyHiddenText": "Completed" if icon else None, + "actions": actions, + "id": list_item.get("list_item_id"), + "rowTitleAttributes": { + "data-qa": f"list-item-{index}-label", + "data-list-item-id": list_item.get("list_item_id"), + }, + } + + if item_label: + row_item["valueList"] = [{"text": item_name}] + + row_item["rowTitle"] = item_label or item_name + row_items: list = [row_item] + + if related_answers: + for block in related_answers[list_item["list_item_id"]]: + summary_row = SummaryRow( + block["question"], + summary_type="SectionSummary", + answers_are_editable=answers_are_editable, + no_answer_provided=flask_babel.lazy_gettext("No answer provided"), + edit_link_text=edit_link_text, + use_answer_label=True, + item_name=item_name, + ) + row_items.extend(summary_row.rowItems) + + rows.append({"rowItems": row_items}) + return rows @blueprint.app_context_processor -def map_list_collector_config_processor(): - return dict(map_list_collector_config=map_list_collector_config) +def map_list_collector_config_processor() -> dict[str, Callable]: + return {"map_list_collector_config": map_list_collector_config} diff --git a/app/keys.py b/app/keys.py index 74eafb5224..a6ef1c6346 100644 --- a/app/keys.py +++ b/app/keys.py @@ -1,2 +1,3 @@ KEY_PURPOSE_AUTHENTICATION = "authentication" KEY_PURPOSE_SUBMISSION = "submission" +KEY_PURPOSE_SDS = "supplementary_data" diff --git a/tests/integration/questionnaire/test_questionnaire_content_variants.py b/app/oidc/__init__.py similarity index 100% rename from tests/integration/questionnaire/test_questionnaire_content_variants.py rename to app/oidc/__init__.py diff --git a/app/oidc/gcp_oidc.py b/app/oidc/gcp_oidc.py new file mode 100644 index 0000000000..18671ac081 --- /dev/null +++ b/app/oidc/gcp_oidc.py @@ -0,0 +1,35 @@ +from cachetools.func import ttl_cache +from google.auth.credentials import Credentials +from google.auth.transport.requests import Request +from google.oauth2.id_token import fetch_id_token_credentials +from structlog import get_logger + +from app.oidc.oidc import OIDCCredentialsService +from app.settings import OIDC_TOKEN_LEEWAY_IN_SECONDS, OIDC_TOKEN_VALIDITY_IN_SECONDS + +TTL = OIDC_TOKEN_VALIDITY_IN_SECONDS - OIDC_TOKEN_LEEWAY_IN_SECONDS + +logger = get_logger() + + +class OIDCCredentialsServiceGCP(OIDCCredentialsService): + @staticmethod + @ttl_cache(maxsize=None, ttl=TTL) + def get_credentials(*, iap_client_id: str) -> Credentials: + """ + The credentials are valid for OIDC_TOKEN_VALIDITY_IN_SECONDS, and we use a ttl cache of this minus a configurable leeway + this is to ensure that even in the edge case where the timers on the cache and the credentials don't quite align, + the OIDC_TOKEN_LEEWAY_IN_SECONDS should be more than enough to cover it and guarantee safety + """ + logger.info("fetching OIDC credentials from GCP", iap_client_id=iap_client_id) + + request = Request() + + credentials = fetch_id_token_credentials( + audience=iap_client_id, request=request + ) + + # Refresh the credential to obtain an ID token. + credentials.refresh(request) + + return credentials diff --git a/app/oidc/local_oidc.py b/app/oidc/local_oidc.py new file mode 100644 index 0000000000..6eb8240433 --- /dev/null +++ b/app/oidc/local_oidc.py @@ -0,0 +1,21 @@ +from functools import lru_cache + +from google.auth.credentials import AnonymousCredentials, Credentials +from structlog import get_logger + +from app.oidc.oidc import OIDCCredentialsService + +logger = get_logger() + + +class OIDCCredentialsServiceLocal(OIDCCredentialsService): + @staticmethod + @lru_cache + def get_credentials(*, iap_client_id: str) -> Credentials: + """ + Anonymous credentials don't expire so can be generated once and cached + """ + logger.info("generating local OIDC credentials", iap_client_id=iap_client_id) + + # Return Credentials which do not provide any authentication or make any requests for tokens + return AnonymousCredentials() diff --git a/app/oidc/oidc.py b/app/oidc/oidc.py new file mode 100644 index 0000000000..3dea808793 --- /dev/null +++ b/app/oidc/oidc.py @@ -0,0 +1,10 @@ +from abc import ABC, abstractmethod + +from google.auth.credentials import Credentials + + +class OIDCCredentialsService(ABC): + @staticmethod + @abstractmethod + def get_credentials(*, iap_client_id: str) -> Credentials: # pragma no cover + ... diff --git a/app/publisher/__init__.py b/app/publisher/__init__.py index e18d0cdf4a..bdd69ec62f 100644 --- a/app/publisher/__init__.py +++ b/app/publisher/__init__.py @@ -1,3 +1,3 @@ -from .publisher import LogPublisher, PubSubPublisher +from app.publisher.publisher import LogPublisher, PubSubPublisher -__all__ = ["PubSubPublisher", "LogPublisher"] +__all__ = ["LogPublisher", "PubSubPublisher"] diff --git a/app/publisher/publisher.py b/app/publisher/publisher.py index 11abdf72c5..909a437a3b 100644 --- a/app/publisher/publisher.py +++ b/app/publisher/publisher.py @@ -12,23 +12,26 @@ class Publisher(ABC): @abstractmethod - def publish(self, topic_id, message, fulfilment_request_transaction_id): + def publish( + self, topic_id: str, message: bytes, fulfilment_request_transaction_id: str + ) -> None: pass # pragma: no cover class PubSubPublisher(Publisher): - def __init__(self): + def __init__(self) -> None: self._client = PublisherClient() _, self._project_id = google.auth.default() - def _publish(self, topic_id, message): + def _publish(self, topic_id: str, message: bytes) -> Future: logger.info("publishing message", topic_id=topic_id) - # pylint: disable=no-member topic_path = self._client.topic_path(self._project_id, topic_id) response: Future = self._client.publish(topic_path, message) return response - def publish(self, topic_id, message: bytes, fulfilment_request_transaction_id: str): + def publish( + self, topic_id: str, message: bytes, fulfilment_request_transaction_id: str + ) -> None: response = self._publish(topic_id, message) try: # Resolve the future @@ -39,7 +42,7 @@ def publish(self, topic_id, message: bytes, fulfilment_request_transaction_id: s message_id=message_id, fulfilment_request_transaction_id=fulfilment_request_transaction_id, ) - except Exception as exc: # pylint:disable=broad-except + except Exception as exc: logger.exception( "message publication failed", topic_id=topic_id, @@ -48,7 +51,9 @@ def publish(self, topic_id, message: bytes, fulfilment_request_transaction_id: s class LogPublisher(Publisher): - def publish(self, topic_id, message: bytes, fulfilment_request_transaction_id: str): + def publish( + self, topic_id: str, message: bytes, fulfilment_request_transaction_id: str + ) -> None: logger.info( "publishing message", topic_id=topic_id, diff --git a/app/questionnaire/__init__.py b/app/questionnaire/__init__.py index 44390dfebc..53980d0a57 100644 --- a/app/questionnaire/__init__.py +++ b/app/questionnaire/__init__.py @@ -1,4 +1,7 @@ -from .location import Location -from .questionnaire_schema import QuestionnaireSchema, QuestionSchema +from app.questionnaire.location import Location +from app.questionnaire.questionnaire_schema import ( + QuestionnaireSchema, + QuestionSchemaType, +) -__all__ = ["QuestionnaireSchema", "Location", "QuestionSchema"] +__all__ = ["Location", "QuestionSchemaType", "QuestionnaireSchema"] diff --git a/app/questionnaire/dependencies.py b/app/questionnaire/dependencies.py new file mode 100644 index 0000000000..5be9818ae6 --- /dev/null +++ b/app/questionnaire/dependencies.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Iterable, Mapping, Sequence + +from ordered_set import OrderedSet +from werkzeug.datastructures import MultiDict + +from app.data_models import ProgressStore +from app.questionnaire import QuestionnaireSchema +from app.questionnaire.questionnaire_schema import get_sources_for_types_from_data +from app.utilities.mappings import get_flattened_mapping_values +from app.utilities.types import LocationType, SectionKey + +if TYPE_CHECKING: + from app.questionnaire.path_finder import PathFinder # pragma: no cover + + +def get_routing_path_block_ids_by_section_for_dependent_sections( + *, + location: LocationType, + progress_store: ProgressStore, + path_finder: PathFinder, + source_types: Iterable[str], + data: MultiDict | Mapping | Sequence, + sections_to_ignore: list | None = None, + ignore_keys: list[str] | None = None, + dependent_sections: dict[str, set[str]] | dict[str, OrderedSet[str]], +) -> dict[SectionKey, tuple[str, ...]]: + block_ids_by_section: dict[SectionKey, tuple[str, ...]] = {} + sections_to_ignore = sections_to_ignore or [] + + dependents = ( + OrderedSet(dependent_sections.get(location.block_id, [])) + if location.block_id + else get_flattened_mapping_values(dependent_sections) + ) + + if dependents and not get_sources_for_types_from_data( + source_types=source_types, data=data, ignore_keys=ignore_keys + ): + return block_ids_by_section + + for section in dependents: + # Dependent sections other than the current section cannot be a repeating section + list_item_id = location.list_item_id if section == location.section_id else None + key = SectionKey(section, list_item_id) + + if key in sections_to_ignore: + continue + + if key in progress_store.started_section_keys(): + routing_path = path_finder.routing_path(key) + block_ids_by_section[key] = routing_path.block_ids + + return block_ids_by_section + + +def get_routing_path_block_ids_by_section_for_calculation_summary_dependencies( + *, + location: LocationType, + progress_store: ProgressStore, + path_finder: PathFinder, + data: MultiDict | Mapping | Sequence, + sections_to_ignore: list | None = None, + ignore_keys: list[str] | None = None, + schema: QuestionnaireSchema, +) -> dict[SectionKey, tuple[str, ...]]: + """ + If the current location depends on any calculated or grand calculated summaries, + for all the sections that those CS or GCS values depend on, get the blocks on the path for that section. + These routing path block ids are then used to ensure the CS/GCS only includes answers on the path + """ + dependent_sections = schema.calculation_summary_section_dependencies_by_block[ + location.section_id + ] + return get_routing_path_block_ids_by_section_for_dependent_sections( + location=location, + progress_store=progress_store, + sections_to_ignore=sections_to_ignore, + data=data, + path_finder=path_finder, + source_types={"calculated_summary", "grand_calculated_summary"}, + ignore_keys=ignore_keys, + dependent_sections=dependent_sections, + ) diff --git a/app/questionnaire/dynamic_answer_options.py b/app/questionnaire/dynamic_answer_options.py index a1aa86ac01..f267f57208 100644 --- a/app/questionnaire/dynamic_answer_options.py +++ b/app/questionnaire/dynamic_answer_options.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Any, Mapping, Union +from typing import Mapping from app.questionnaire.rules.operator import Operator from app.questionnaire.rules.rule_evaluator import RuleEvaluator, RuleEvaluatorTypes @@ -12,15 +12,13 @@ @dataclass class DynamicAnswerOptions: - dynamic_options_schema: Mapping[str, Any] + dynamic_options_schema: Mapping rule_evaluator: RuleEvaluator value_source_resolver: ValueSourceResolver def evaluate(self) -> tuple[dict[str, str], ...]: values = self.dynamic_options_schema["values"] - resolved_values: Union[ - ValueSourceEscapedTypes, ValueSourceTypes, RuleEvaluatorTypes - ] + resolved_values: ValueSourceEscapedTypes | ValueSourceTypes | RuleEvaluatorTypes if "source" in values: if values["source"] != "answers": diff --git a/app/questionnaire/location.py b/app/questionnaire/location.py index a3f293f7d4..ae568090c0 100644 --- a/app/questionnaire/location.py +++ b/app/questionnaire/location.py @@ -1,13 +1,15 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Mapping, Optional +from typing import Any, Mapping from flask import url_for +from app.utilities.types import SectionKey + class InvalidLocationException(Exception): - def __init__(self, value): + def __init__(self, value: str): super().__init__() self.value = value @@ -24,15 +26,15 @@ class Location: """ section_id: str - block_id: Optional[str] = None - list_name: Optional[str] = None - list_item_id: Optional[str] = None + block_id: str | None = None + list_name: str | None = None + list_item_id: str | None = None - def __hash__(self): + def __hash__(self) -> int: return hash(frozenset(self.__dict__.values())) @classmethod - def from_dict(cls, location_dict: Mapping): + def from_dict(cls, location_dict: Mapping[str, str]) -> Location: section_id = location_dict["section_id"] block_id = location_dict["block_id"] list_item_id = location_dict.get("list_item_id") @@ -44,7 +46,7 @@ def from_dict(cls, location_dict: Mapping): list_item_id=list_item_id, ) - def url(self, **kwargs) -> str: + def url(self, **kwargs: Any) -> str: """ Return the survey runner url that this location represents Any additional keyword arguments are parsed as query strings. @@ -57,3 +59,7 @@ def url(self, **kwargs) -> str: list_item_id=self.list_item_id, **kwargs, ) + + @property + def section_key(self) -> SectionKey: + return SectionKey(self.section_id, self.list_item_id) diff --git a/app/questionnaire/path_finder.py b/app/questionnaire/path_finder.py index 6d8483c2d7..466c0aaf6c 100644 --- a/app/questionnaire/path_finder.py +++ b/app/questionnaire/path_finder.py @@ -1,51 +1,35 @@ -from typing import Mapping, Optional +from typing import Iterable, Mapping, Sequence from werkzeug.datastructures import ImmutableDict -from app.data_models.answer_store import AnswerStore -from app.data_models.list_store import ListStore -from app.data_models.progress_store import CompletionStatus, ProgressStore -from app.questionnaire.location import Location +from app.data_models import CompletionStatus +from app.data_models.data_stores import DataStores +from app.questionnaire.location import Location, SectionKey from app.questionnaire.questionnaire_schema import QuestionnaireSchema from app.questionnaire.routing_path import RoutingPath -from app.questionnaire.rules.operator import OPERATION_MAPPING -from app.questionnaire.rules.rule_evaluator import RuleEvaluator -from app.questionnaire.when_rules import evaluate_goto, evaluate_when_rules +from app.questionnaire.rules.rule_evaluator import RuleEvaluator, RuleEvaluatorTypes +from app.utilities.types import LocationType class PathFinder: - def __init__( - self, - schema: QuestionnaireSchema, - answer_store: AnswerStore, - list_store: ListStore, - progress_store: ProgressStore, - metadata: Mapping, - response_metadata: Mapping, - ): - self.answer_store = answer_store - self.metadata = metadata - self.response_metadata = response_metadata + def __init__(self, schema: QuestionnaireSchema, data_stores: DataStores): + self.data_stores = data_stores self.schema = schema - self.progress_store = progress_store - self.list_store = list_store - def routing_path( - self, section_id: str, list_item_id: Optional[str] = None - ) -> RoutingPath: + def routing_path(self, section_key: SectionKey) -> RoutingPath: """ Visits all the blocks in a section and returns a path given a list of answers. """ routing_path_block_ids: list[str] = [] - current_location = Location(section_id=section_id, list_item_id=list_item_id) - section = self.schema.get_section(section_id) + current_location = Location(**section_key.to_dict()) + section = self.schema.get_section(section_key.section_id) list_name = self.schema.get_repeating_list_for_section( current_location.section_id ) if section: - when_rules_block_dependencies = self._get_when_rules_block_dependencies( - section["id"] + when_rules_block_dependencies = self.get_when_rules_block_dependencies( + section_key.section_id ) blocks = self._get_not_skipped_blocks_in_section( current_location, @@ -59,37 +43,43 @@ def routing_path( blocks, current_location, when_rules_block_dependencies ) - return RoutingPath(routing_path_block_ids, section_id, list_item_id, list_name) + return RoutingPath( + block_ids=routing_path_block_ids, + list_name=list_name, + **section_key.to_dict(), + ) - def _get_when_rules_block_dependencies(self, section_id: str) -> list[str]: + def get_when_rules_block_dependencies(self, section_id: str) -> list[str]: """NB: At present when rules block dependencies does not fully support repeating sections. - It is supported when the section is dependent i.e the current section is repeating and building the routing path for sections that are not, + It is supported when the section is dependent i.e. the current section is repeating and building the routing path for sections that are not, It isn't supported if it needs to build the path for repeating sections""" + dependencies_for_section = ( + self.schema.get_all_when_rules_section_dependencies_for_section(section_id) + ) + return [ block_id - for dependent_section in self.schema.when_rules_section_dependencies_map.get( - section_id, {} - ) - for block_id in self.routing_path(dependent_section) - if (dependent_section, None) in self.progress_store.started_section_keys() + for dependent_section in dependencies_for_section + for block_id in self.routing_path(SectionKey(dependent_section)) + if SectionKey(dependent_section) + in self.data_stores.progress_store.started_section_keys() ] def _get_not_skipped_blocks_in_section( self, - location: Location, + current_location: LocationType, routing_path_block_ids: list[str], section: ImmutableDict, when_rules_block_dependencies: list[str], - ) -> list[Mapping]: + ) -> list[dict] | None: # :TODO: Fix group skipping in its own section. Routing path will be empty and therefore not checked if section: - not_skipped_blocks: list[Mapping] = [] + not_skipped_blocks: list[dict] = [] for group in section["groups"]: - if "skip_conditions" in group: skip_conditions = group.get("skip_conditions") if self.evaluate_skip_conditions( - location, + current_location, routing_path_block_ids, skip_conditions, when_rules_block_dependencies, @@ -100,7 +90,9 @@ def _get_not_skipped_blocks_in_section( return not_skipped_blocks @staticmethod - def _block_index_for_block_id(blocks, block_id): + def _block_index_for_block_id( + blocks: Iterable[Mapping], block_id: str + ) -> int | None: return next( (index for (index, block) in enumerate(blocks) if block["id"] == block_id), None, @@ -108,12 +100,12 @@ def _block_index_for_block_id(blocks, block_id): def _build_routing_path_block_ids( self, - blocks: list[Mapping], - current_location: Location, + blocks: Sequence[Mapping], + current_location: LocationType, when_rules_block_dependencies: list[str], ) -> list[str]: # Keep going unless we've hit the last block - + # routing_path_block_ids can be mutated by _evaluate_routing_rules routing_path_block_ids: list[str] = [] block_index = 0 repeating_list = self.schema.get_repeating_list_for_section( @@ -149,9 +141,10 @@ def _build_routing_path_block_ids( routing_path_block_ids.append(block_id) # If routing rules exist then a rule must match (i.e. default goto) - routing_rules = block.get("routing_rules") + routing_rules: Iterable[Mapping] | None = block.get("routing_rules") if routing_rules: - block_index = self._evaluate_routing_rules( + # Type ignore: block_index will always be non-null when evaluate is called + block_index = self._evaluate_routing_rules( # type: ignore this_location, blocks, routing_rules, @@ -171,45 +164,36 @@ def _build_routing_path_block_ids( # No routing rules, so step forward a block block_index = block_index + 1 + return routing_path_block_ids # pragma: no cover + def _evaluate_routing_rules( self, - this_location, - blocks, - routing_rules, - block_index, - routing_path_block_ids, - when_rules_block_dependencies, - ): - if when_rules_block_dependencies: - routing_path_block_ids = ( - when_rules_block_dependencies + routing_path_block_ids - ) + this_location: Location, + blocks: Iterable[Mapping], + routing_rules: Iterable[Mapping], + block_index: int, + routing_path_block_ids: list[str], + when_rules_block_dependencies: list[str], + ) -> int | None: + # Use `list` to create a shallow copy since routing_path_block_ids is mutated hence we don't want to update its memory reference + block_ids_for_dependencies = ( + list(routing_path_block_ids) + when_rules_block_dependencies + ) when_rule_evaluator = RuleEvaluator( self.schema, - self.answer_store, - self.list_store, - self.metadata, - self.response_metadata, + self.data_stores, location=this_location, - routing_path_block_ids=routing_path_block_ids, + routing_path_block_ids=block_ids_for_dependencies, ) for rule in routing_rules: - if "goto" in rule: - rule = rule["goto"] - should_goto = evaluate_goto( - rule, - self.schema, - self.metadata, - self.answer_store, - self.list_store, - current_location=this_location, - routing_path_block_ids=routing_path_block_ids, - ) - else: - should_goto = should_goto_new(rule, when_rule_evaluator) + rule_valid = ( + when_rule_evaluator.evaluate(when_rule) + if (when_rule := rule.get("when")) + else True + ) - if should_goto: + if rule_valid: if rule.get("section") == "End": return None @@ -232,96 +216,66 @@ def _evaluate_routing_rules( def evaluate_skip_conditions( self, - this_location, - routing_path_block_ids, - skip_conditions, - when_rules_block_dependencies, - ): + current_location: LocationType, + routing_path_block_ids: list[str], + skip_conditions: ImmutableDict[str, dict] | None, + when_rules_block_dependencies: list[str], + ) -> RuleEvaluatorTypes: if not skip_conditions: return False - if when_rules_block_dependencies: - routing_path_block_ids = ( - when_rules_block_dependencies + routing_path_block_ids - ) + block_ids_for_dependencies = ( + list(routing_path_block_ids) + when_rules_block_dependencies + ) - if isinstance(skip_conditions, dict): - when_rule_evaluator = RuleEvaluator( - self.schema, - self.answer_store, - self.list_store, - self.metadata, - self.response_metadata, - location=this_location, - routing_path_block_ids=routing_path_block_ids, - ) + when_rule_evaluator = RuleEvaluator( + schema=self.schema, + data_stores=self.data_stores, + location=current_location, + routing_path_block_ids=block_ids_for_dependencies, + ) - return when_rule_evaluator.evaluate(skip_conditions["when"]) - - for when in skip_conditions: - condition = evaluate_when_rules( - when["when"], - self.schema, - self.metadata, - self.answer_store, - self.list_store, - current_location=this_location, - routing_path_block_ids=routing_path_block_ids, - ) - if condition is True: - return True - return False + return when_rule_evaluator.evaluate(skip_conditions["when"]) - def _get_next_block_id(self, rule): + def _get_next_block_id(self, rule: Mapping) -> str: if "group" in rule: - return self.schema.get_first_block_id_for_group(rule["group"]) - return rule["block"] + # Type ignore: by this point the block for the rule will exist + return self.schema.get_first_block_id_for_group(rule["group"]) # type: ignore + # Type ignore: the rules block will be a string + return rule["block"] # type: ignore def _remove_current_blocks_answers_for_backwards_routing( - self, rules: dict, this_location: Location + self, rule: Mapping, this_location: Location ) -> None: - if block_id := this_location.block_id: answer_ids_for_current_block = self.schema.get_answer_ids_for_block( block_id ) - if "when" in rules: - if isinstance(rules["when"], dict): - self._remove_current_blocks_answers_for_new_backwards_routing( - rules["when"], answer_ids_for_current_block - ) - else: - for rule in rules["when"]: - if "id" in rule and rule["id"] in answer_ids_for_current_block: - self.answer_store.remove_answer(rule["id"]) + if "when" in rule: + self._remove_block_answers_for_backward_routing_according_to_when_rule( + rule["when"], answer_ids_for_current_block + ) - self.progress_store.remove_location_for_backwards_routing(this_location) - self.progress_store.update_section_status( - CompletionStatus.IN_PROGRESS, this_location.section_id + self.data_stores.progress_store.remove_location_for_backwards_routing( + this_location + ) + self.data_stores.progress_store.update_section_status( + CompletionStatus.IN_PROGRESS, this_location.section_key ) - def _remove_current_blocks_answers_for_new_backwards_routing( - self, rules: dict, answer_ids_for_current_block: list[str] + def _remove_block_answers_for_backward_routing_according_to_when_rule( + self, rules: Mapping, answer_ids_for_current_block: list[str] ) -> None: operands = self.schema.get_operands(rules) + for rule in operands: if isinstance(rule, dict) and ( "identifier" in rule and rule["identifier"] in answer_ids_for_current_block ): - if ( - "identifier" in rule - and rule["identifier"] in answer_ids_for_current_block - ): - self.answer_store.remove_answer(rule["identifier"]) - if any(operator in rule for operator in OPERATION_MAPPING): - return self._remove_current_blocks_answers_for_new_backwards_routing( + self.data_stores.answer_store.remove_answer(rule["identifier"]) + + if QuestionnaireSchema.has_operator(rule): + return self._remove_block_answers_for_backward_routing_according_to_when_rule( rule, answer_ids_for_current_block ) - - -def should_goto_new(rule, when_rule_evaluator): - if when_rule := rule.get("when"): - return when_rule_evaluator.evaluate(when_rule) - - return True diff --git a/app/questionnaire/placeholder_parser.py b/app/questionnaire/placeholder_parser.py index 05b801df65..6e57ea3a03 100644 --- a/app/questionnaire/placeholder_parser.py +++ b/app/questionnaire/placeholder_parser.py @@ -1,31 +1,43 @@ +from __future__ import annotations + from decimal import Decimal from typing import ( TYPE_CHECKING, Any, + Iterable, Mapping, MutableMapping, - Optional, Sequence, - Union, + TypeAlias, ) -from app.data_models.answer_store import AnswerStore -from app.data_models.list_store import ListStore -from app.questionnaire import Location, QuestionnaireSchema +from app.data_models.data_stores import DataStores +from app.questionnaire import QuestionnaireSchema +from app.questionnaire import path_finder as pf +from app.questionnaire.dependencies import ( + get_routing_path_block_ids_by_section_for_calculation_summary_dependencies, + get_routing_path_block_ids_by_section_for_dependent_sections, +) from app.questionnaire.placeholder_transforms import PlaceholderTransforms -from app.questionnaire.relationship_location import RelationshipLocation +from app.questionnaire.questionnaire_schema import ( + TRANSFORMS_REQUIRING_ROUTING_PATH, + TRANSFORMS_REQUIRING_UNRESOLVED_ARGUMENTS, +) from app.questionnaire.value_source_resolver import ( ValueSourceEscapedTypes, ValueSourceResolver, ValueSourceTypes, ) +from app.utilities.mappings import get_flattened_mapping_values, get_values_for_key +from app.utilities.types import LocationType, SectionKey if TYPE_CHECKING: - from app.questionnaire.placeholder_renderer import ( - PlaceholderRenderer, # pragma: no cover + from app.questionnaire.placeholder_renderer import ( # pragma: no cover + PlaceholderRenderer, ) -TransformedValueTypes = Union[None, str, int, Decimal, bool] + +TransformedValueTypes: TypeAlias = None | str | int | Decimal | bool class PlaceholderParser: @@ -37,54 +49,81 @@ class PlaceholderParser: def __init__( self, language: str, - answer_store: AnswerStore, - list_store: ListStore, - metadata: Mapping, - response_metadata: Mapping, + data_stores: DataStores, schema: QuestionnaireSchema, - renderer: "PlaceholderRenderer", - list_item_id: Optional[str] = None, - location: Union[Location, RelationshipLocation, None] = None, + renderer: PlaceholderRenderer, + list_item_id: str | None = None, + location: LocationType | None = None, + placeholder_preview_mode: bool | None = False, ): - - self._answer_store = answer_store - self._list_store = list_store - self._metadata = metadata - self._response_metadata = response_metadata - self._schema = schema - self._list_item_id = list_item_id - self._location = location self._transformer = PlaceholderTransforms(language, schema, renderer) self._placeholder_map: MutableMapping[ - str, Union[ValueSourceEscapedTypes, ValueSourceTypes, None] + str, ValueSourceEscapedTypes | ValueSourceTypes | None ] = {} + self._data_stores = data_stores + self._list_item_id = list_item_id + self._schema = schema + self._location = location + self._placeholder_preview_mode = placeholder_preview_mode + + self._path_finder = pf.PathFinder( + schema=self._schema, data_stores=self._data_stores + ) + + self._value_source_resolver = self._get_value_source_resolver() + self._routing_path_block_ids_by_section_key: dict = {} + + def __call__( + self, placeholder_list: Sequence[Mapping] + ) -> MutableMapping[str, ValueSourceEscapedTypes | ValueSourceTypes]: + sections_to_ignore = list(self._routing_path_block_ids_by_section_key) + + if routing_path_block_ids_map := self._get_routing_path_block_ids_by_section_for_calculated_summary_dependencies( + data=placeholder_list, + sections_to_ignore=sections_to_ignore, + ): + self._routing_path_block_ids_by_section_key.update( + routing_path_block_ids_map + ) + + routing_path_block_ids = get_flattened_mapping_values( + routing_path_block_ids_map + ) + self._value_source_resolver = self._get_value_source_resolver( + routing_path_block_ids=routing_path_block_ids, + ) + + for placeholder in placeholder_list: + # :TODO: Caching of placeholder values will need to be revisited once validation is added to ensure that placeholders are globally unique + # if placeholder["placeholder"] not in self._placeholder_map: + self._placeholder_map[placeholder["placeholder"]] = self._parse_placeholder( + placeholder + ) + return self._placeholder_map - self._value_source_resolver = ValueSourceResolver( - answer_store=self._answer_store, - list_store=self._list_store, - metadata=self._metadata, + def _get_value_source_resolver( + self, + *, + routing_path_block_ids: Iterable[str] | None = None, + assess_routing_path: bool | None = False, + ) -> ValueSourceResolver: + return ValueSourceResolver( + data_stores=self._data_stores, schema=self._schema, location=self._location, list_item_id=self._list_item_id, escape_answer_values=True, - response_metadata=self._response_metadata, use_default_answer=True, + assess_routing_path=assess_routing_path, + routing_path_block_ids=routing_path_block_ids, ) - def __call__( - self, placeholder_list: Sequence[Mapping] - ) -> MutableMapping[str, Union[ValueSourceEscapedTypes, ValueSourceTypes]]: - placeholder_list = QuestionnaireSchema.get_mutable_deepcopy(placeholder_list) - for placeholder in placeholder_list: - if placeholder["placeholder"] not in self._placeholder_map: - self._placeholder_map[ - placeholder["placeholder"] - ] = self._parse_placeholder(placeholder) - return self._placeholder_map + def _parse_placeholder(self, placeholder: Mapping) -> Any: + if self._placeholder_preview_mode and not self._all_value_sources_metadata( + placeholder + ): + return f'{{{placeholder["placeholder"]}}}' - def _parse_placeholder( - self, placeholder: Mapping - ) -> Union[ValueSourceEscapedTypes, ValueSourceTypes, TransformedValueTypes]: try: return self._parse_transforms(placeholder["transforms"]) except KeyError: @@ -94,24 +133,31 @@ def _parse_transforms( self, transform_list: Sequence[Mapping] ) -> TransformedValueTypes: transformed_value: TransformedValueTypes = None - for transform in transform_list: - transform_args: MutableMapping[str, Any] = {} + transform_args: MutableMapping = {} + value_source_resolver = self._get_value_source_resolver_for_transform( + transform + ) + + if transform["transform"] in TRANSFORMS_REQUIRING_UNRESOLVED_ARGUMENTS: + transform_args["unresolved_arguments"] = transform["arguments"] for arg_key, arg_value in transform["arguments"].items(): - resolved_value: Union[ - ValueSourceEscapedTypes, ValueSourceTypes, TransformedValueTypes - ] + resolved_value: ( + ValueSourceEscapedTypes | ValueSourceTypes | TransformedValueTypes + ) if isinstance(arg_value, list): - resolved_value = self._resolve_value_source_list(arg_value) + resolved_value = value_source_resolver.resolve_list( + value_source_list=arg_value, + ) elif isinstance(arg_value, dict): if "value" in arg_value: resolved_value = arg_value["value"] elif arg_value["source"] == "previous_transform": resolved_value = transformed_value else: - resolved_value = self._value_source_resolver.resolve(arg_value) + resolved_value = value_source_resolver.resolve(arg_value) else: resolved_value = arg_value @@ -123,14 +169,66 @@ def _parse_transforms( return transformed_value - def _resolve_value_source_list( - self, value_source_list: list[dict] - ) -> list[ValueSourceTypes]: - values: list[ValueSourceTypes] = [] - for value_source in value_source_list: - value = self._value_source_resolver.resolve(value_source) - if isinstance(value, list): - values.extend(value) - else: - values.append(value) - return values + def _get_routing_path_block_ids_by_section_for_calculated_summary_dependencies( + self, + data: Sequence[Mapping], + sections_to_ignore: list[str] | None = None, + ) -> dict[SectionKey, tuple[str, ...]] | None: + if not self._location: + return {} + + return ( + get_routing_path_block_ids_by_section_for_calculation_summary_dependencies( + location=self._location, + progress_store=self._data_stores.progress_store, + sections_to_ignore=sections_to_ignore, + data=data, + path_finder=self._path_finder, + ignore_keys=["when"], + schema=self._schema, + ) + ) + + @staticmethod + def _all_value_sources_metadata(placeholder: Mapping) -> bool: + sources = get_values_for_key("source", data=placeholder) + return all(source == "metadata" for source in sources) + + def _get_value_source_resolver_for_transform( + self, transform: Mapping + ) -> ValueSourceResolver: + if ( + self._location + and transform["transform"] in TRANSFORMS_REQUIRING_ROUTING_PATH + ): + dependent_sections = ( + self._schema.placeholder_transform_section_dependencies_by_block[ + self._location.section_id + ] + ) + block_ids = get_routing_path_block_ids_by_section_for_dependent_sections( + location=self._location, + progress_store=self._data_stores.progress_store, + sections_to_ignore=["when"], + path_finder=self._path_finder, + data=transform, + source_types={"answers"}, + dependent_sections=dependent_sections, + ) + self._routing_path_block_ids_by_section_key.update(block_ids) + transform_routing_path_block_ids = get_flattened_mapping_values(block_ids) + + value_source_routing_block_ids = ( + self._value_source_resolver.routing_path_block_ids or set() + ) + + routing_path_block_ids = ( + set(value_source_routing_block_ids) | transform_routing_path_block_ids + ) + + return self._get_value_source_resolver( + routing_path_block_ids=routing_path_block_ids, + assess_routing_path=True, + ) + + return self._value_source_resolver diff --git a/app/questionnaire/placeholder_renderer.py b/app/questionnaire/placeholder_renderer.py index c603dbb98d..33f437d4c0 100644 --- a/app/questionnaire/placeholder_renderer.py +++ b/app/questionnaire/placeholder_renderer.py @@ -1,15 +1,17 @@ -from typing import Any, Mapping, MutableMapping, Optional, Union +from copy import deepcopy +from typing import Mapping, MutableMapping from jsonpointer import resolve_pointer, set_pointer from app.data_models.answer import AnswerValueTypes -from app.data_models.answer_store import AnswerStore -from app.data_models.list_store import ListStore -from app.questionnaire import Location, QuestionnaireSchema -from app.questionnaire.placeholder_parser import PlaceholderParser +from app.data_models.data_stores import DataStores +from app.questionnaire import QuestionnaireSchema +from app.questionnaire.placeholder_parser import ( # pylint: disable=cyclic-import + PlaceholderParser, +) from app.questionnaire.plural_forms import get_plural_form_key -from app.questionnaire.relationship_location import RelationshipLocation from app.questionnaire.schema_utils import find_pointers_containing +from app.utilities.types import LocationType class PlaceholderRenderer: @@ -18,78 +20,86 @@ class PlaceholderRenderer: strings """ + PLACEHOLDER_ERROR_MESSAGE = "No placeholder found to render" + def __init__( self, language: str, - answer_store: AnswerStore, - list_store: ListStore, - metadata: Mapping, - response_metadata: Mapping, + data_stores: DataStores, schema: QuestionnaireSchema, - location: Union[None, Location, RelationshipLocation] = None, + location: LocationType | None = None, + placeholder_preview_mode: bool | None = False, ): + self._placeholder_preview_mode = placeholder_preview_mode self._language = language - self._answer_store = answer_store - self._list_store = list_store - self._metadata = metadata - self._response_metadata = response_metadata + self._data_stores = data_stores self._schema = schema self._location = location def render_pointer( self, - dict_to_render: Mapping[str, Any], + *, + dict_to_render: Mapping, pointer_to_render: str, - list_item_id: Optional[str], + list_item_id: str | None, + placeholder_parser: PlaceholderParser, ) -> str: pointer_data = resolve_pointer(dict_to_render, pointer_to_render) - return self.render_placeholder(pointer_data, list_item_id) + return self.render_placeholder(pointer_data, list_item_id, placeholder_parser) def get_plural_count( self, schema_partial: Mapping[str, str] - ) -> Optional[AnswerValueTypes]: + ) -> AnswerValueTypes | None: source = schema_partial["source"] source_id = schema_partial["identifier"] if source == "answers": - answer = self._answer_store.get_answer(source_id) + answer = self._data_stores.answer_store.get_answer(source_id) return answer.value if answer else None if source == "list": - return len(self._list_store[source_id]) + return len(self._data_stores.list_store[source_id]) - metadata_source_id: str = self._metadata[source_id] - return metadata_source_id + return ( + self._data_stores.metadata[source_id] + if self._data_stores.metadata + else None + ) def render_placeholder( self, - placeholder_data: MutableMapping[str, Any], - list_item_id: Optional[str], + placeholder_data: MutableMapping, + list_item_id: str | None, + placeholder_parser: PlaceholderParser | None = None, ) -> str: - placeholder_parser = PlaceholderParser( - language=self._language, - answer_store=self._answer_store, - list_store=self._list_store, - metadata=self._metadata, - response_metadata=self._response_metadata, - schema=self._schema, - list_item_id=list_item_id, - location=self._location, - renderer=self, - ) + if not placeholder_parser: + placeholder_parser = PlaceholderParser( + language=self._language, + data_stores=self._data_stores, + schema=self._schema, + list_item_id=list_item_id, + location=self._location, + renderer=self, + placeholder_preview_mode=self._placeholder_preview_mode, + ) placeholder_data = QuestionnaireSchema.get_mutable_deepcopy(placeholder_data) if "text_plural" in placeholder_data: plural_schema: Mapping[str, dict] = placeholder_data["text_plural"] - count = self.get_plural_count(plural_schema["count"]) + # Type ignore: For a valid schema the plural count will return an integer + count: int = ( + 0 # type: ignore + if self._placeholder_preview_mode + else self.get_plural_count(plural_schema["count"]) + ) plural_form_key = get_plural_form_key(count, self._language) plural_forms: Mapping[str, str] = plural_schema["forms"] placeholder_data["text"] = plural_forms[plural_form_key] if "text" not in placeholder_data and "placeholders" not in placeholder_data: - raise ValueError("No placeholder found to render") + raise ValueError(self.PLACEHOLDER_ERROR_MESSAGE) transformed_values = placeholder_parser(placeholder_data["placeholders"]) formatted_placeholder_data: str = placeholder_data["text"].format( @@ -99,16 +109,107 @@ def render_placeholder( return formatted_placeholder_data def render( - self, dict_to_render: Mapping[str, Any], list_item_id: Optional[str] - ) -> Mapping[str, Any]: + self, + *, + data_to_render: Mapping, + list_item_id: str | None, + ) -> dict: """ Transform the current schema json to a fully rendered dictionary """ - dict_to_render = QuestionnaireSchema.get_mutable_deepcopy(dict_to_render) - pointers = find_pointers_containing(dict_to_render, "placeholders") + data_to_render_mutable: dict = QuestionnaireSchema.get_mutable_deepcopy( + data_to_render + ) + + self._handle_and_resolve_dynamic_answers(data_to_render_mutable) + + pointers = find_pointers_containing(data_to_render_mutable, "placeholders") + + placeholder_parser = PlaceholderParser( + language=self._language, + data_stores=self._data_stores, + schema=self._schema, + list_item_id=list_item_id, + location=self._location, + renderer=self, + placeholder_preview_mode=self._placeholder_preview_mode, + ) for pointer in pointers: - rendered_text = self.render_pointer(dict_to_render, pointer, list_item_id) - set_pointer(dict_to_render, pointer, rendered_text) + rendered_text = self.render_pointer( + dict_to_render=data_to_render_mutable, + pointer_to_render=pointer, + list_item_id=list_item_id, + placeholder_parser=placeholder_parser, + ) + set_pointer(data_to_render_mutable, pointer, rendered_text) + return data_to_render_mutable + + def _handle_and_resolve_dynamic_answers(self, data_to_render_mutable: dict) -> None: + pointers = find_pointers_containing(data_to_render_mutable, "dynamic_answers") + + for pointer in pointers: + data = resolve_pointer(data_to_render_mutable, pointer) + dynamic_answers = data["dynamic_answers"] + + if dynamic_answers["values"]["source"] == "list": + self.resolve_dynamic_answers_ids(dynamic_answers) + self.resolve_dynamic_answers(dynamic_answers) + + updated_value = { + "answers": dynamic_answers["answers"] + data.get("answers", []), + "dynamic_answers": dynamic_answers, + } + + del updated_value["dynamic_answers"]["answers"] - return dict_to_render + if pointer: + set_pointer(data_to_render_mutable, pointer, updated_value) + else: + data_to_render_mutable |= updated_value + + def resolve_dynamic_answers_ids( + self, + dynamic_answers: dict, + ) -> None: + list_name = dynamic_answers["values"]["identifier"] + list_items = self._data_stores.list_store[list_name].items + + resolved_dynamic_answers = [] + + for dynamic_answer in dynamic_answers["answers"]: + for item in list_items: + resolved_dynamic_answer = deepcopy(dynamic_answer) + resolved_dynamic_answer["original_answer_id"] = dynamic_answer["id"] + resolved_dynamic_answer["id"] = f"{dynamic_answer['id']}-{item}" + resolved_dynamic_answer["list_item_id"] = item + + resolved_dynamic_answers.append(resolved_dynamic_answer) + + dynamic_answers["answers"] = resolved_dynamic_answers + + def resolve_dynamic_answers( + self, + dynamic_answers: dict, + ) -> None: + for answer in dynamic_answers["answers"]: + placeholder_parser = PlaceholderParser( + language=self._language, + data_stores=self._data_stores, + schema=self._schema, + list_item_id=answer["list_item_id"], + location=self._location, + renderer=self, + placeholder_preview_mode=self._placeholder_preview_mode, + ) + + pointers = find_pointers_containing(answer, "placeholders") + + for pointer in pointers: + rendered_text = self.render_pointer( + dict_to_render=answer, + pointer_to_render=pointer, + list_item_id=answer["list_item_id"], + placeholder_parser=placeholder_parser, + ) + set_pointer(answer, pointer, rendered_text) diff --git a/app/questionnaire/placeholder_transforms.py b/app/questionnaire/placeholder_transforms.py index dc25c2360a..500626aba6 100644 --- a/app/questionnaire/placeholder_transforms.py +++ b/app/questionnaire/placeholder_transforms.py @@ -1,18 +1,27 @@ +from collections.abc import Sized from datetime import date, datetime, timezone from decimal import Decimal -from typing import TYPE_CHECKING, Optional, Sequence, Sized, Union +from typing import TYPE_CHECKING, Literal, Mapping, Sequence from urllib.parse import quote from babel.dates import format_datetime -from babel.numbers import format_currency, format_decimal from dateutil.relativedelta import relativedelta from flask_babel import ngettext +from werkzeug.datastructures import ImmutableDict -from app.questionnaire.questionnaire_schema import QuestionnaireSchema +from app.questionnaire.questionnaire_schema import ( + QuestionnaireSchema, + get_calculated_summary_ids_for_grand_calculated_summary, +) from app.questionnaire.rules.operations import DateOffset from app.questionnaire.rules.operations_helper import OperationHelper from app.questionnaire.rules.utils import parse_datetime from app.settings import DEFAULT_LOCALE +from app.utilities.decimal_places import ( + custom_format_decimal, + custom_format_unit, + get_formatted_currency, +) if TYPE_CHECKING: from app.questionnaire.placeholder_renderer import ( @@ -41,11 +50,45 @@ def __init__( input_date_format = "%Y-%m-%d" def format_currency( - self, number: Optional[Union[float, str]] = None, currency: str = "GBP" + self, + number: int | Decimal | float, + unresolved_arguments: Mapping, + currency: str = "GBP", ) -> str: - formatted_currency: str = format_currency(number, currency, locale=self.locale) + """ + The raw arguments for the transform are required here, in addition to the formatted number, as custom logic is required + to calculate the correct number of decimals based on the source of the transform. The decimal only takes into account if the source is + an answer or a calculated summary without any previous transform. + """ + formatted_currency: str = get_formatted_currency( + value=number, + currency=currency, + locale=self.locale, + decimal_limit=self._get_decimal_limit(unresolved_arguments), + ) return formatted_currency + def _get_decimal_limit(self, unresolved_arguments: Mapping) -> int | None: + decimal_limit = None + source = unresolved_arguments["number"].get("source") + identifier = unresolved_arguments["number"].get("identifier") + if source == "answers": + decimal_limit = self.schema.get_decimal_limit([identifier]) + elif source == "calculated_summary": + decimal_limit = self.schema.get_decimal_limit_from_calculated_summaries( + [identifier] + ) + elif source == "grand_calculated_summary": + # Type ignore: Validator will have checked the id so the block is guaranteed to exist + grand_calculated_summary_block: ImmutableDict = self.schema.get_block(identifier) # type: ignore + decimal_limit = self.schema.get_decimal_limit_from_calculated_summaries( + get_calculated_summary_ids_for_grand_calculated_summary( + grand_calculated_summary_block + ) + ) + + return decimal_limit + def format_date(self, date_to_format: str, date_format: str) -> str: date_as_datetime = datetime.strptime( date_to_format, self.input_date_format @@ -56,22 +99,22 @@ def format_date(self, date_to_format: str, date_format: str) -> str: ) return formatted_datetime - @staticmethod - def format_list(list_to_format: Sequence[str]) -> str: - formatted_list = "
    " - for item in list_to_format: - formatted_list += f"
  • {item}
  • " - formatted_list += "
" + def format_list(self, list_to_format: Sequence[str] | None) -> str: + filtered_list = self.remove_empty_from_list(list_to_format or []) + formatted_list = ( + f"
    {''.join(f'
  • {item}
  • ' for item in filtered_list)}
" + if filtered_list + else "" + ) return formatted_list @staticmethod def remove_empty_from_list(list_to_filter: Sequence[str]) -> list[str]: - """ :param list_to_filter: anything that is iterable :return: a list with no empty values - In this filter the following values are considered non empty: + In this filter the following values are considered empty: - None - any empty sequence, for example, '', (), []. - any empty mapping, for example, {}. @@ -97,8 +140,8 @@ def telephone_number_link(self, telephone_number: str) -> str: def email_link( self, email_address: str, - email_subject: Optional[str] = None, - email_subject_append: Optional[str] = None, + email_subject: str | None = None, + email_subject_append: str | None = None, ) -> str: href = f"mailto:{email_address}" if email_subject: @@ -125,16 +168,32 @@ def format_possessive(self, string_to_format: str) -> str: return string_to_format - def format_number(self, number: Union[int, Decimal, str]) -> str: - if number or number == 0: - formatted_decimal: str = format_decimal(number, locale=self.locale) - return formatted_decimal + def format_number(self, number: int | Decimal | float) -> str: + return custom_format_decimal(number, self.locale) - return "" + @staticmethod + def format_percentage(value: int | Decimal | str) -> str: + return f"{value}%" + + def format_unit( + self, + unit: str, + value: int | Decimal, + unit_length: Literal["short", "long", "narrow"] | None = None, + ) -> str: + length = unit_length or "short" + + formatted_unit: str = custom_format_unit( + value=value, + measurement_unit=unit, + length=length, + locale=self.locale, + ) + + return formatted_unit @staticmethod def calculate_date_difference(first_date: str, second_date: str) -> str: - time = relativedelta( parse_datetime(second_date), parse_datetime(first_date), @@ -222,13 +281,12 @@ def format_date_range(self, date_range: tuple[str, str]) -> str: return f"{start_date_formatted} to {end_date_formatted}" @staticmethod - def add(lhs: Union[int, Decimal], rhs: Union[int, Decimal]) -> Union[int, Decimal]: + def add(lhs: int | Decimal, rhs: int | Decimal) -> int | Decimal: return lhs + rhs def format_ordinal( - self, number_to_format: int, determiner: Optional[str] = None + self, number_to_format: int, determiner: str | None = None ) -> str: - indicator = self.get_ordinal_indicator(number_to_format) if determiner == "a_or_an" and self.language in ["en", "eo"]: @@ -268,6 +326,11 @@ def get_ordinal_indicator(self, number_to_format: int) -> str: 19: "eg", }.get(number_to_format, "fed") + language_code_error_message = ( + f"Language code '{self.language}' not implemented." + ) + raise NotImplementedError(language_code_error_message) + def first_non_empty_item(self, items: Sequence[str]) -> str: """ :param items: anything that is iterable @@ -308,7 +371,7 @@ def _create_hyperlink(href: str, link_text: str) -> str: return f'{link_text}' @staticmethod - def list_item_count(list_to_count: Optional[Sized]) -> int: + def list_item_count(list_to_count: Sized | None) -> int: return len(list_to_count or []) def option_label_from_value(self, value: str, answer_id: str) -> str: @@ -319,5 +382,5 @@ def option_label_from_value(self, value: str, answer_id: str) -> str: return self.ops_helper.get_option_label_from_value(value, answer_id) @staticmethod - def conditional_trad_as(trad_as: Optional[str]) -> str: + def conditional_trad_as(trad_as: str | None) -> str: return f" ({trad_as})" if trad_as else "" diff --git a/app/questionnaire/plural_forms.py b/app/questionnaire/plural_forms.py index 1956f3bf85..8fecb25d93 100644 --- a/app/questionnaire/plural_forms.py +++ b/app/questionnaire/plural_forms.py @@ -3,7 +3,7 @@ from app.questionnaire.questionnaire_schema import DEFAULT_LANGUAGE_CODE -def get_plural_form_key(count, language=DEFAULT_LANGUAGE_CODE): +def get_plural_form_key(count: int, language: str = DEFAULT_LANGUAGE_CODE) -> str: mappings = { "en": {"one": "n is 1"}, "cy": { @@ -23,5 +23,6 @@ def get_plural_form_key(count, language=DEFAULT_LANGUAGE_CODE): } plural_rule = PluralRule(mappings[language]) + plural_form: str = plural_rule(count) - return plural_rule(count) + return plural_form diff --git a/app/questionnaire/questionnaire_schema.py b/app/questionnaire/questionnaire_schema.py index 103c870a37..c83a5f4bfd 100644 --- a/app/questionnaire/questionnaire_schema.py +++ b/app/questionnaire/questionnaire_schema.py @@ -1,37 +1,75 @@ -from collections import abc, defaultdict +# pylint: disable=too-many-lines +from collections import defaultdict from copy import deepcopy from dataclasses import dataclass +from decimal import Decimal from functools import cached_property -from typing import Any, Generator, Iterable, Mapping, Optional, Sequence, Union +from typing import Any, Generator, Iterable, Literal, Mapping, Sequence, TypeAlias from flask_babel import force_locale -from werkzeug.datastructures import ImmutableDict +from ordered_set import OrderedSet +from werkzeug.datastructures import ImmutableDict, MultiDict from app.data_models.answer import Answer from app.forms import error_messages from app.questionnaire.rules.operator import OPERATION_MAPPING +from app.questionnaire.schema_utils import get_answers_from_question +from app.settings import MAX_NUMBER +from app.utilities.make_immutable import make_immutable +from app.utilities.mappings import ( + get_flattened_mapping_values, + get_mappings_with_key, + get_values_for_key, +) DEFAULT_LANGUAGE_CODE = "en" +LIST_COLLECTORS_WITH_REPEATING_BLOCKS = {"ListCollector", "ListCollectorContent"} +LIST_COLLECTOR_BLOCKS = { + "ListCollector", + "ListCollectorContent", + "PrimaryPersonListCollector", + "RelationshipCollector", +} + LIST_COLLECTOR_CHILDREN = [ "ListAddQuestion", "ListEditQuestion", "ListRemoveQuestion", "PrimaryPersonListAddOrEditQuestion", + "ListRepeatingQuestion", ] RELATIONSHIP_CHILDREN = ["UnrelatedQuestion"] -QuestionSchema = Mapping[str, Any] +QuestionSchemaType = Mapping + +DependencyDictType: TypeAlias = dict[str, OrderedSet[str]] + +TRANSFORMS_REQUIRING_ROUTING_PATH = {"first_non_empty_item"} +TRANSFORMS_REQUIRING_UNRESOLVED_ARGUMENTS = ["format_currency"] + +NUMERIC_ANSWER_TYPES = { + "Currency", + "Duration", + "Number", + "Percentage", + "Unit", +} class InvalidSchemaConfigurationException(Exception): - pass + def __init__(self, value: str = "No dynamic or static choices") -> None: + super().__init__() + self.value = value + + def __str__(self) -> str: + return str(self.value) @dataclass(frozen=True) -class AnswerDependent: - """Represents a dependent belonging to some answer. +class Dependent: + """Represents a dependent belonging to some answer or list. The dependent can be a reference to another answer, or just the parent block of the answer. If the dependent has an answer_id, then the dependent answer is removed @@ -42,8 +80,8 @@ class AnswerDependent: section_id: str block_id: str - for_list: Optional[str] = None - answer_id: Optional[str] = None + for_list: str | None = None + answer_id: str | None = None class QuestionnaireSchema: # pylint: disable=too-many-public-methods @@ -51,13 +89,44 @@ def __init__( self, questionnaire_json: Mapping, language_code: str = DEFAULT_LANGUAGE_CODE ): self._parent_id_map: dict[str, str] = {} - self._list_name_to_section_map: dict[str, list[str]] = {} - self._answer_dependencies_map: dict[str, set[AnswerDependent]] = defaultdict( - set + self._list_collector_section_ids_by_list_name: dict[str, list[str]] = ( + defaultdict(list) + ) + self._answer_dependencies_map: dict[str, set[Dependent]] = defaultdict(set) + self._list_dependencies_map: dict[str, set[Dependent]] = defaultdict(set) + self._when_rules_section_dependencies_by_section: dict[str, set[str]] = ( + defaultdict(set) + ) + self._when_rules_section_dependencies_by_section_for_progress_value_source: ( + defaultdict[str, OrderedSet[str]] + ) = defaultdict( + OrderedSet + ) + self._when_rules_block_dependencies_by_section_for_progress_value_source: ( + defaultdict[str, DependencyDictType] + ) = defaultdict( + lambda: defaultdict(OrderedSet) + ) + self.calculation_summary_section_dependencies_by_block: dict[ + str, DependencyDictType + ] = defaultdict(lambda: defaultdict(OrderedSet)) + self._when_rules_section_dependencies_by_answer: dict[str, set[str]] = ( + defaultdict(set) ) - self._when_rules_section_dependencies_map: dict[str, set[str]] = {} + self._when_rules_section_dependencies_by_list: dict[str, set[str]] = ( + defaultdict(set) + ) + self._placeholder_transform_section_dependencies_by_block: dict[ + str, dict[str, set[str]] + ] = defaultdict(lambda: defaultdict(set)) self._language_code = language_code self._questionnaire_json = questionnaire_json + self._min_and_max_map: dict[str, dict[str, int]] = defaultdict( + lambda: defaultdict(int) + ) + self._list_names_by_list_repeating_block_id: dict[str, str] = {} + self._repeating_block_answer_ids: set[str] = set() + self.dynamic_answers_parent_block_ids: set[str] = set() # The ordering here is required as they depend on each other. self._sections_by_id = self._get_sections_by_id() @@ -65,18 +134,130 @@ def __init__( self._blocks_by_id = self._get_blocks_by_id() self._questions_by_id = self._get_questions_by_id() self._answers_by_id = self._get_answers_by_id() + self._dynamic_answer_ids: set[str] = set() # Post schema parsing. - self._populate_answer_dependencies() + self._populate_answer_and_list_dependencies() self._populate_when_rules_section_dependencies() + self._populate_calculation_summary_section_dependencies() + self._populate_min_max_for_numeric_answers() + self._populate_placeholder_transform_section_dependencies() + + @property + def placeholder_transform_section_dependencies_by_block( + self, + ) -> dict[str, dict[str, set[str]]]: + return self._placeholder_transform_section_dependencies_by_block @cached_property - def answer_dependencies(self) -> ImmutableDict[str, set[AnswerDependent]]: + def answer_dependencies(self) -> ImmutableDict[str, set[Dependent]]: return ImmutableDict(self._answer_dependencies_map) @cached_property - def when_rules_section_dependencies_map(self) -> ImmutableDict[str, set[str]]: - return ImmutableDict(self._when_rules_section_dependencies_map) + def list_dependencies(self) -> ImmutableDict[str, set[Dependent]]: + return ImmutableDict(self._list_dependencies_map) + + @cached_property + # Type ignore: make_immutable uses generic types so return type is manually specified + def min_and_max_map(self) -> ImmutableDict[str, ImmutableDict[str, int]]: + return make_immutable(self._min_and_max_map) # type: ignore + + def _create_min_max_map( + self, + min_max: Literal["minimum", "maximum"], + answer_id: str, + answers: Iterable[ImmutableDict], + default_min_max: int, + ) -> None: + longest_value_length = 0 + for answer in answers: + value = answer.get(min_max, {}).get("value") + + if isinstance(value, float | int | Decimal): + # Factor out the decimals as it's accounted for in jinja_filters.py + value_length = len(str(int(value))) + + longest_value_length = max(longest_value_length, value_length) + + elif isinstance(value, Mapping) and value: + if value.get("source") == "answers": + longest_value_length = max( + longest_value_length, + self._min_and_max_map[value["identifier"]][min_max], + ) + + self._min_and_max_map[answer_id][min_max] = ( + longest_value_length or default_min_max + ) + + def _populate_min_max_for_numeric_answers(self) -> None: + for answer_id, answers in self._answers_by_id.items(): + # validator ensures all answers will be of the same type so its sufficient to only check the first + if answers[0]["type"] in NUMERIC_ANSWER_TYPES: + self._create_min_max_map("minimum", answer_id, answers, 1) + self._create_min_max_map( + "maximum", answer_id, answers, len(str(MAX_NUMBER)) + ) + + @cached_property + def when_rules_section_dependencies_by_section( + self, + ) -> ImmutableDict[str, set[str]]: + return ImmutableDict(self._when_rules_section_dependencies_by_section) + + @cached_property + def when_rules_section_dependencies_for_progress( + self, + ) -> ImmutableDict[str, set[str]]: + """ + This method flips the dependencies that were captured for progress value sources so that they can be + evaluated properly for when rules, this is because for when rules we need to check for dependencies in + previous sections, whereas for progress we are checking for dependent blocks/sections in "future" sections + """ + when_rules_section_dependencies_for_progress = defaultdict(set) + for ( + section, + dependent, + ) in ( + self._when_rules_block_dependencies_by_section_for_progress_value_source.items() + ): + section_dependents = get_flattened_mapping_values(dependent) + for dependent_section in section_dependents: + when_rules_section_dependencies_for_progress[dependent_section].add( + section + ) + + for ( + section, + dependents, + ) in ( + self._when_rules_section_dependencies_by_section_for_progress_value_source.items() + ): + for dependent_section in dependents: + when_rules_section_dependencies_for_progress[dependent_section].add( + section + ) + return ImmutableDict(when_rules_section_dependencies_for_progress) + + @cached_property + def when_rules_section_dependencies_by_section_for_progress_value_source( + self, + ) -> ImmutableDict[str, OrderedSet[str]]: + return ImmutableDict( + self._when_rules_section_dependencies_by_section_for_progress_value_source + ) + + @cached_property + def when_rules_block_dependencies_by_section_for_progress_value_source( + self, + ) -> ImmutableDict[str, DependencyDictType]: + return ImmutableDict( + self._when_rules_block_dependencies_by_section_for_progress_value_source + ) + + @cached_property + def when_rules_section_dependencies_by_answer(self) -> ImmutableDict[str, set[str]]: + return ImmutableDict(self._when_rules_section_dependencies_by_answer) @cached_property def language_code(self) -> str: @@ -91,51 +272,54 @@ def json(self) -> Any: return self.serialize(self._questionnaire_json) @cached_property - def survey(self) -> Optional[str]: - survey: Optional[str] = self.json.get("survey") + def survey(self) -> str | None: + survey: str | None = self.json.get("survey") return survey @cached_property - def form_type(self) -> Optional[str]: - form_type: Optional[str] = self.json.get("form_type") + def form_type(self) -> str | None: + form_type: str | None = self.json.get("form_type") return form_type @cached_property - def region_code(self) -> Optional[str]: - region_code: Optional[str] = self.json.get("region_code") + def region_code(self) -> str | None: + region_code: str | None = self.json.get("region_code") return region_code + @cached_property + def preview_enabled(self) -> bool: + preview_enabled: bool = self.json.get("preview_questions", False) + return preview_enabled + @cached_property def parent_id_map(self) -> Any: return self.serialize(self._parent_id_map) + @cached_property + def supplementary_lists(self) -> frozenset[str]: + return frozenset(self.json.get("supplementary_data", {}).get("lists", [])) + @classmethod def serialize(cls, data: Any) -> Any: - if isinstance(data, abc.Hashable): - return data - if isinstance(data, list): - return tuple((cls.serialize(item) for item in data)) - if isinstance(data, dict): - key_value_tuples = {k: cls.serialize(v) for k, v in data.items()} - return ImmutableDict(key_value_tuples) + return make_immutable(data) @classmethod def get_mutable_deepcopy(cls, data: Any) -> Any: if isinstance(data, tuple): - return list((cls.get_mutable_deepcopy(item) for item in data)) + return [cls.get_mutable_deepcopy(item) for item in data] if isinstance(data, ImmutableDict): key_value_tuples = {k: cls.get_mutable_deepcopy(v) for k, v in data.items()} return dict(key_value_tuples) return deepcopy(data) @cached_property - def _flow(self) -> ImmutableDict[str, Any]: + def _flow(self) -> ImmutableDict: questionnaire_flow: ImmutableDict = self.json["questionnaire_flow"] return questionnaire_flow @cached_property - def flow_options(self) -> ImmutableDict[str, Any]: - options: ImmutableDict[str, Any] = self._flow["options"] + def flow_options(self) -> ImmutableDict: + options: ImmutableDict = self._flow["options"] return options @cached_property @@ -152,8 +336,42 @@ def is_view_submitted_response_enabled(self) -> bool: is_enabled: bool = schema.get("view_response", False) return is_enabled + @cached_property + def list_names_by_list_repeating_block_id(self) -> ImmutableDict[str, str]: + return ImmutableDict(self._list_names_by_list_repeating_block_id) + + @cached_property + def list_collector_repeating_block_ids(self) -> list[str]: + return list(self._list_names_by_list_repeating_block_id.keys()) + + @cached_property + def list_collector_section_ids_by_list_name(self) -> ImmutableDict[str, tuple[str]]: + # Type ignore: make_immutable is generic so type is manually specified + return make_immutable(self._list_collector_section_ids_by_list_name) # type: ignore + + def get_all_when_rules_section_dependencies_for_section( + self, section_id: str + ) -> set[str]: + all_section_dependencies = self.when_rules_section_dependencies_by_section.get( + section_id, set() + ) + + if progress_dependencies := self.when_rules_section_dependencies_for_progress.get( + section_id + ): + all_section_dependencies.update(progress_dependencies) + + return all_section_dependencies + + def get_when_rule_section_dependencies_for_list(self, list_name: str) -> set[str]: + """Gets the set of all sections which reference the list in a when rule somewhere""" + return self._when_rules_section_dependencies_by_list.get(list_name, set()) + def _get_sections_by_id(self) -> dict[str, ImmutableDict]: - return {section["id"]: section for section in self.json.get("sections", [])} + return { + section["id"]: section + for section in self.json.get("sections", ImmutableDict({})) + } def _get_groups_by_id(self) -> dict[str, ImmutableDict]: groups_by_id: dict[str, ImmutableDict] = {} @@ -173,13 +391,11 @@ def _get_blocks_by_id(self) -> dict[str, ImmutableDict]: for block in group["blocks"]: block_id = block["id"] self._parent_id_map[block_id] = group["id"] - blocks[block_id] = block - if block["type"] in ( - "ListCollector", - "PrimaryPersonListCollector", - "RelationshipCollector", - ): + if block["type"] in LIST_COLLECTOR_BLOCKS: + self._list_collector_section_ids_by_list_name[ + block["for_list"] + ].append(self._parent_id_map[group["id"]]) for nested_block_name in [ "add_block", "edit_block", @@ -192,6 +408,14 @@ def _get_blocks_by_id(self) -> dict[str, ImmutableDict]: nested_block_id = nested_block["id"] blocks[nested_block_id] = nested_block self._parent_id_map[nested_block_id] = block_id + if repeating_blocks := block.get("repeating_blocks"): + for repeating_block in repeating_blocks: + repeating_block_id = repeating_block["id"] + blocks[repeating_block_id] = repeating_block + self._parent_id_map[repeating_block_id] = block_id + self._list_names_by_list_repeating_block_id[ + repeating_block_id + ] = block["for_list"] return blocks @@ -212,9 +436,16 @@ def _get_answers_by_id(self) -> dict[str, list[ImmutableDict]]: for question in self._get_flattened_questions(): question_id = question["id"] - for answer in question["answers"]: + is_for_repeating_block = ( + self._parent_id_map[question_id] + in self.list_collector_repeating_block_ids + ) + + for answer in get_answers_from_question(question): answer_id = answer["id"] self._parent_id_map[answer_id] = question_id + if is_for_repeating_block: + self._repeating_block_answer_ids.add(answer_id) answers_by_id[answer_id].append(answer) for option in answer.get("options", []): @@ -226,126 +457,233 @@ def _get_answers_by_id(self) -> dict[str, list[ImmutableDict]]: return answers_by_id - def _populate_answer_dependencies(self) -> None: + def _populate_answer_and_list_dependencies(self) -> None: for block in self.get_blocks(): - if block["type"] == "CalculatedSummary": - self._update_answer_dependencies_for_calculated_summary( - block["calculation"]["answers_to_calculate"], block["id"] - ) + if block["type"] in {"CalculatedSummary", "GrandCalculatedSummary"}: + self._update_dependencies_for_summary(block) continue + + if block["type"] == "ListCollectorContent" and block.get( + "repeating_blocks" + ): + # Editable list collectors don't need this because the add/remove handlers manage revisiting repeating blocks + self._list_dependencies_map[block["for_list"]].add( + self._get_dependent_for_block_id(block_id=block["id"]) + ) + for question in self.get_all_questions_for_block(block): + self.update_dependencies_for_dynamic_answers( + question=question, block_id=block["id"] + ) + if question["type"] == "Calculated": - self._update_answer_dependencies_for_calculations( - question["calculations"], + self._update_dependencies_for_calculations( + question["calculations"], block_id=block["id"] ) continue for answer in question.get("answers", []): - self._update_answer_dependencies_for_answer( - answer, block_id=block["id"] - ) + self._update_dependencies_for_answer(answer, block_id=block["id"]) for option in answer.get("options", []): if "detail_answer" in option: - self._update_answer_dependencies_for_answer( + self._update_dependencies_for_answer( option["detail_answer"], block_id=block["id"] ) - def _update_answer_dependencies_for_calculated_summary( - self, calculated_summary_answer_ids: list[str], block_id: str + def _update_dependencies_for_summary(self, block: ImmutableDict) -> None: + if block["type"] == "CalculatedSummary": + self._update_dependencies_for_calculated_summary_dependency( + calculated_summary_block=block, dependent_block=block + ) + elif block["type"] == "GrandCalculatedSummary": + self._update_dependencies_for_grand_calculated_summary(block) + + def _update_dependencies_for_calculated_summary_dependency( + self, *, calculated_summary_block: ImmutableDict, dependent_block: ImmutableDict ) -> None: + """ + For a block that depends on a calculated summary block, add the block as a dependency of each of the calculated summary answers + Similarly if the calculated summary depends on a list, then add the block as a dependency of the list + """ + calculated_summary_answer_ids = get_calculated_summary_answer_ids( + calculated_summary_block + ) + dependent = self._get_dependent_for_block_id(block_id=dependent_block["id"]) for answer_id in calculated_summary_answer_ids: - self._answer_dependencies_map[answer_id] |= { - self._get_answer_dependent_for_block_id(block_id=block_id) - } + if list_name := self.get_list_name_for_answer_id( + answer_id, value_source_update=False + ): + # dynamic/repeating answers means the calculated summary also depends on the list those answers loop over + self._list_dependencies_map[list_name].add(dependent) + self._answer_dependencies_map[answer_id].add(dependent) - def _update_answer_dependencies_for_calculations( - self, - calculations: tuple[ImmutableDict[str, Any]], + def _update_dependencies_for_grand_calculated_summary( + self, grand_calculated_summary_block: ImmutableDict + ) -> None: + grand_calculated_summary_calculated_summary_ids = ( + get_calculated_summary_ids_for_grand_calculated_summary( + grand_calculated_summary_block + ) + ) + for calculated_summary_id in grand_calculated_summary_calculated_summary_ids: + # Type ignore: safe to assume block exists + calculated_summary_block: ImmutableDict = self.get_block(calculated_summary_id) # type: ignore + self._update_dependencies_for_calculated_summary_dependency( + calculated_summary_block=calculated_summary_block, + dependent_block=grand_calculated_summary_block, + ) + + def _update_dependencies_for_calculations( + self, calculations: tuple[ImmutableDict, ...], *, block_id: str ) -> None: for calculation in calculations: - if not (source_answer_id := calculation.get("answer_id")): - continue - dependents = { - self._get_answer_dependent_for_block_id( - block_id=self.get_block_for_answer_id(answer_id)["id"] # type: ignore + if source_answer_id := calculation.get("answer_id"): + dependents = { + self._get_dependent_for_block_id( + block_id=self.get_block_for_answer_id(answer_id)["id"] # type: ignore + ) + for answer_id in calculation["answers_to_calculate"] + } + self._answer_dependencies_map[source_answer_id] |= dependents + + elif isinstance(calculation.get("value"), dict): + self._update_dependencies_for_value_source( + calculation["value"], + block_id=block_id, ) - for answer_id in calculation["answers_to_calculate"] - } - self._answer_dependencies_map[source_answer_id] |= dependents - def _update_answer_dependencies_for_answer( + def _update_dependencies_for_answer( self, answer: Mapping, *, block_id: str ) -> None: for key in ["minimum", "maximum"]: value = answer.get(key, {}).get("value") if isinstance(value, dict): - self._update_answer_dependencies_for_value_source( - value, block_id=block_id + self._update_dependencies_for_value_source( + value, + block_id=block_id, ) if dynamic_options_values := answer.get("dynamic_options", {}).get("values"): - self._update_answer_dependencies_for_dynamic_options( + self._update_dependencies_for_dynamic_options( dynamic_options_values, block_id=block_id, answer_id=answer["id"] ) - def _update_answer_dependencies_for_dynamic_options( - self, dynamic_options_values: Mapping, *, block_id: str, answer_id: str + def _update_dependencies_for_dynamic_options( + self, + dynamic_options_values: Mapping, + *, + block_id: str, + answer_id: str, ) -> None: - value_sources = self._get_dictionaries_with_key( - "source", dynamic_options_values - ) + value_sources = get_mappings_with_key("source", data=dynamic_options_values) for value_source in value_sources: - self._update_answer_dependencies_for_value_source( + self._update_dependencies_for_value_source( value_source, block_id=block_id, answer_id=answer_id ) - def _update_answer_dependencies_for_value_source( - self, value_source: Mapping, *, block_id: str, answer_id: Optional[str] = None + def _update_dependencies_for_calculated_summary_value_source( + self, *, calculated_summary_id: str, block_id: str, answer_id: str | None + ) -> None: + """ + For the given block (and optionally answer within) set it as a dependency of each calculated summary answer + If the calculated summary depends on a list, make the block depend on it too + """ + # Type ignore: validator checks the validity of value sources. + calculated_summary_block: ImmutableDict = self.get_block(calculated_summary_id) # type: ignore + answer_ids_for_block = get_calculated_summary_answer_ids( + calculated_summary_block + ) + dependent = self._get_dependent_for_block_id( + block_id=block_id, answer_id=answer_id + ) + for answer_id_for_block in answer_ids_for_block: + self._answer_dependencies_map[answer_id_for_block].add(dependent) + # if the answer is repeating, then the calculated summary also depends on the list it loops over + if list_name := self.get_list_name_for_answer_id(answer_id_for_block): + self._list_dependencies_map[list_name].add(dependent) + + def _update_dependencies_for_value_source( + self, + value_source: Mapping, + *, + block_id: str, + answer_id: str | None = None, ) -> None: + """ + For a given value source, get the answer ids it consists of, or the list it references, + and add the given block (and optionally answer) as a dependency of those answers or lists + """ if value_source["source"] == "answers": self._answer_dependencies_map[value_source["identifier"]] |= { - self._get_answer_dependent_for_block_id(block_id=block_id, answer_id=answer_id) # type: ignore + self._get_dependent_for_block_id(block_id=block_id, answer_id=answer_id) } + elif value_source["source"] == "calculated_summary": + identifier = value_source["identifier"] + self._update_dependencies_for_calculated_summary_value_source( + calculated_summary_id=identifier, block_id=block_id, answer_id=answer_id + ) - def _get_answer_dependent_for_block_id( - self, *, block_id: str, answer_id: Optional[str] = None - ) -> AnswerDependent: + elif value_source["source"] == "grand_calculated_summary": + identifier = value_source["identifier"] + # Type ignore: validator will ensure identifier is valid + grand_calculated_summary_block: ImmutableDict = self.get_block(identifier) # type: ignore + for ( + calculated_summary_id + ) in get_calculated_summary_ids_for_grand_calculated_summary( + grand_calculated_summary_block + ): + self._update_dependencies_for_calculated_summary_value_source( + calculated_summary_id=calculated_summary_id, + block_id=block_id, + answer_id=answer_id, + ) + + if value_source["source"] == "list": + self._list_dependencies_map[value_source["identifier"]].add( + self._get_dependent_for_block_id(block_id=block_id) + ) + + def _get_dependent_for_block_id( + self, + *, + block_id: str, + answer_id: str | None = None, + for_list: str | None = None, + ) -> Dependent: section_id: str = self.get_section_id_for_block_id(block_id) # type: ignore - for_list = self.get_repeating_list_for_section(section_id) + if not for_list: + for_list = self.get_repeating_list_for_section(section_id) - return AnswerDependent( + return Dependent( block_id=block_id, section_id=section_id, for_list=for_list, answer_id=answer_id, ) - def _get_flattened_questions(self) -> list[ImmutableDict[str, Any]]: + def _get_flattened_questions(self) -> list[ImmutableDict]: return [ question for questions in self._questions_by_id.values() for question in questions ] - def get_section_ids_required_for_hub(self) -> list[str]: - return self.flow_options.get("required_completed_sections", []) + def get_section_ids_required_for_hub(self) -> tuple[str, ...]: + # Type ignore: the type of the .get() returned value is Any + return self.flow_options.get("required_completed_sections", tuple()) # type: ignore - def get_summary_options(self) -> ImmutableDict[str, Any]: - return self.flow_options.get("summary", {}) + def get_summary_options(self) -> ImmutableDict[str, bool]: + # Type ignore: the type of the .get() returned value is Any + return self.flow_options.get("summary", ImmutableDict({})) # type: ignore def get_sections(self) -> Iterable[ImmutableDict]: return self._sections_by_id.values() - def get_section(self, section_id: str) -> Optional[ImmutableDict]: - return self._sections_by_id.get(section_id) + def get_section_ids(self) -> Iterable[str]: + return self._sections_by_id.keys() - def get_section_ids_dependent_on_list(self, list_name: str) -> list[str]: - try: - return self._list_name_to_section_map[list_name] - except KeyError: - section_ids = self._section_ids_associated_to_list_name(list_name) - self._list_name_to_section_map[list_name] = section_ids - return section_ids + def get_section(self, section_id: str) -> ImmutableDict | None: + return self._sections_by_id.get(section_id) def get_submission(self) -> ImmutableDict: schema: ImmutableDict = self.json.get("submission", ImmutableDict({})) @@ -355,50 +693,12 @@ def get_post_submission(self) -> ImmutableDict: schema: ImmutableDict = self.json.get("post_submission", ImmutableDict({})) return schema - def _is_list_name_in_rule( - self, rules: Union[Mapping, Sequence], list_name: str - ) -> bool: - if isinstance(rules, Mapping) and any( - operator in rules for operator in OPERATION_MAPPING - ): - rules = self.get_operands(rules) - - for rule in rules: - if not isinstance(rule, Mapping): - continue - - # Old rules - if "list" in rule: - return rule.get("list") == list_name - - # New rules - if "source" in rule: - return ( - rule.get("source") == "list" and rule.get("identifier") == list_name - ) - - # Nested rules - if any(operator in rule for operator in OPERATION_MAPPING): - return self._is_list_name_in_rule(rule, list_name) - @staticmethod - def get_operands(rules: Mapping) -> list: + def get_operands(rules: Mapping) -> Sequence: operator = next(iter(rules)) - operands: list = rules[operator] + operands: Sequence = rules[operator] return operands - def _section_ids_associated_to_list_name(self, list_name: str) -> list[str]: - section_ids: list[str] = [] - - for section in self.get_sections(): - ignore_keys = ["question_variants", "content_variants"] - when_rules = self._get_values_for_key(section, "when", ignore_keys) - - rule: Union[Mapping, list] = next(when_rules, []) - if self._is_list_name_in_rule(rule, list_name): - section_ids.append(section["id"]) - return section_ids - @staticmethod def get_blocks_for_section( section: Mapping, @@ -408,7 +708,7 @@ def get_blocks_for_section( @classmethod def get_driving_question_for_list( cls, section: Mapping, list_name: str - ) -> Optional[ImmutableDict]: + ) -> ImmutableDict | None: for block in cls.get_blocks_for_section(section): if ( block["type"] == "ListCollectorDrivingQuestion" @@ -416,16 +716,17 @@ def get_driving_question_for_list( ): return block - def get_remove_block_id_for_list(self, list_name: str) -> Optional[str]: + def get_remove_block_id_for_list(self, list_name: str) -> str | None: for block in self.get_blocks(): - if block["type"] == "ListCollector" and block["for_list"] == list_name: + if ( + is_list_collector_block_editable(block) + and block["for_list"] == list_name + ): remove_block_id: str = block["remove_block"]["id"] return remove_block_id - def get_individual_response_list(self) -> Optional[str]: - list_name: Optional[str] = self.json.get("individual_response", {}).get( - "for_list" - ) + def get_individual_response_list(self) -> str | None: + list_name: str | None = self.json.get("individual_response", {}).get("for_list") return list_name def get_individual_response_show_on_hub(self) -> bool: @@ -434,55 +735,56 @@ def get_individual_response_show_on_hub(self) -> bool: ) return show_on_hub - def get_individual_response_individual_section_id(self) -> Optional[str]: - section_id: Optional[str] = self._questionnaire_json.get( + def get_individual_response_individual_section_id(self) -> str | None: + section_id: str | None = self._questionnaire_json.get( "individual_response", {} ).get("individual_section_id") return section_id - def get_title_for_section(self, section_id: str) -> Optional[str]: + def get_title_for_section(self, section_id: str) -> str | None: if section := self.get_section(section_id): return section.get("title") - def get_show_on_hub_for_section(self, section_id: str) -> Optional[bool]: + def get_show_on_hub_for_section(self, section_id: str) -> bool | None: + # Type ignore: the type of the .get() returned value is Any if section := self.get_section(section_id): - return section.get("show_on_hub", True) + return section.get("show_on_hub", True) # type: ignore - def get_summary_for_section(self, section_id: str) -> Optional[ImmutableDict]: + def get_summary_for_section(self, section_id: str) -> ImmutableDict | None: if section := self.get_section(section_id): return section.get("summary") - def get_summary_title_for_section(self, section_id: str) -> Optional[str]: + def get_summary_title_for_section(self, section_id: str) -> str | None: if summary := self.get_summary_for_section(section_id): return summary.get("title") - def show_summary_on_completion_for_section(self, section_id: str) -> Optional[bool]: + def show_summary_on_completion_for_section(self, section_id: str) -> bool | None: if summary := self.get_summary_for_section(section_id): - return summary.get("show_on_completion", False) + # Type ignore: the type of the .get() returned value is Any + return summary.get("show_on_completion", False) # type: ignore - def get_repeat_for_section(self, section_id: str) -> Optional[ImmutableDict]: + def get_repeat_for_section(self, section_id: str) -> ImmutableDict | None: if section := self.get_section(section_id): return section.get("repeat") - def get_repeating_list_for_section(self, section_id: str) -> Optional[str]: + def get_repeating_list_for_section(self, section_id: str) -> str | None: if repeat := self.get_repeat_for_section(section_id): return repeat.get("for_list") - def get_repeating_title_for_section( - self, section_id: str - ) -> Optional[ImmutableDict]: + def get_repeating_title_for_section(self, section_id: str) -> ImmutableDict | None: if repeat := self.get_repeat_for_section(section_id): - return repeat.get("title") + title: ImmutableDict = repeat["title"] + return title - def get_repeating_page_title_for_section(self, section_id: str) -> Optional[str]: + def get_repeating_page_title_for_section(self, section_id: str) -> str | None: if repeat := self.get_repeat_for_section(section_id): return repeat.get("page_title") - def get_custom_page_title_for_section(self, section_id: str) -> Optional[str]: + def get_custom_page_title_for_section(self, section_id: str) -> str | None: if summary := self.get_summary_for_section(section_id): return summary.get("page_title") - def get_section_for_block_id(self, block_id: str) -> Optional[ImmutableDict]: + def get_section_for_block_id(self, block_id: str) -> ImmutableDict | None: block = self.get_block(block_id) if ( @@ -496,7 +798,7 @@ def get_section_for_block_id(self, block_id: str) -> Optional[ImmutableDict]: return self.get_section(section_id) - def get_section_id_for_block_id(self, block_id: str) -> Optional[str]: + def get_section_id_for_block_id(self, block_id: str) -> str | None: if section := self.get_section_for_block_id(block_id): section_id: str = section["id"] return section_id @@ -504,48 +806,56 @@ def get_section_id_for_block_id(self, block_id: str) -> Optional[str]: def get_groups(self) -> Iterable[ImmutableDict]: return self._groups_by_id.values() - def get_group(self, group_id: str) -> Optional[ImmutableDict]: + def get_group(self, group_id: str) -> ImmutableDict | None: return self._groups_by_id.get(group_id) - def get_group_for_block_id(self, block_id: str) -> Optional[ImmutableDict]: + def get_group_for_block_id(self, block_id: str) -> ImmutableDict | None: return self._group_for_block(block_id) - def get_first_block_id_for_group(self, group_id: str) -> Optional[str]: - group = self.get_group(group_id) - if group: + def get_first_block_id_for_group(self, group_id: str) -> str | None: + if group := self.get_group(group_id): block_id: str = group["blocks"][0]["id"] return block_id - def get_first_block_id_for_section(self, section_id: str) -> Optional[str]: - section = self.get_section(section_id) - if section: + def get_first_block_id_for_section(self, section_id: str) -> str | None: + if section := self.get_section(section_id): group_id: str = section["groups"][0]["id"] return self.get_first_block_id_for_group(group_id) def get_blocks(self) -> Iterable[ImmutableDict]: return self._blocks_by_id.values() - def get_block(self, block_id: str) -> Optional[ImmutableDict]: + def get_block(self, block_id: str) -> ImmutableDict | None: return self._blocks_by_id.get(block_id) def is_block_valid(self, block_id: str) -> bool: return bool(self.get_block(block_id)) - def get_block_for_answer_id(self, answer_id: str) -> Optional[ImmutableDict]: + def get_block_for_answer_id(self, answer_id: str) -> ImmutableDict | None: return self._block_for_answer(answer_id) - def is_block_in_repeating_section(self, block_id: str) -> Optional[bool]: + def is_block_in_repeating_section(self, block_id: str) -> bool | None: if section_id := self.get_section_id_for_block_id(block_id=block_id): return bool(self.get_repeating_list_for_section(section_id)) - def is_answer_in_list_collector_block(self, answer_id: str) -> Optional[bool]: + def is_answer_in_list_collector_block(self, answer_id: str) -> bool | None: if block := self.get_block_for_answer_id(answer_id): return self.is_list_block_type(block["type"]) - def is_answer_in_repeating_section(self, answer_id: str) -> Optional[bool]: + def is_answer_in_repeating_section(self, answer_id: str) -> bool | None: if block := self.get_block_for_answer_id(answer_id): return self.is_block_in_repeating_section(block_id=block["id"]) + def is_answer_dynamic(self, answer_id: str) -> bool: + return answer_id in self._dynamic_answer_ids + + def is_answer_in_list_collector_repeating_block(self, answer_id: str) -> bool: + return answer_id in self._repeating_block_answer_ids + + def get_list_name_for_dynamic_answer(self, block_id: str) -> str: + # type ignore block always exists at this point + return self.get_block(block_id)["question"]["dynamic_answers"]["values"]["identifier"] # type: ignore + def is_repeating_answer( self, answer_id: str, @@ -553,8 +863,29 @@ def is_repeating_answer( return bool( self.is_answer_in_list_collector_block(answer_id) or self.is_answer_in_repeating_section(answer_id) + or self.is_answer_dynamic(answer_id) ) + def get_list_name_for_answer_id( + self, answer_id: str, value_source_update: bool = True + ) -> str | None: + """ + if the answer is updated for calculated summary value source, return the name of the list, if updated for calculated summary dependency and the answer + is part of a repeating section, return None. + """ + # Type ignore: safe to assume block exists, same for section below. + block: ImmutableDict = self.get_block_for_answer_id(answer_id) # type: ignore + block_id: str = block["id"] + if self.is_answer_dynamic(answer_id): + return self.get_list_name_for_dynamic_answer(block_id) + if self.is_answer_in_list_collector_repeating_block(answer_id): + return self.list_names_by_list_repeating_block_id[block_id] + if self.is_answer_in_list_collector_block(answer_id): + return block["for_list"] # type: ignore + if self.is_answer_in_repeating_section(answer_id) and value_source_update: + section_id: str = self.get_section_id_for_block_id(block_id) # type: ignore + return self.get_repeating_list_for_section(section_id) + def get_answers_by_answer_id(self, answer_id: str) -> list[ImmutableDict]: """Return answers matching answer id, including all matching answers inside variants @@ -562,7 +893,7 @@ def get_answers_by_answer_id(self, answer_id: str) -> list[ImmutableDict]: answers: list[ImmutableDict] = self._answers_by_id.get(answer_id, []) return answers - def get_default_answer(self, answer_id: str) -> Optional[Answer]: + def get_default_answer(self, answer_id: str) -> Answer | None: if answer_schemas := self.get_answers_by_answer_id(answer_id): first_answer_schema = answer_schemas[0] try: @@ -573,77 +904,150 @@ def get_default_answer(self, answer_id: str) -> Optional[Answer]: def get_add_block_for_list_collector( self, list_collector_id: str - ) -> Optional[ImmutableDict]: - add_block_map = { - "ListCollector": "add_block", - "PrimaryPersonListCollector": "add_or_edit_block", - } + ) -> ImmutableDict | None: if list_collector := self.get_block(list_collector_id): - add_block: ImmutableDict = list_collector[ - add_block_map[list_collector["type"]] - ] + add_block_map = { + "ListCollector": "add_block", + "PrimaryPersonListCollector": "add_or_edit_block", + } + add_block: ImmutableDict | None = list_collector.get( + add_block_map.get(list_collector["type"]) + ) return add_block - def get_answer_ids_for_list_items( + def get_edit_block_for_list_collector( + self, list_collector_id: str + ) -> ImmutableDict | None: + # Type ignore: for any valid list collector id, list collector block will always exist + return self.get_block(list_collector_id).get("edit_block") # type: ignore + + def get_repeating_blocks_for_list_collector( self, list_collector_id: str - ) -> Optional[list[str]]: + ) -> list[ImmutableDict] | None: + if list_collector := self.get_block(list_collector_id): + # Type ignore: the type of the .get() returned value is Any + return list_collector.get("repeating_blocks", []) # type: ignore + + def get_answer_ids_for_list_items(self, list_collector_id: str) -> list[str]: """ - Get answer ids used to add items to a list. + Get answer ids used to add items to a list, including any repeating block answers if any exist. """ + answer_ids = [] if add_block := self.get_add_block_for_list_collector(list_collector_id): - return self.get_answer_ids_for_block(add_block["id"]) + answer_ids.extend(self.get_answer_ids_for_block(add_block["id"])) + if repeating_blocks := self.get_repeating_blocks_for_list_collector( + list_collector_id + ): + for repeating_block in repeating_blocks: + answer_ids.extend(self.get_answer_ids_for_block(repeating_block["id"])) + return answer_ids - def get_questions(self, question_id: str) -> Optional[list[ImmutableDict]]: + def get_questions(self, question_id: str) -> list[ImmutableDict] | None: """Return a list of questions matching some question id This includes all questions inside variants """ return self._questions_by_id.get(question_id) - @staticmethod - def get_list_collectors_for_list( - section: Mapping, for_list: str, primary: bool = False - ) -> Generator[ImmutableDict, None, None]: - collector_type = "PrimaryPersonListCollector" if primary else "ListCollector" + def get_list_collectors_for_list_for_sections( + self, sections: list[str], for_list: str, primary: bool = False + ) -> list[ImmutableDict]: + blocks: list[ImmutableDict] = [] + for section_id in sections: + if section := self.get_section(section_id): + collector_type = ( + {"PrimaryPersonListCollector"} + if primary + else LIST_COLLECTORS_WITH_REPEATING_BLOCKS + ) - return ( - block - for block in QuestionnaireSchema.get_blocks_for_section(section) - if block["type"] == collector_type and block["for_list"] == for_list + blocks.extend( + block + for block in self.get_blocks_for_section(section) + if block["type"] in collector_type and block["for_list"] == for_list + ) + + return blocks + + def get_list_collectors_for_list( + self, for_list: str, primary: bool = False, section_id: str | None = None + ) -> list[ImmutableDict]: + sections = ( + [section_id] + if section_id + else self._list_collector_section_ids_by_list_name[for_list] ) - @staticmethod - def get_list_collector_for_list( - section: Mapping, for_list: str, primary: bool = False - ) -> Optional[ImmutableDict]: - try: - return next( - QuestionnaireSchema.get_list_collectors_for_list( - section, for_list, primary - ) - ) - except StopIteration: - return None + return self.get_list_collectors_for_list_for_sections( + sections, for_list, primary + ) @classmethod - def get_answer_ids_for_question(cls, question: Mapping) -> list[str]: - answer_ids: list[str] = [] - - for answer in question.get("answers", []): - answer_ids.append(answer["id"]) - for option in answer.get("options", []): + def get_answers_for_question_by_id( + cls, question: QuestionSchemaType + ) -> dict[str, dict]: + answers: dict[str, dict] = {} + + for answer in get_answers_from_question(question): + answers[answer["id"]] = answer + for option in answer.get("options", {}): if "detail_answer" in option: - answer_ids.append(option["detail_answer"]["id"]) + answers[option["detail_answer"]["id"]] = option["detail_answer"] - return answer_ids + return answers + + @classmethod + def get_answer_ids_for_question(cls, question: QuestionSchemaType) -> list[str]: + return list(cls.get_answers_for_question_by_id(question).keys()) def get_first_answer_id_for_block(self, block_id: str) -> str: answer_ids = self.get_answer_ids_for_block(block_id) return answer_ids[0] - def get_answer_ids_for_block(self, block_id: str) -> list[str]: - block = self.get_block(block_id) + def get_answer_format_for_calculated_summary( + self, calculated_summary_block_id: str + ) -> dict: + """ + Given a calculated summary block id, find the format of the total by using the first answer + """ + # Type ignore: the block will exist for any valid calculated summary id + calculated_summary_block: ImmutableDict = self.get_block(calculated_summary_block_id) # type: ignore + answer_ids = get_calculated_summary_answer_ids(calculated_summary_block) + decimal_limit = self.get_decimal_limit(answer_ids) + first_answer_id = answer_ids[0] + first_answer = self.get_answers_by_answer_id(first_answer_id)[0] + return { + "type": first_answer["type"].lower(), + "unit": first_answer.get("unit"), + "unit_length": first_answer.get("unit_length"), + "currency": first_answer.get("currency"), + "decimal_places": decimal_limit, + } - if block: + def get_decimal_limit_from_calculated_summaries( + self, calculated_summary_block_ids: list[str] + ) -> int | None: + """ + Get the max number of decimal places from the calculated summary block(s) passed in + """ + decimal_limits: list[int] = [] + for calculated_summary_id in calculated_summary_block_ids: + # Type ignore: the block will exist for any valid calculated summary id + answer_ids = get_calculated_summary_answer_ids(self.get_block(calculated_summary_id)) # type: ignore + if (decimal_limit := self.get_decimal_limit(answer_ids)) is not None: + decimal_limits.append(decimal_limit) + return max(decimal_limits, default=None) + + def get_decimal_limit(self, answer_ids: list[str]) -> int | None: + decimal_limits: list[int] = [ + decimal_places + for answer_id in answer_ids + for answer in self.get_answers_by_answer_id(answer_id) + if (decimal_places := answer.get("decimal_places")) is not None + ] + return max(decimal_limits, default=None) + + def get_answer_ids_for_block(self, block_id: str) -> list[str]: + if block := self.get_block(block_id): if block.get("question"): return self.get_answer_ids_for_question(block["question"]) if block.get("question_variants"): @@ -661,7 +1065,7 @@ def get_relationship_collectors(self) -> list[ImmutableDict]: def get_relationship_collectors_by_list_name( self, list_name: str - ) -> Optional[list[ImmutableDict]]: + ) -> list[ImmutableDict] | None: relationship_collectors = self.get_relationship_collectors() if relationship_collectors: return [ @@ -672,7 +1076,7 @@ def get_relationship_collectors_by_list_name( def get_unrelated_block_no_answer_values( self, unrelated_answer_id: str - ) -> Optional[list[str]]: + ) -> list[str] | None: if unrelated_answers := self.get_answers_by_answer_id(unrelated_answer_id): return [ option["value"] @@ -682,7 +1086,7 @@ def get_unrelated_block_no_answer_values( ] @staticmethod - def get_single_string_value(schema_object: Union[Mapping, str]) -> str: + def get_single_string_value(schema_object: Mapping | str) -> str: """ Resolves an identifying string value for the schema_object. If text_plural the `other` form is returned. :return: string value @@ -706,9 +1110,9 @@ def get_all_questions_for_block(block: Mapping) -> list[ImmutableDict]: if block.get("question"): all_questions.append(block["question"]) elif block.get("question_variants"): - for variant in block["question_variants"]: - all_questions.append(variant["question"]) - + all_questions.extend( + variant["question"] for variant in block["question_variants"] + ) return all_questions return [] @@ -728,11 +1132,12 @@ def is_list_block_type(block_type: str) -> bool: @staticmethod def is_question_block_type(block_type: str) -> bool: - return block_type in [ + return block_type in { "Question", "ListCollectorDrivingQuestion", "ConfirmationQuestion", - ] + "ListRepeatingQuestion", + } @staticmethod def has_address_lookup_answer(question: Mapping) -> bool: @@ -742,35 +1147,11 @@ def has_address_lookup_answer(question: Mapping) -> bool: if answer["type"] == "Address" and "lookup_options" in answer ) - def _get_values_for_key( - self, block: Mapping, key: str, ignore_keys: Optional[list[str]] = None - ) -> Generator: - ignore_keys = ignore_keys or [] - for k, v in block.items(): - try: - if k in ignore_keys: - continue - if k == key: - yield v - if isinstance(v, dict): - yield from self._get_values_for_key(v, key, ignore_keys) - elif isinstance(v, (list, tuple)): - for d in v: - yield from self._get_values_for_key(d, key, ignore_keys) - except AttributeError: - continue - - def _get_dictionaries_with_key( - self, key: str, dictionary: Mapping - ) -> Generator[Mapping, None, None]: - if key in dictionary: - yield dictionary - - for value in dictionary.values(): - if isinstance(value, Sequence): - for element in value: - if isinstance(element, Mapping): - yield from self._get_dictionaries_with_key(key, element) + @staticmethod + def has_operator(rule: Any) -> bool: + return isinstance(rule, Mapping) and any( + operator in rule for operator in OPERATION_MAPPING + ) def _get_parent_section_id_for_block(self, block_id: str) -> str: parent_block_id = self._parent_id_map[block_id] @@ -778,18 +1159,22 @@ def _get_parent_section_id_for_block(self, block_id: str) -> str: section_id = self._parent_id_map[group_id] return section_id - def _block_for_answer(self, answer_id: str) -> Optional[ImmutableDict]: + def _block_for_answer(self, answer_id: str) -> ImmutableDict | None: question_id = self._parent_id_map[answer_id] block_id = self._parent_id_map[question_id] parent_block_id = self._parent_id_map[block_id] parent_block = self.get_block(parent_block_id) - if parent_block and parent_block["type"] == "ListCollector": + if ( + parent_block + and parent_block["type"] in LIST_COLLECTORS_WITH_REPEATING_BLOCKS + and block_id not in self.list_collector_repeating_block_ids + ): return parent_block return self.get_block(block_id) - def _group_for_block(self, block_id: str) -> Optional[ImmutableDict]: + def _group_for_block(self, block_id: str) -> ImmutableDict | None: block = self.get_block(block_id) parent_id = self._parent_id_map[block_id] if block and block["type"] in LIST_COLLECTOR_CHILDREN: @@ -809,48 +1194,330 @@ def _get_error_messages(self) -> dict: return messages def _populate_when_rules_section_dependencies(self) -> None: + """ + Populates section dependencies for when rules, including when rules containing progress value sources. + + Question variants and content variants don't need including, since the answer ids, block ids, and question ids + remain the same, so a change in the variant, does not impact questionnaire progress. + """ for section in self.get_sections(): - when_rules = self._get_values_for_key(section, "when") - rules: Union[Mapping, list] = next(when_rules, []) + rules: list[Mapping] = [] + when_rules = get_values_for_key( + "when", + data=section, + ignore_keys=["question_variants", "content_variants"], + ) + for when_rule in when_rules: + rules.extend(get_mappings_with_key("source", data=when_rule)) - if rules_section_dependencies := self._get_rules_section_dependencies( - section["id"], rules + for rule in rules: + self._populate_dependencies_for_rule( + rule, current_section_id=section["id"] + ) + + def _populate_dependencies_for_rule( + self, rule: Mapping, *, current_section_id: str + ) -> None: + """ + For a given rule, update dependency maps to indicate that the section containing the rule + depends on the answer/block/progress etc. that the rule is referencing. + """ + identifier: str = rule["identifier"] + source: str = rule["source"] + selector: str | None = rule.get("selector") + + dependent_answer_ids: set[str] = set() + dependent_section_ids: set[str] = set() + + progress_section_dependencies = ( + self._when_rules_section_dependencies_by_section_for_progress_value_source + ) + progress_block_dependencies = ( + self._when_rules_block_dependencies_by_section_for_progress_value_source + ) + + if source == "answers": + dependent_answer_ids.add(identifier) + elif source == "calculated_summary": + calculated_summary_block = self.get_block(identifier) + # Type Ignore: Calculated summary block will exist at this point + calculated_summary_answer_ids = get_calculated_summary_answer_ids( + calculated_summary_block # type: ignore + ) + dependent_answer_ids.update(calculated_summary_answer_ids) + elif source == "grand_calculated_summary": + # grand calculated summary section could differ from cs & answer sections, include it in dependent sections + grand_calculated_summary_section_id: str = self.get_section_id_for_block_id(identifier) # type: ignore + if grand_calculated_summary_section_id != current_section_id: + dependent_section_ids.add(grand_calculated_summary_section_id) + dependent_answer_ids.update( + self.get_answer_ids_for_grand_calculated_summary_id(identifier) + ) + elif source == "list": + self._when_rules_section_dependencies_by_list[identifier].add( + current_section_id + ) + elif source == "progress": + if selector == "section" and identifier != current_section_id: + progress_section_dependencies[identifier].add(current_section_id) + elif ( + selector == "block" + and (block_section_id := self.get_section_id_for_block_id(identifier)) + != current_section_id ): - self._when_rules_section_dependencies_map[ - section["id"] - ] = rules_section_dependencies + # Type ignore: The identifier key will return a list + progress_block_dependencies[block_section_id][identifier].add( # type: ignore + current_section_id + ) - def _get_rules_section_dependencies( - self, current_section_id: str, rules: Union[Mapping, Sequence] + dependent_section_ids |= self._get_section_dependencies_for_dependent_answers( + current_section_id, dependent_answer_ids + ) + if dependent_section_ids: + self._when_rules_section_dependencies_by_section[current_section_id].update( + dependent_section_ids + ) + + def _get_section_dependencies_for_dependent_answers( + self, current_section_id: str, dependent_answer_ids: Iterable[str] ) -> set[str]: - rules_section_dependencies: set[str] = set() + """ + For a set of answer ids dependent on a rule in the current section, add the current section + as a dependency of the answer and return the set of sections those answers reside in. + """ + section_dependencies: set[str] = set() + # Type Ignore: Added to this method as the block will exist at this point + for answer_id in dependent_answer_ids: + block = self.get_block_for_answer_id(answer_id) + section_id = self.get_section_id_for_block_id(block["id"]) # type: ignore + if section_id != current_section_id: + self._when_rules_section_dependencies_by_answer[answer_id].add( + current_section_id + ) + section_dependencies.add(section_id) # type: ignore + return section_dependencies - if isinstance(rules, Mapping) and any( - operator in rules for operator in OPERATION_MAPPING - ): - rules = self.get_operands(rules) + def _populate_calculation_summary_section_dependencies(self) -> None: + """ + For each block, find all the calculated and grand calculated summary value source dependencies + and make sure all involved sections are added as dependencies for that block and its section. - for rule in rules: - if not isinstance(rule, Mapping): - continue + Since calculated summaries can only contain answers from the section they are in, + it is sufficient to check the section for the calculated summary only. For grand calculated summaries + Need to include the section of the gcs, and each cs it references. + """ + for section in self.get_sections(): + for block in self.get_blocks_for_section(section): + sources = get_mappings_with_key( + "source", data=block, ignore_keys=["when"] + ) + section_dependencies: set[str] = set() + + for source in sources: + if source["source"] == "calculated_summary": + section_dependencies.add( + self.get_section_id_for_block_id(source["identifier"]) # type: ignore + ) + elif source["source"] == "grand_calculated_summary": + section_dependencies.update( + self.get_section_ids_for_grand_calculated_summary_id( + source["identifier"] + ) + ) + self.calculation_summary_section_dependencies_by_block[section["id"]][ + block["id"] + ].update(section_dependencies) + + def _get_section_ids_for_answer_ids( + self, answer_ids: Iterable[str] + ) -> OrderedSet[str]: + section_dependencies: OrderedSet[str] = OrderedSet() + for answer_id in answer_ids: + block = self.get_block_for_answer_id(answer_id) + # Type ignore: block_id and section_id is never None + section_id = self.get_section_id_for_block_id(block["id"]) # type: ignore + section_dependencies.add(section_id) # type: ignore + return section_dependencies + + def get_answer_ids_for_grand_calculated_summary_id( + self, grand_calculated_summary_id: str + ) -> list[str]: + # Type ignores: can assume the cs and gcs exist + answer_ids: list[str] = [] + block: ImmutableDict = self.get_block(grand_calculated_summary_id) # type: ignore + for ( + calculated_summary_id + ) in get_calculated_summary_ids_for_grand_calculated_summary(block): + calculated_summary_block: ImmutableDict = self.get_block( # type: ignore + calculated_summary_id + ) + answer_ids.extend( + get_calculated_summary_answer_ids(calculated_summary_block) + ) + return answer_ids + + def get_section_ids_for_grand_calculated_summary_id( + self, grand_calculated_summary_id: str + ) -> set[str]: + """ + Returns all sections that the grand calculated summary depends on, + i.e. the grand calculated summary section and the sections for each included calculated summary + """ + # Type ignores: Can assume the block and each section exists + block_ids = {grand_calculated_summary_id} + block: ImmutableDict = self.get_block(grand_calculated_summary_id) # type: ignore + block_ids.update(get_calculated_summary_ids_for_grand_calculated_summary(block)) + return {self.get_section_id_for_block_id(block_id) for block_id in block_ids} # type: ignore + + def get_summary_item_for_list_for_section( + self, *, section_id: str, list_name: str + ) -> ImmutableDict | None: + if summary := self.get_summary_for_section(section_id): + for item in summary.get("items", []): + if item.get("for_list") == list_name: + return item # type: ignore + + def get_related_answers_for_list_for_section( + self, *, section_id: str, list_name: str + ) -> tuple[ImmutableDict] | None: + if item := self.get_summary_item_for_list_for_section( + section_id=section_id, list_name=list_name + ): + return item.get("related_answers") - answer_id = None + def get_item_label(self, section_id: str, list_name: str) -> str | None: + if summary := self.get_summary_for_section(section_id): + for item in summary.get("items", []): + if item["for_list"] == list_name and item.get("item_label"): + return str(item["item_label"]) - if "id" in rule: - answer_id = rule["id"] - elif rule.get("source") == "answers": - answer_id = rule.get("identifier") + def get_item_anchor(self, section_id: str, list_name: str) -> str | None: + if summary := self.get_summary_for_section(section_id): + for item in summary.get("items", []): + if item["for_list"] == list_name and item.get("item_anchor_answer_id"): + return f"#{str(item['item_anchor_answer_id'])}" - if answer_id: - block = self.get_block_for_answer_id(answer_id) # type: ignore - section_id = self.get_section_id_for_block_id(block["id"]) # type: ignore + def _update_dependencies_for_placeholders( + self, answer_ids: Iterable[str], block: ImmutableDict + ) -> None: + if dependent_sections := self._get_section_ids_for_answer_ids( + answer_ids=answer_ids + ): + # Type Ignore: At this point the section id and block id cannot be None + section_id = self.get_section_id_for_block_id(block["id"]) + self._placeholder_transform_section_dependencies_by_block[section_id][ # type: ignore + block["id"] + ].update( + dependent_sections + ) - if section_id != current_section_id: - rules_section_dependencies.add(section_id) # type: ignore + def _populate_placeholder_transform_section_dependencies(self) -> None: + for block in self.get_blocks(): + # Calculation summary blocks can indirectly reference placeholders from their dependent blocks, so + # logic is needed to identify the relevant dependent blocks that may contain placeholders and assess + # whether there are any dependent sections + if block["type"] == "GrandCalculatedSummary": + answer_ids = self.get_answer_ids_for_grand_calculated_summary_id( + block["id"] + ) + elif block["type"] == "CalculatedSummary": + answer_ids = get_calculated_summary_answer_ids(block) + else: + answer_ids = [] + + if answer_ids: + dependent_blocks = [ + self.get_block_for_answer_id(answer_id) + for answer_id in set(answer_ids) + ] + for dependent_block in dependent_blocks: + # Type ignore: Block will exist at this point + if placeholder_answer_ids := get_placeholder_answer_ids_requiring_routing_path( + dependent_block # type: ignore + ): + self._update_dependencies_for_placeholders( + placeholder_answer_ids, block + ) + + elif placeholder_answer_ids := get_placeholder_answer_ids_requiring_routing_path( + block + ): + self._update_dependencies_for_placeholders( + placeholder_answer_ids, block + ) - if any(operator in rule for operator in OPERATION_MAPPING): - rules_section_dependencies.update( - self._get_rules_section_dependencies(current_section_id, rule) + def update_dependencies_for_dynamic_answers( + self, *, question: Mapping, block_id: str + ) -> None: + if dynamic_answers := question.get("dynamic_answers"): + self.dynamic_answers_parent_block_ids.add(block_id) + for answer in dynamic_answers["answers"]: + value_source = dynamic_answers["values"] + self._update_dependencies_for_value_source( + value_source, + block_id=block_id, + answer_id=answer["id"], ) - return rules_section_dependencies + self._dynamic_answer_ids.add(answer["id"]) + + self._update_dependencies_for_answer(answer, block_id=block_id) + + +def is_summary_with_calculation(summary_type: str) -> bool: + return summary_type in {"GrandCalculatedSummary", "CalculatedSummary"} + + +def get_sources_for_types_from_data( + *, + source_types: Iterable[str], + data: MultiDict | Mapping | Sequence, + ignore_keys: list | None = None, +) -> list: + sources = get_mappings_with_key(key="source", data=data, ignore_keys=ignore_keys) + + return [source for source in sources if source["source"] in source_types] + + +def get_identifiers_from_calculation_block( + *, calculation_block: Mapping, source_type: str +) -> list[str]: + values = get_sources_for_types_from_data( + source_types={source_type}, data=calculation_block["calculation"]["operation"] + ) + + return [value["identifier"] for value in values] + + +def get_calculated_summary_answer_ids(calculated_summary_block: Mapping) -> list[str]: + if calculated_summary_block["calculation"].get("answers_to_calculate"): + return list(calculated_summary_block["calculation"]["answers_to_calculate"]) + + return get_identifiers_from_calculation_block( + calculation_block=calculated_summary_block, source_type="answers" + ) + + +def get_calculated_summary_ids_for_grand_calculated_summary( + grand_calculated_summary_block: Mapping, +) -> list[str]: + return get_identifiers_from_calculation_block( + calculation_block=grand_calculated_summary_block, + source_type="calculated_summary", + ) + + +def is_list_collector_block_editable(block: Mapping) -> bool: + return bool(block["type"] == "ListCollector") + + +def get_placeholder_answer_ids_requiring_routing_path(block: ImmutableDict) -> set[str]: + transforms = get_mappings_with_key("transform", data=block) + + return { + item["identifier"] + for transform in transforms + if transform["transform"] in TRANSFORMS_REQUIRING_ROUTING_PATH + for item in transform["arguments"]["items"] + if item.get("source") == "answers" + } diff --git a/app/questionnaire/questionnaire_store_updater.py b/app/questionnaire/questionnaire_store_updater.py index 7f274c4479..8ee6591c31 100644 --- a/app/questionnaire/questionnaire_store_updater.py +++ b/app/questionnaire/questionnaire_store_updater.py @@ -1,74 +1,94 @@ from collections import defaultdict from itertools import combinations -from typing import Any, Dict, Iterable, List, Mapping, Optional, Tuple, Union +from typing import Iterable, Mapping, MutableMapping, Sequence -from app.data_models import AnswerValueTypes, QuestionnaireStore +from ordered_set import OrderedSet +from werkzeug.datastructures import ImmutableDict + +from app.data_models import ( + AnswerValueTypes, + CompletionStatus, + QuestionnaireStore, + SupplementaryDataStore, +) from app.data_models.answer_store import Answer -from app.data_models.progress_store import CompletionStatus, SectionKeyType +from app.data_models.relationship_store import RelationshipDict, RelationshipStore from app.questionnaire import QuestionnaireSchema -from app.questionnaire.location import Location -from app.questionnaire.questionnaire_schema import AnswerDependent +from app.questionnaire.location import Location, SectionKey +from app.questionnaire.questionnaire_schema import Dependent +from app.questionnaire.router import Router +from app.utilities.types import ( + DependentSection, + LocationType, + SupplementaryDataListMapping, +) -class QuestionnaireStoreUpdater: - """Component responsible for any actions that need to happen as a result of updating the questionnaire_store""" +class QuestionnaireStoreUpdaterBase: + """Component responsible for any actions that need to happen as a result of updating the questionnaire_store + his should only be used over the QuestionnaireStoreUpdater if location is unknown""" - EMPTY_ANSWER_VALUES: Tuple = (None, [], "", {}) + EMPTY_ANSWER_VALUES: tuple[None, list, str, dict] = (None, [], "", {}) def __init__( self, - current_location: Location, schema: QuestionnaireSchema, questionnaire_store: QuestionnaireStore, - current_question: Mapping[str, Any], + router: Router, ): - self._current_location = current_location - self._current_question = current_question or {} self._schema = schema self._questionnaire_store = questionnaire_store - self._answer_store = self._questionnaire_store.answer_store - self._list_store = self._questionnaire_store.list_store - self._progress_store = self._questionnaire_store.progress_store + self._answer_store = self._questionnaire_store.data_stores.answer_store + self._list_store = self._questionnaire_store.data_stores.list_store + self._progress_store = self._questionnaire_store.data_stores.progress_store + self._router = router + + self.dependent_block_id_by_section_key: Mapping[SectionKey, set[str]] = ( + defaultdict(set) + ) + self.dependent_sections: set[DependentSection] = set() - self.dependent_block_id_by_section_key: Mapping[ - SectionKeyType, set[str] - ] = defaultdict(set) + @property + def _supplementary_data_store(self) -> SupplementaryDataStore: + return self._questionnaire_store.data_stores.supplementary_data_store - def save(self): + @_supplementary_data_store.setter + def _supplementary_data_store(self, store: SupplementaryDataStore) -> None: + self._questionnaire_store.data_stores.supplementary_data_store = store + + def save(self) -> None: if self.is_dirty(): self._questionnaire_store.save() - def is_dirty(self): - if ( + def is_dirty(self) -> bool: + return bool( self._answer_store.is_dirty or self._list_store.is_dirty or self._progress_store.is_dirty - ): - return True - return False + ) def update_relationships_answer( self, - relationship_store, - relationships_answer_id, - ): + relationship_store: RelationshipStore, + relationships_answer_id: str, + ) -> None: self._answer_store.add_or_update( - Answer(relationships_answer_id, relationship_store.serialize()) + # Type ignore: serialize returns a list of typed dicts, so it is a valid answer type + Answer(relationships_answer_id, relationship_store.serialize()) # type: ignore ) def remove_completed_relationship_locations_for_list_name( self, list_name: str ) -> None: - target_relationship_collectors = self._get_relationship_collectors_by_list_name( + if target_relationship_collectors := self._get_relationship_collectors_by_list_name( list_name - ) - if target_relationship_collectors: + ): for target in target_relationship_collectors: block_id = target["id"] section_id = self._schema.get_section_for_block_id(block_id)["id"] # type: ignore self.remove_completed_location(Location(section_id, block_id)) - def update_relationship_question_completeness(self, list_name: str) -> None: + def _update_relationship_question_completeness(self, list_name: str) -> None: relationship_collectors = self._get_relationship_collectors_by_list_name( list_name ) @@ -79,15 +99,12 @@ def update_relationship_question_completeness(self, list_name: str) -> None: list_items = self._list_store[list_name] for collector in relationship_collectors: - relationship_answer_id = self._schema.get_first_answer_id_for_block( collector["id"] ) - relationship_answers = self._get_relationships_in_answer_store( + if relationship_answers := self._get_relationships_in_answer_store( relationship_answer_id - ) - - if relationship_answers: + ): pairs = { (answer["list_item_id"], answer["to_list_item_id"]) for answer in relationship_answers @@ -101,61 +118,73 @@ def update_relationship_question_completeness(self, list_name: str) -> None: location = Location(section_id, collector["id"]) self.add_completed_location(location) - def _get_relationship_collectors_by_list_name(self, list_name: str): + def _get_relationship_collectors_by_list_name( + self, list_name: str + ) -> list[ImmutableDict] | None: return self._schema.get_relationship_collectors_by_list_name(list_name) - def _get_relationships_in_answer_store(self, relationship_answer_id: str): + def _get_relationships_in_answer_store( + self, relationship_answer_id: str + ) -> list[RelationshipDict]: return self._answer_store.get_answer(relationship_answer_id).value # type: ignore - def remove_answers(self, answer_ids: List, list_item_id: str = None): + def remove_answers( + self, answer_ids: list[str], list_item_id: str | None = None + ) -> None: for answer_id in answer_ids: self._answer_store.remove_answer(answer_id, list_item_id=list_item_id) - def add_primary_person(self, list_name): - self.remove_completed_relationship_locations_for_list_name(list_name) - - if self._list_store[list_name].primary_person: - return self._list_store[list_name].primary_person - - # If a primary person was initially answered negatively, then changed to positive, - # the location must be removed from the progress store. - self.remove_completed_location(self._current_location) - - return self._list_store.add_list_item(list_name, primary_person=True) - - def add_list_item(self, list_name): + def add_list_item(self, list_name: str) -> str: new_list_item_id = self._list_store.add_list_item(list_name) self.remove_completed_relationship_locations_for_list_name(list_name) return new_list_item_id - def remove_primary_person(self, list_name: str): - """Remove the primary person and all of their answers. - Any context for the primary person will be removed - """ - list_item_id = self._list_store[list_name].primary_person - if list_item_id: - self.remove_list_item_and_answers(list_name, list_item_id) - - def remove_list_item_and_answers(self, list_name: str, list_item_id: str): - """Remove answers from the answer store and update the list store to remove it. + def remove_list_item_data(self, list_name: str, list_item_id: str) -> None: + """Remove answers from the answer store, remove list item progress from the progress store and update the list store to remove it. Any related relationship answers are re-evaluated for completeness. """ self._list_store.delete_list_item(list_name, list_item_id) - self._answer_store.remove_all_answers_for_list_item_id( - list_item_id=list_item_id - ) + self._answer_store.remove_all_answers_for_list_item_ids(list_item_id) - answers = self.get_relationship_answers_for_list_name(list_name) - if answers: - self.remove_relationship_answers_for_list_item_id(list_item_id, answers) - self.update_relationship_question_completeness(list_name) + if answers := self._get_relationship_answers_for_list_name(list_name): + self._remove_relationship_answers_for_list_item_id(list_item_id, answers) + self._update_relationship_question_completeness(list_name) self._progress_store.remove_progress_for_list_item_id(list_item_id=list_item_id) - def get_relationship_answers_for_list_name( + def remove_list_data(self, list_name: str) -> None: + """Delete entire list and remove any associated answers""" + self._answer_store.remove_all_answers_for_list_item_ids( + *self._list_store[list_name].items + ) + self._list_store.delete_list(list_name) + + def capture_dependencies_for_list_change(self, list_name: str) -> None: + """ + Captures the dependencies when an item is added to or removed from the given list. + Any list collector sections will be affected by the change as well as other sections using when rules + """ + self._capture_block_dependencies_for_list(list_name) + section_ids = self._schema.get_when_rule_section_dependencies_for_list( + list_name + ) + section_ids.update( + self._schema.list_collector_section_ids_by_list_name.get(list_name, set()) + ) + + for section_key in self.started_section_keys(section_ids=section_ids): + # Only add sections which are repeated sections for this list, or the section in which this list is collected + # Prevents list item progresses being added as dependants as these are captured by started_section_keys(section_ids=section_ids) + if section_key.list_item_id and not self._schema.get_repeat_for_section( + section_key.section_id + ): + continue + self.dependent_sections.add(DependentSection(**section_key.to_dict())) + + def _get_relationship_answers_for_list_name( self, list_name: str - ) -> Union[List[Answer], None]: + ) -> list[Answer] | None: associated_relationship_collectors = ( self._get_relationship_collectors_by_list_name(list_name) ) @@ -170,18 +199,18 @@ def get_relationship_answers_for_list_name( return self._answer_store.get_answers_by_answer_id(relationship_answer_ids) def update_same_name_items( - self, list_name: str, same_name_answer_ids: Optional[List[str]] - ): + self, list_name: str, same_name_answer_ids: list[str] | None + ) -> None: if not same_name_answer_ids: return same_name_items = set() - people_names: Dict[str, list] = {} + people_names: dict[str, str] = {} - list_model = self._questionnaire_store.list_store[list_name] + list_model = self._list_store[list_name] for current_list_item_id in list_model: - answers = self._questionnaire_store.answer_store.get_answers_by_answer_id( + answers = self._answer_store.get_answers_by_answer_id( answer_ids=same_name_answer_ids, list_item_id=current_list_item_id ) current_names = [answer.value.casefold() for answer in answers if answer] # type: ignore @@ -190,12 +219,12 @@ def update_same_name_items( if matching_list_item_id := people_names.get(current_list_item_name): same_name_items |= {current_list_item_id, matching_list_item_id} else: - people_names[current_list_item_name] = current_list_item_id # type: ignore + people_names[current_list_item_name] = current_list_item_id - list_model.same_name_items = list(same_name_items) # type: ignore + list_model.same_name_items = list(same_name_items) - def remove_relationship_answers_for_list_item_id( - self, list_item_id: str, answers: List + def _remove_relationship_answers_for_list_item_id( + self, list_item_id: str, answers: list ) -> None: for answer in answers: answers_to_keep = [ @@ -206,27 +235,25 @@ def remove_relationship_answers_for_list_item_id( answer.value = answers_to_keep self._answer_store.add_or_update(answer) - def add_completed_location(self, location: Optional[Location] = None): + def add_completed_location(self, location: LocationType) -> None: if not self._progress_store.is_routing_backwards: - location = location or self._current_location self._progress_store.add_completed_location(location) - def remove_completed_location(self, location: Optional[Location] = None) -> bool: - location = location or self._current_location + def remove_completed_location(self, location: LocationType) -> bool: return self._progress_store.remove_completed_location(location) def update_section_status( - self, is_complete: bool, section_id: str, list_item_id: Optional[str] = None - ): + self, *, is_complete: bool, section_key: SectionKey + ) -> bool: status = ( CompletionStatus.COMPLETED if is_complete else CompletionStatus.IN_PROGRESS ) - self._progress_store.update_section_status(status, section_id, list_item_id) + return self._progress_store.update_section_status(status, section_key) def _update_answer( self, answer_id: str, - list_item_id: Optional[str], + list_item_id: str | None, answer_value: AnswerValueTypes, ) -> bool: answer_value_to_store = ( @@ -252,62 +279,186 @@ def _update_answer( ) ) - def _capture_dependencies_for_answer(self, answer_id: str) -> None: - """Captures a unique list of block ids that are dependents of the provided answer id. - - The block_ids are mapped to the section key. Dependencies in a repeating section use the list items - for the repeating list when creating the section key. + def _capture_block_dependencies_for_list(self, list_name: str) -> None: + """Captures a list of block ids that are dependents of the given list""" + dependencies: set[Dependent] = self._schema.list_dependencies.get( + list_name, set() + ) + for dependent in dependencies: + list_item_ids = self._get_list_item_ids_for_list_dependency(dependent) + self._capture_block_dependent(dependent, list_item_ids) + + def _get_list_item_ids_for_list_dependency( + self, dependency: Dependent + ) -> list[str] | list[None]: + if dependency.for_list: + return self._list_store[dependency.for_list].items + return [None] + + def _capture_block_dependent( + self, dependent: Dependent, list_item_ids: Sequence[str] | Sequence[None] + ) -> None: + """ + The block_id is mapped to the section key. Dependents in a repeating section should be passed in with the list items + for the repeating list for creating the section key. Blocks are captured regardless of whether they are complete. This avoids fetching the completed blocks multiples times, as you may have multiple dependencies for one block which may also apply to each item in the list. However, when updating the progress store, the block ids are checked to ensure they exist in the progress store. """ - dependencies: set[AnswerDependent] = self._schema.answer_dependencies.get( - answer_id, set() - ) + for list_item_id in list_item_ids: + if dependent.answer_id: + self._answer_store.remove_answer( + dependent.answer_id, list_item_id=list_item_id + ) + self.dependent_block_id_by_section_key[ + SectionKey(dependent.section_id, list_item_id) + ].add(dependent.block_id) - for dependency in dependencies: - if dependency.for_list: - list_item_ids: Union[list[str], list[None]] = self._list_store[ - dependency.for_list - ].items - else: - list_item_ids = [None] + def _capture_section_dependencies_for_answer(self, answer_id: str) -> None: + """Captures a unique list of section ids that are dependents of the provided answer id.""" + + answer_id_section_dependents = ( + self._schema.when_rules_section_dependencies_by_answer + ) - for list_item_id in list_item_ids: - if dependency.answer_id: - self._answer_store.remove_answer( - dependency.answer_id, list_item_id=list_item_id + for section_id in answer_id_section_dependents.get(answer_id, {}): + if repeating_list := self._schema.get_repeating_list_for_section( + section_id + ): + for list_item_id in self._list_store[repeating_list].items: + self.dependent_sections.add( + DependentSection(section_id, list_item_id) ) + else: + self.dependent_sections.add(DependentSection(section_id)) - self.dependent_block_id_by_section_key[ - (dependency.section_id, list_item_id) - ].add(dependency.block_id) + def _capture_section_dependencies_progress_value_source_for_section( + self, + section_id: str, + ) -> None: + """ + Captures a unique list of section ids that are dependents of the provided section, for progress value sources. + """ + dependent_sections: Iterable = ( + self._schema.when_rules_section_dependencies_by_section_for_progress_value_source.get( + section_id, set() + ) + ) + self._update_section_dependencies(dependent_sections) - def update_answers( - self, form_data: Mapping[str, Any], list_item_id: Optional[str] = None + def _capture_section_dependencies_progress_value_source_for_block( + self, *, section_id: str, block_id: str ) -> None: - list_item_id = list_item_id or self._current_location.list_item_id - answer_ids_for_question = self._schema.get_answer_ids_for_question( - self._current_question + """ + Captures a unique list of section ids that are dependents of the provided block, for progress value sources. + """ + dependent_sections: Iterable = ( + self._schema.when_rules_block_dependencies_by_section_for_progress_value_source.get( + section_id, {} + ).get( + block_id, set() + ) ) + self._update_section_dependencies(dependent_sections) - for answer_id, answer_value in form_data.items(): - if answer_id not in answer_ids_for_question: + def _update_section_dependencies(self, dependent_sections: Iterable) -> None: + for section_id in dependent_sections: + if repeating_list := self._schema.get_repeating_list_for_section( + section_id + ): + for list_item_id in self._list_store[repeating_list].items: + self.dependent_sections.add( + DependentSection(section_id, list_item_id) + ) + else: + self.dependent_sections.add(DependentSection(section_id)) + + def update_progress_for_dependent_sections(self) -> None: + """Removes dependent blocks from the progress store and updates the progress to IN_PROGRESS. + Section progress is not updated for the current location as it is handled by `handle_post` on block handlers. + """ + evaluated_dependents: list[tuple] = [] + + chronological_dependents = self._get_chronological_section_dependents() + + for section in chronological_dependents: + if ( + section.section_id, + section.list_item_id, + ) not in self.started_section_keys(): continue - answer_updated = self._update_answer(answer_id, list_item_id, answer_value) - if answer_updated: - self._capture_dependencies_for_answer(answer_id) + if ( + section.section_id, + section.list_item_id, + ) not in evaluated_dependents: + self._evaluate_dependents( + dependent_section=section, evaluated_dependents=evaluated_dependents + ) + evaluated_dependents.append((section.section_id, section.list_item_id)) - def update_progress_for_dependant_sections(self) -> None: - """Removes dependent blocks from the progress store and updates the progress to IN_PROGRESS. + def _evaluate_dependents( + self, + *, + dependent_section: DependentSection, + evaluated_dependents: list[tuple], + ) -> None: + is_path_complete = dependent_section.is_complete + if is_path_complete is None: + is_path_complete = self._router.is_path_complete( + self._router.routing_path(dependent_section.section_key) + ) - Section progress is not updated for the current location as it is handled by `handle_post` on block handlers. + if self.update_section_status( + is_complete=is_path_complete, section_key=dependent_section.section_key + ): + dependents_of_dependent: OrderedSet = ( + self._schema.when_rules_section_dependencies_by_section_for_progress_value_source.get( + dependent_section.section_id, OrderedSet() + ) + ) + for dependent_section_id in dependents_of_dependent: + if repeating_list := self._schema.get_repeating_list_for_section( + dependent_section_id + ): + for item_id in self._list_store[repeating_list].items: + if ( + dependent_section_id, + item_id, + ) not in evaluated_dependents: + self._evaluate_dependent_of_dependents( + dependent_section_id=dependent_section_id, + list_item_id=item_id, + evaluated_dependents=evaluated_dependents, + ) + elif ( + dependent_section_id, + dependent_section.list_item_id, + ) not in evaluated_dependents: + self._evaluate_dependent_of_dependents( + dependent_section_id=dependent_section_id, + evaluated_dependents=evaluated_dependents, + ) + + def _evaluate_dependent_of_dependents( + self, + dependent_section_id: str, + evaluated_dependents: list[tuple], + list_item_id: str | None = None, + ) -> None: + self._evaluate_dependents( + dependent_section=DependentSection( + section_id=dependent_section_id, + list_item_id=list_item_id, + ), + evaluated_dependents=evaluated_dependents, + ) + evaluated_dependents.append((dependent_section_id, list_item_id)) + + def remove_dependent_blocks_and_capture_dependent_sections(self) -> None: + """Removes dependent blocks from the progress store.""" - When updating the progress store, the routing path is not re-evaluated because - removing previously completed blocks means the section can't be complete. - """ for ( section_key, blocks_to_remove, @@ -326,15 +477,219 @@ def update_progress_for_dependant_sections(self) -> None: ) blocks_removed |= self.remove_completed_location(location) - if blocks_removed and ( - section_id != self._current_location.section_id - or list_item_id != self._current_location.list_item_id + if blocks_removed: + self._capture_dependent_section(section_key) + + def _capture_dependent_section(self, section_key: SectionKey) -> None: + """ + Since this section key will be marked as incomplete, any `DependentSection` with is_complete as `None` + can be removed as we do not need to re-evaluate progress as we already know the section would be incomplete. + """ + dependent = DependentSection(**section_key.to_dict()) + if dependent in self.dependent_sections: + self.dependent_sections.remove(dependent) + + self.dependent_sections.add( + DependentSection(**section_key.to_dict(), is_complete=False) + ) + + def started_section_keys( + self, section_ids: Iterable[str] | None = None + ) -> list[SectionKey]: + return self._progress_store.started_section_keys(section_ids) + + def _get_chronological_section_dependents(self) -> list: + sections = list(self._schema.get_section_ids()) + return sorted( + self.dependent_sections, key=lambda x: sections.index(x.section_id) + ) + + def set_supplementary_data(self, to_set: MutableMapping) -> None: + """ + Used to set or update the supplementary data whenever the sds endpoint is called + (Which should be once per session, but only if the sds_dataset_id has changed) + + this updates ListStore to add/update any lists for supplementary data and stores the + identifier -> list_item_id mappings in the supplementary data store to use in the payload at the end + """ + list_mappings: dict[str, list[SupplementaryDataListMapping]] = {} + modified_lists: set[str] = set() + + if self._supplementary_data_store.list_mappings: + modified_lists |= self._remove_outdated_supplementary_lists_and_answers( + new_data=to_set + ) + + for list_name, list_data in to_set.get("items", {}).items(): + mappings, lists = self._create_supplementary_list( + list_name=list_name, list_data=list_data + ) + list_mappings[list_name] = mappings + modified_lists |= lists + + for list_name in modified_lists: + self.capture_dependencies_for_list_change(list_name) + + self._supplementary_data_store = SupplementaryDataStore( + supplementary_data=to_set, list_mappings=list_mappings + ) + + def _create_supplementary_list( + self, *, list_name: str, list_data: Sequence[dict] + ) -> tuple[list[SupplementaryDataListMapping], set[str]]: + """ + Creates or updates a list in ListStore based off supplementary data + returns the identifier -> list_item_id mappings used and the lists that were modified in the process + """ + list_mappings: list[SupplementaryDataListMapping] = [] + modified_lists: set[str] = set() + for list_item in list_data: + identifier = list_item["identifier"] + # if any pre-existing supplementary data already has a mapping for this list item + # then its already in the list store and doesn't require adding + if not ( + list_item_id := self._supplementary_data_store.list_lookup.get( + list_name, {} + ).get(identifier) ): - self.update_section_status( - is_complete=False, - section_id=section_id, - list_item_id=list_item_id, + list_item_id = self.add_list_item(list_name) + modified_lists.add(list_name) + list_mappings.append( + SupplementaryDataListMapping( + identifier=identifier, list_item_id=list_item_id ) + ) + return list_mappings, modified_lists - def started_section_keys(self, section_ids: Optional[Iterable[str]] = None): - return self._progress_store.started_section_keys(section_ids) + def _remove_outdated_supplementary_lists_and_answers( + self, new_data: MutableMapping + ) -> set[str]: + """ + In the case that existing supplementary data is being replaced with new data: any list items in the old data + but not the new data are removed from the list store and related answers are deleted + :param new_data - the new supplementary data for comparison + :return: any lists that were modified by the change in supplementary data + """ + modified_lists: set[str] = set() + for ( + list_name, + mappings, + ) in self._supplementary_data_store.list_lookup.items(): + if list_name in new_data.get("items", {}): + new_identifiers = [ + item["identifier"] for item in new_data["items"][list_name] + ] + for identifier, list_item_id in mappings.items(): + if identifier not in new_identifiers: + modified_lists.add(list_name) + self.remove_list_item_data(list_name, list_item_id) + else: + self.remove_list_data(list_name) + return modified_lists + + +class QuestionnaireStoreUpdater(QuestionnaireStoreUpdaterBase): + """Component responsible for any actions that need to happen as a result of updating the questionnaire_store""" + + def __init__( + self, + current_location: LocationType, + schema: QuestionnaireSchema, + questionnaire_store: QuestionnaireStore, + router: Router, + current_question: Mapping | None, + ): + self._current_location = current_location + self._current_question = current_question or {} + super().__init__( + schema=schema, questionnaire_store=questionnaire_store, router=router + ) + + def add_primary_person(self, list_name: str) -> str: + self.remove_completed_relationship_locations_for_list_name(list_name) + + if primary_person := self._list_store[list_name].primary_person: + return primary_person + + # If a primary person was initially answered negatively, then changed to positive, + # the location must be removed from the progress store. + self.remove_completed_location(self._current_location) + + return self._list_store.add_list_item(list_name, primary_person=True) + + def remove_primary_person(self, list_name: str) -> None: + """Remove the primary person and all of their answers. + Any context for the primary person will be removed + """ + if list_item_id := self._list_store[list_name].primary_person: + self.remove_list_item_data(list_name, list_item_id) + + def _capture_block_dependencies_for_answer(self, answer_id: str) -> None: + """Captures a unique list of block ids that are dependents of the provided answer id.""" + dependencies: set[Dependent] = self._schema.answer_dependencies.get( + answer_id, set() + ) + is_repeating_answer = self._schema.is_answer_in_repeating_section(answer_id) + + for dependent in dependencies: + list_item_ids = self._get_list_item_ids_for_answer_dependency( + dependent, is_repeating_answer + ) + self._capture_block_dependent(dependent, list_item_ids) + + def _get_list_item_ids_for_answer_dependency( + self, dependency: Dependent, is_repeating_answer: bool | None = False + ) -> list[str] | list[None]: + """ + Gets the list item ids that relate to the dependency of the answer. + If the dependency is repeating, and so is the answer, then we must be in that repeating section, + so the only relevant list item id is the current one. + If the answer is not repeating, but the dependency is, then all list item ids need including. + """ + if dependency.for_list: + if is_repeating_answer: + # Type ignore: in this scenario the list item id must exist + return [self._current_location.list_item_id] # type: ignore + return self._list_store[dependency.for_list].items + return [None] + + def update_answers( + self, form_data: Mapping, list_item_id: str | None = None + ) -> None: + list_item_id = list_item_id or self._current_location.list_item_id + answers_by_answer_id = self._schema.get_answers_for_question_by_id( + self._current_question + ) + + for answer_id, answer_value in form_data.items(): + if answer_id not in answers_by_answer_id: + continue + + resolved_answer = answers_by_answer_id[answer_id] + answer_id_to_use = resolved_answer.get("original_answer_id") or answer_id + list_item_id_to_use = resolved_answer.get("list_item_id") or list_item_id + + if self._update_answer(answer_id_to_use, list_item_id_to_use, answer_value): + self._capture_section_dependencies_for_answer(answer_id_to_use) + self._capture_block_dependencies_for_answer(answer_id_to_use) + + if self._answer_store.is_dirty: + self.capture_progress_section_dependencies() + + def capture_progress_section_dependencies(self) -> None: + """ + Captures a unique list of section ids that are dependents of the current section or block, for progress value sources. + """ + self._capture_section_dependencies_progress_value_source_for_section( + self._current_location.section_id + ) + # Type ignore: block id will exist when at any time this is called + self._capture_section_dependencies_progress_value_source_for_block( + section_id=self._current_location.section_id, + block_id=self._current_location.block_id, # type: ignore + ) + + def _capture_dependent_section(self, section_key: SectionKey) -> None: + """Only capture the dependent section if it is not the current one""" + if section_key != self._current_location.section_key: + super()._capture_dependent_section(section_key) diff --git a/app/questionnaire/relationship_location.py b/app/questionnaire/relationship_location.py index da15643dd4..29c3bb1aba 100644 --- a/app/questionnaire/relationship_location.py +++ b/app/questionnaire/relationship_location.py @@ -1,24 +1,24 @@ -from __future__ import annotations - from dataclasses import dataclass -from typing import Mapping, Optional +from typing import Any, Mapping from flask import url_for +from app.questionnaire.location import SectionKey + @dataclass class RelationshipLocation: section_id: str block_id: str - list_name: str - list_item_id: str - to_list_item_id: Optional[str] = None + list_name: str | None + list_item_id: str | None + to_list_item_id: str | None = None def for_json(self) -> Mapping: attributes = vars(self) return {k: v for k, v in attributes.items() if v is not None} - def url(self, **kwargs) -> str: + def url(self, **kwargs: Any) -> str: if self.to_list_item_id: return url_for( "questionnaire.relationships", @@ -34,3 +34,7 @@ def url(self, **kwargs) -> str: block_id=self.block_id, **kwargs, ) + + @property + def section_key(self) -> SectionKey: + return SectionKey(self.section_id, self.list_item_id) diff --git a/app/questionnaire/relationship_router.py b/app/questionnaire/relationship_router.py index c725f895e2..f0667f58b0 100644 --- a/app/questionnaire/relationship_router.py +++ b/app/questionnaire/relationship_router.py @@ -1,5 +1,3 @@ -from typing import List, Optional - from app.data_models.answer_store import AnswerStore from app.data_models.relationship_store import RelationshipStore from app.questionnaire.relationship_location import RelationshipLocation @@ -16,9 +14,9 @@ def __init__( list_name: str, list_item_ids: list[str], relationships_block_id: str, - unrelated_block_id: Optional[str] = None, - unrelated_answer_id: Optional[str] = None, - unrelated_no_answer_values: Optional[List[str]] = None, + unrelated_block_id: str | None = None, + unrelated_answer_id: str | None = None, + unrelated_no_answer_values: list[str] | None = None, ): self.answer_store = answer_store self.relationship_store = relationship_store @@ -28,33 +26,37 @@ def __init__( self.relationships_block_id = relationships_block_id self.unrelated_block_id = unrelated_block_id self.unrelated_answer_id = unrelated_answer_id - self.unrelated_no_answer_values = unrelated_no_answer_values + self.unrelated_no_answer_values = unrelated_no_answer_values or [] self.path = self._relationships_routing_path() - def can_access_location(self, location): + def can_access_location(self, location: RelationshipLocation) -> bool: return location in self.path - def get_first_location(self): + def get_first_location(self) -> RelationshipLocation: return self.path[0] - def get_last_location(self): + def get_last_location(self) -> RelationshipLocation: return self.path[-1] - def get_next_location(self, location): + def get_next_location( + self, location: RelationshipLocation + ) -> RelationshipLocation | None: try: location_index = self.path.index(location) return self.path[location_index + 1] except IndexError: return None - def get_previous_location(self, location): + def get_previous_location( + self, location: RelationshipLocation + ) -> RelationshipLocation | None: location_index = self.path.index(location) if not location_index: return None return self.path[location_index - 1] - def _relationships_routing_path(self): - path = [] + def _relationships_routing_path(self) -> list[RelationshipLocation]: + path: list[RelationshipLocation] = [] for from_index, from_list_item_id in enumerate(self.list_item_ids): path += self._individual_relationships_routing_path( from_list_item_id=from_list_item_id, @@ -64,9 +66,9 @@ def _relationships_routing_path(self): return path def _individual_relationships_routing_path( - self, from_list_item_id, to_list_item_ids - ): - path = [] + self, from_list_item_id: str, to_list_item_ids: list[str] + ) -> list[RelationshipLocation]: + path: list[RelationshipLocation] = [] number_of_unrelated_relationships = 0 number_of_relationships_left = len(to_list_item_ids) unrelated_block_in_path = False @@ -87,7 +89,9 @@ def _individual_relationships_routing_path( ) unrelated_block_in_path = True unrelated_answer = self.answer_store.get_answer( - self.unrelated_answer_id, from_list_item_id + # Type ignore: unrelated_answer_id will exist if the unrelated_block_id does + self.unrelated_answer_id, # type: ignore + from_list_item_id, ) if ( unrelated_answer diff --git a/app/questionnaire/return_location.py b/app/questionnaire/return_location.py new file mode 100644 index 0000000000..b86382384e --- /dev/null +++ b/app/questionnaire/return_location.py @@ -0,0 +1,25 @@ +from dataclasses import asdict, dataclass + + +@dataclass(kw_only=True, frozen=True) +class ReturnLocation: + """ + Used to store return locations in the questionnaire. + + return_to: The name of the type of summary page to return to + return_to_block_id: The block_id of the block to return to + return_to_answer_id: The answer_id of the answer to return to + return_to_list_item_id: The list_item_id to return to if the location is associated with a list + """ + + return_to: str | None = None + return_to_block_id: str | None = None + return_to_answer_id: str | None = None + return_to_list_item_id: str | None = None + + def to_dict(self, answer_id_is_anchor: bool = False) -> dict: + attributes = asdict(self) + if answer_id_is_anchor: + attributes["_anchor"] = attributes["return_to_answer_id"] + del attributes["return_to_answer_id"] + return {k: v for k, v in attributes.items() if v is not None} diff --git a/app/questionnaire/router.py b/app/questionnaire/router.py index a2956366cc..8424e636b5 100644 --- a/app/questionnaire/router.py +++ b/app/questionnaire/router.py @@ -1,41 +1,30 @@ -from typing import Generator, Mapping, Optional, Union +from typing import Generator, Mapping from flask import url_for -from app.data_models import AnswerStore, ListStore, ProgressStore -from app.data_models.progress_store import SectionKeyType +from app.data_models.data_stores import DataStores +from app.data_models.list_store import ListModel from app.questionnaire import QuestionnaireSchema -from app.questionnaire.location import Location +from app.questionnaire.location import Location, SectionKey from app.questionnaire.path_finder import PathFinder +from app.questionnaire.return_location import ReturnLocation from app.questionnaire.routing_path import RoutingPath from app.questionnaire.rules.rule_evaluator import RuleEvaluator -from app.questionnaire.when_rules import evaluate_when_rules +from app.utilities.types import LocationType class Router: def __init__( self, schema: QuestionnaireSchema, - answer_store: AnswerStore, - list_store: ListStore, - progress_store: ProgressStore, - metadata: Mapping[str, Union[str, int, list]], - response_metadata: Mapping, + data_stores: DataStores, ): self._schema = schema - self._answer_store = answer_store - self._list_store = list_store - self._progress_store = progress_store - self._metadata = metadata - self._response_metadata = response_metadata + self._data_stores = data_stores self._path_finder = PathFinder( self._schema, - self._answer_store, - self._list_store, - self._progress_store, - self._metadata, - self._response_metadata, + self._data_stores, ) @property @@ -52,27 +41,28 @@ def is_questionnaire_complete(self) -> bool: return not first_incomplete_section_key def get_first_incomplete_location_in_questionnaire_url(self) -> str: - first_incomplete_section_key = self._get_first_incomplete_section_key() - - if first_incomplete_section_key: - section_id, list_item_id = first_incomplete_section_key - + if first_incomplete_section_key := self._get_first_incomplete_section_key(): section_routing_path = self._path_finder.routing_path( - section_id=section_id, list_item_id=list_item_id + first_incomplete_section_key ) return self.get_section_resume_url(section_routing_path) return self.get_next_location_url_for_end_of_section() - def get_last_location_in_questionnaire_url(self) -> str: - routing_path = self.routing_path(*self._get_last_complete_section_key()) - return self.get_last_location_in_section(routing_path).url() + def get_last_location_in_questionnaire_url(self) -> str | None: + if section_key := self._get_last_complete_section_key(): + if self.can_display_section_summary(section_key): + return url_for( + "questionnaire.get_section", section_id=section_key.section_id + ) + routing_path = self.routing_path(section_key) + return self.get_last_location_in_section(routing_path).url() - def is_list_item_in_list_store(self, list_item_id: str, list_name: str) -> bool: - return list_item_id in self._list_store[list_name] + def _is_list_item_in_list_store(self, list_item_id: str, list_name: str) -> bool: + return list_item_id in self._data_stores.list_store[list_name] def can_access_location( - self, location: Location, routing_path: RoutingPath + self, location: LocationType, routing_path: RoutingPath ) -> bool: """ Checks whether the location is valid and accessible. @@ -84,7 +74,7 @@ def can_access_location( if ( location.list_item_id and location.list_name - and not self.is_list_item_in_list_store( + and not self._is_list_item_in_list_store( location.list_item_id, location.list_name ) ): @@ -93,82 +83,107 @@ def can_access_location( return location.block_id in self._get_allowable_path(routing_path) def can_access_hub(self) -> bool: - return self._schema.is_flow_hub and all( - self._progress_store.is_section_complete(section_id) - for section_id in self._schema.get_section_ids_required_for_hub() - if section_id in self.enabled_section_ids - ) + if not self._schema.is_flow_hub: + return False - def can_display_section_summary( - self, section_id: str, list_item_id: Optional[str] = None - ) -> bool: + for section_id in self._schema.get_section_ids_required_for_hub(): + if section_id in self.enabled_section_ids: + repeating_list_for_section = ( + self._schema.get_repeating_list_for_section(section_id) + ) + + items: ListModel | list[None] = ( + self._data_stores.list_store.get(repeating_list_for_section) + if repeating_list_for_section + else [None] + ) + + for list_item_id in items: + section_key = SectionKey(section_id, list_item_id) + if not self._data_stores.progress_store.is_section_complete( + section_key + ): + return False + + return True + + def can_display_section_summary(self, section_key: SectionKey) -> bool: return bool( - self._schema.get_summary_for_section(section_id) - ) and self._progress_store.is_section_complete(section_id, list_item_id) + self._schema.get_summary_for_section(section_key.section_id) + ) and self._data_stores.progress_store.is_section_complete(section_key) - def routing_path( - self, section_id: str, list_item_id: Optional[str] = None - ) -> RoutingPath: - return self._path_finder.routing_path(section_id, list_item_id) + def routing_path(self, section_key: SectionKey) -> RoutingPath: + return self._path_finder.routing_path(section_key) def get_next_location_url( self, - location: Location, + location: LocationType, routing_path: RoutingPath, - return_to: Optional[str] = None, + return_location: ReturnLocation, ) -> str: """ Get the next location in the section. If the section is complete, determine where to go next, whether it be a summary, the hub or the next incomplete location. """ - if self._progress_store.is_section_complete( - location.section_id, location.list_item_id + is_section_complete = self._data_stores.progress_store.is_section_complete( + location.section_key + ) + + if return_to_url := self.get_return_to_location_url( + location=location, + return_location=return_location, + routing_path=routing_path, + is_for_previous=False, + is_section_complete=is_section_complete, ): - if return_to and ( - return_to_url := self._get_return_to_location_url(location, return_to) - ): - return return_to_url + return return_to_url - return self._get_next_location_url_for_complete_section(location) + if is_section_complete: + return self._get_next_location_url_for_complete_section( + location.section_key + ) # Due to backwards routing you can be on the last block of the path but with an in_progress section is_last_block_on_path = routing_path[-1] == location.block_id if is_last_block_on_path: - return self._get_first_incomplete_location_in_section(routing_path).url() - - return self.get_next_block_url(location, routing_path, return_to=return_to) + # Type ignore: The section is not complete therefore we must have a location + next_location: Location = self._get_first_incomplete_location_in_section(routing_path) # type: ignore + return next_location.url() + + return self._get_next_block_url( + location, + routing_path, + return_location, + ) - def _get_next_location_url_for_complete_section(self, location: Location) -> str: - if self._schema.show_summary_on_completion_for_section(location.section_id): - return self._get_section_url(location) + def _get_next_location_url_for_complete_section( + self, section_key: SectionKey + ) -> str: + if self._schema.show_summary_on_completion_for_section(section_key.section_id): + return self._get_section_url(section_key) return self.get_next_location_url_for_end_of_section() def get_previous_location_url( self, - location: Location, + location: LocationType, routing_path: RoutingPath, - return_to: Optional[str] = None, - return_to_answer_id: Optional[str] = None, - ) -> Optional[str]: + return_location: ReturnLocation, + ) -> str | None: """ Returns the previous 'location' to visit given a set of user answers or returns to the summary if - the `return_to` var is set and the section is complete. + the `return_location.return_to` var is set and the section is complete. """ - - if return_to and ( - self._progress_store.is_section_complete( - location.section_id, location.list_item_id - ) - and ( - return_to_url := self._get_return_to_location_url( - location, return_to, return_to_answer_id=return_to_answer_id - ) - ) + if return_to_url := self.get_return_to_location_url( + location=location, + return_location=return_location, + routing_path=routing_path, + is_for_previous=True, ): return return_to_url - block_id_index = routing_path.index(location.block_id) + # Type ignore: the location will have a block id at this point + block_id_index = routing_path.index(location.block_id) # type: ignore if block_id_index != 0: previous_block_id = routing_path[block_id_index - 1] @@ -177,16 +192,14 @@ def get_previous_location_url( return url_for( "questionnaire.relationships", last=True, - return_to=return_to, - _anchor=return_to_answer_id, + **return_location.to_dict(answer_id_is_anchor=True), ) return url_for( "questionnaire.block", block_id=previous_block_id, list_name=routing_path.list_name, list_item_id=routing_path.list_item_id, - return_to=return_to, - _anchor=return_to_answer_id, + **return_location.to_dict(answer_id_is_anchor=True), ) if self.can_access_hub(): @@ -194,20 +207,215 @@ def get_previous_location_url( return None - def _get_return_to_location_url( + def get_return_to_location_url( self, - location: Location, - return_to: str, - return_to_answer_id: Optional[str] = None, - ) -> Optional[str]: - if return_to == "section-summary": + *, + location: LocationType, + return_location: ReturnLocation, + routing_path: RoutingPath, + is_for_previous: bool, + is_section_complete: bool | None = None, + ) -> str | None: + if not return_location.return_to: + return None + + if return_location.return_to == "grand-calculated-summary" and ( + url := self._get_return_to_for_grand_calculated_summary( + return_location=return_location, + section_key=location.section_key, + routing_path=routing_path, + is_for_previous=is_for_previous, + location=location, + ) + ): + return url + + if return_location.return_to.startswith("calculated-summary") and ( + url := self._get_return_to_for_calculated_summary( + location=location, + routing_path=routing_path, + return_location=return_location, + ) + ): + return url + + if is_section_complete is None: + is_section_complete = self._data_stores.progress_store.is_section_complete( + location.section_key + ) + + if not is_section_complete: + # go to the next incomplete item in the section whilst preserving return to parameters + return self._get_return_url_for_inaccessible_location( + is_for_previous=is_for_previous, + routing_path=routing_path, + return_location=return_location, + ) + + if return_location.return_to == "section-summary": return self._get_section_url( - location, return_to_answer_id=return_to_answer_id + location.section_key, + return_to_answer_id=return_location.return_to_answer_id, + ) + if ( + return_location.return_to == "final-summary" + and self.is_questionnaire_complete + ): + return url_for( + "questionnaire.submit_questionnaire", + _anchor=return_location.return_to_answer_id, + ) + + def _get_return_to_for_grand_calculated_summary( + self, + *, + return_location: ReturnLocation, + section_key: SectionKey, + routing_path: RoutingPath, + is_for_previous: bool, + location: LocationType, + ) -> str | None: + """ + Builds the return url for a grand calculated summary, + and accounts for it possibly being in a different section to the calculated summaries it references + """ + if not ( + return_location.return_to_block_id + and self._schema.is_block_valid(return_location.return_to_block_id) + ): + return None + + return_to_block_id = return_location.return_to_block_id + # Type ignore: if the block is valid, then we'll be able to find a section for it + grand_calculated_summary_section: str = ( + self._schema.get_section_id_for_block_id(return_to_block_id) # type: ignore + ) + list_item_id = location.list_item_id or return_location.return_to_list_item_id + list_name = ( + self._data_stores.list_store.get_list_name_for_list_item_id(list_item_id) + if list_item_id + else None + ) + if grand_calculated_summary_section != section_key.section_id: + # the grand calculated summary is in a different section which will have a different routing path + # but does not go to it unless the section is enabled and the current section is complete + if ( + not self._data_stores.progress_store.is_section_complete(section_key) + or grand_calculated_summary_section not in self.enabled_section_ids + ): + return None + routing_path = self._path_finder.routing_path( + SectionKey( + section_id=grand_calculated_summary_section, + list_item_id=list_item_id, + ) + ) + if self.can_access_location( + Location( + block_id=return_location.return_to_block_id, + section_id=grand_calculated_summary_section, + list_item_id=list_item_id, + list_name=list_name, + ), + routing_path, + ): + return url_for( + "questionnaire.block", + block_id=return_location.return_to_block_id, + list_item_id=list_item_id, + list_name=list_name, + _anchor=return_location.return_to_answer_id, + ) + # since the above may define a different routing_path, + # retrieval of the next incomplete block needs to be here instead of returning None and allowing default behaviour + return self._get_return_url_for_inaccessible_location( + is_for_previous=is_for_previous, + return_location=return_location, + routing_path=routing_path, + ) + + def _get_return_to_for_calculated_summary( + self, + *, + return_location: ReturnLocation, + location: LocationType, + routing_path: RoutingPath, + ) -> str | None: + """ + The return url for a calculated summary varies based on whether it's standalone or part of a grand calculated summary + + If the user goes from GrandCalculatedSummary -> CalculatedSummary -> Question, then return_to_block_ids needs to be a list + so that both the calculated summary id and the grand calculated summary ids are stored. + """ + block_id = None + remaining: list[str] = [] + # for a calculated summary this might have multiple items, e.g. a calculated summary to go to and then a grand calculated one + if return_location.return_to_block_id: + # the first item is the block id to route to (e.g. a calculated summary to go back to first) + # anything remaining forms where to go next (e.g. a grand calculated summary) + block_id, *remaining = return_location.return_to_block_id.split(",") + + if self.can_access_location( + Location( + block_id=block_id, + section_id=location.section_id, + list_item_id=location.list_item_id, + ), + routing_path, + ): + # if the next location is valid, the new url is that location, and the new 'return to block id' is just what remains + return_to_block_id = ",".join(remaining) if remaining else None + + # remove first item and return the remaining ones + # Type ignore: return_location.return_to will always be populated at this point + return_to = ",".join(return_location.return_to.split(",")[1:]) or None # type: ignore + return_to_answer_ids = [] + anchor = None + + if return_location.return_to_answer_id: + ( + anchor, + *return_to_answer_ids, + ) = return_location.return_to_answer_id.split(",") + + return_to_answer_id = ( + ",".join(return_to_answer_ids) if return_to_answer_ids else None ) - if return_to == "final-summary" and self.is_questionnaire_complete: return url_for( - "questionnaire.submit_questionnaire", _anchor=return_to_answer_id + "questionnaire.block", + block_id=block_id, + list_name=location.list_name, + list_item_id=location.list_item_id, + return_to=return_to, + return_to_block_id=return_to_block_id, + return_to_list_item_id=return_location.return_to_list_item_id, + return_to_answer_id=return_to_answer_id, + _anchor=anchor, + ) + + def _get_return_url_for_inaccessible_location( + self, + *, + is_for_previous: bool, + return_location: ReturnLocation, + routing_path: RoutingPath, + ) -> str | None: + """ + Routes to the next incomplete block in the section and preserves return to parameters + but only when routing forwards, returns None in the case of the previous link + """ + if ( + not is_for_previous + and return_location.return_to + and ( + next_incomplete_location := self._get_first_incomplete_location_in_section( + routing_path + ) + ) + ): + return next_incomplete_location.url( + **return_location.to_dict(), ) def get_next_location_url_for_end_of_section(self) -> str: @@ -220,9 +428,7 @@ def get_next_location_url_for_end_of_section(self) -> str: return self.get_first_incomplete_location_in_questionnaire_url() def get_section_resume_url(self, routing_path: RoutingPath) -> str: - section_key = (routing_path.section_id, routing_path.list_item_id) - - if section_key in self._progress_store: + if routing_path.section_key in self._data_stores.progress_store: location = self._get_first_incomplete_location_in_section(routing_path) if location: return location.url(resume=True) @@ -251,36 +457,28 @@ def get_last_location_in_section(routing_path: RoutingPath) -> Location: ) def full_routing_path(self) -> list[RoutingPath]: - full_routing_path = [] + full_routing_path: list[RoutingPath] = [] for section_id in self.enabled_section_ids: repeating_list = self._schema.get_repeating_list_for_section(section_id) if repeating_list: - for list_item_id in self._list_store[repeating_list]: - full_routing_path.append( - self._path_finder.routing_path( - section_id=section_id, list_item_id=list_item_id - ) - ) + full_routing_path.extend( + self._path_finder.routing_path(SectionKey(section_id, list_item_id)) + for list_item_id in self._data_stores.list_store[repeating_list] + ) else: full_routing_path.append( - self._path_finder.routing_path(section_id=section_id) + self._path_finder.routing_path(SectionKey(section_id)) ) return full_routing_path - def _is_block_complete( - self, block_id: str, section_id: str, list_item_id: str - ) -> bool: - return block_id in self._progress_store.get_completed_block_ids( - section_id, list_item_id - ) - def _get_first_incomplete_location_in_section( self, routing_path: RoutingPath - ) -> Location: + ) -> Location | None: for block_id in routing_path: - if not self._is_block_complete( - block_id, routing_path.section_id, routing_path.list_item_id + if not self._data_stores.progress_store.is_block_complete( + block_id=block_id, + section_key=routing_path.section_key, ): return Location( block_id=block_id, @@ -293,92 +491,86 @@ def _get_allowable_path(self, routing_path: RoutingPath) -> list[str]: """ The allowable path is the completed path plus the next location """ - allowable_path = [] + allowable_path: list[str] = [] if routing_path: for block_id in routing_path: allowable_path.append(block_id) - if not self._is_block_complete( - block_id, routing_path.section_id, routing_path.list_item_id + if not self._data_stores.progress_store.is_block_complete( + block_id=block_id, + section_key=routing_path.section_key, ): return allowable_path return allowable_path - def get_enabled_section_keys( + def _get_enabled_section_keys( self, - ) -> Generator[SectionKeyType, None, None]: + ) -> Generator[SectionKey, None, None]: for section_id in self.enabled_section_ids: - repeating_list = self._schema.get_repeating_list_for_section(section_id) - - if repeating_list: - for list_item_id in self._list_store[repeating_list]: - section_key: SectionKeyType = (section_id, list_item_id) - yield section_key + if repeating_list := self._schema.get_repeating_list_for_section( + section_id + ): + for list_item_id in self._data_stores.list_store[repeating_list]: + yield SectionKey(section_id, list_item_id) else: - section_key = (section_id, None) - yield section_key + yield SectionKey(section_id) - def _get_first_incomplete_section_key(self) -> tuple[str, Optional[str]]: - for section_id, list_item_id in self.get_enabled_section_keys(): - if not self._progress_store.is_section_complete(section_id, list_item_id): - return section_id, list_item_id + def _get_first_incomplete_section_key(self) -> SectionKey | None: + for section_key in self._get_enabled_section_keys(): + if not self._data_stores.progress_store.is_section_complete(section_key): + return section_key - def _get_last_complete_section_key(self) -> tuple[str, Optional[str]]: - for section_id, list_item_id in list(self.get_enabled_section_keys())[::-1]: - if self._progress_store.is_section_complete(section_id, list_item_id): - return section_id, list_item_id + def _get_last_complete_section_key(self) -> SectionKey | None: + for section_key in list(self._get_enabled_section_keys())[::-1]: + if self._data_stores.progress_store.is_section_complete(section_key): + return section_key def _is_section_enabled(self, section: Mapping) -> bool: if "enabled" not in section: return True enabled = section["enabled"] - if isinstance(enabled, dict): - when_rule_evaluator = RuleEvaluator( - self._schema, - self._answer_store, - self._list_store, - self._metadata, - self._response_metadata, - location=None, - routing_path_block_ids=None, - ) + section_id = section["id"] - return bool(when_rule_evaluator.evaluate(enabled["when"])) + routing_path_block_ids = self._path_finder.get_when_rules_block_dependencies( + section_id + ) - return any( - evaluate_when_rules( - condition["when"], - self._schema, - self._metadata, - self._answer_store, - self._list_store, - ) - for condition in enabled + when_rule_evaluator = RuleEvaluator( + data_stores=self._data_stores, + schema=self._schema, + location=Location(section_id=section_id), + routing_path_block_ids=routing_path_block_ids, ) + return bool(when_rule_evaluator.evaluate(enabled["when"])) + @staticmethod - def get_next_block_url( - location: Location, routing_path: RoutingPath, **kwargs: Optional[str] + def _get_next_block_url( + location: LocationType, + routing_path: RoutingPath, + return_location: ReturnLocation, ) -> str: - next_block_id = routing_path[routing_path.index(location.block_id) + 1] + # Type ignore: the location will have a block + next_block_id = routing_path[routing_path.index(location.block_id) + 1] # type: ignore return url_for( "questionnaire.block", block_id=next_block_id, list_name=routing_path.list_name, list_item_id=routing_path.list_item_id, - **kwargs, + _external=False, + **return_location.to_dict(), ) @staticmethod def _get_section_url( - location: Location, return_to_answer_id: Optional[str] = None + section_key: SectionKey, + return_to_answer_id: str | None = None, ) -> str: return url_for( "questionnaire.get_section", - section_id=location.section_id, - list_item_id=location.list_item_id, _anchor=return_to_answer_id, + **section_key.to_dict(), ) diff --git a/app/questionnaire/routing_path.py b/app/questionnaire/routing_path.py index cc0583c545..92d0ca9449 100644 --- a/app/questionnaire/routing_path.py +++ b/app/questionnaire/routing_path.py @@ -1,25 +1,37 @@ +from typing import Iterator, SupportsIndex + +from app.utilities.types import SectionKey + + class RoutingPath: """Holds a list of block_ids and has section_id, list_item_id and list_name attributes""" - def __init__(self, block_ids, section_id, list_item_id=None, list_name=None): + def __init__( + self, + *, + block_ids: list[str], + section_id: str, + list_item_id: str | None = None, + list_name: str | None = None, + ): self.block_ids = tuple(block_ids) self.section_id = section_id self.list_item_id = list_item_id self.list_name = list_name - def __len__(self): + def __len__(self) -> int: return len(self.block_ids) - def __getitem__(self, index): + def __getitem__(self, index: int) -> str: return self.block_ids[index] - def __iter__(self): + def __iter__(self) -> Iterator[str]: return iter(self.block_ids) - def __reversed__(self): + def __reversed__(self) -> Iterator[str]: return reversed(self.block_ids) - def __eq__(self, other): + def __eq__(self, other: object) -> bool: if isinstance(other, RoutingPath): return ( self.block_ids == other.block_ids @@ -33,5 +45,9 @@ def __eq__(self, other): return self.block_ids == other - def index(self, *args): - return self.block_ids.index(*args) + def index(self, value: str, *args: SupportsIndex) -> int: + return self.block_ids.index(value, *args) + + @property + def section_key(self) -> SectionKey: + return SectionKey(self.section_id, self.list_item_id) diff --git a/app/questionnaire/rules/helpers.py b/app/questionnaire/rules/helpers.py index 5db0abb155..3c6bf24ac9 100644 --- a/app/questionnaire/rules/helpers.py +++ b/app/questionnaire/rules/helpers.py @@ -1,12 +1,12 @@ from datetime import datetime from decimal import Decimal from functools import wraps -from typing import Any, Callable, Sequence, Union +from typing import Any, Callable, Sequence -ValueTypes = Union[bool, str, int, float, Decimal, None, datetime] +ValueTypes = bool | str | int | float | Decimal | None | datetime -def _casefold(value: Union[list, ValueTypes]) -> Union[list, ValueTypes]: +def _casefold(value: list | ValueTypes) -> list | ValueTypes: if isinstance(value, str): return value.casefold() diff --git a/app/questionnaire/rules/operations.py b/app/questionnaire/rules/operations.py index 0c2953f939..a821001dd0 100644 --- a/app/questionnaire/rules/operations.py +++ b/app/questionnaire/rules/operations.py @@ -1,3 +1,4 @@ +from collections.abc import Sized from copy import deepcopy from datetime import date from decimal import Decimal @@ -5,12 +6,10 @@ TYPE_CHECKING, Iterable, Mapping, - Optional, Sequence, - Sized, + TypeAlias, TypedDict, TypeVar, - Union, ) from babel.dates import format_datetime @@ -29,7 +28,7 @@ ) ComparableValue = TypeVar("ComparableValue", str, int, float, Decimal, date) -NonArrayPrimitiveTypes = Union[str, int, float, Decimal, None] +NonArrayPrimitiveTypes: TypeAlias = str | int | float | Decimal | None DAYS_OF_WEEK = { "MONDAY": 0, @@ -56,6 +55,8 @@ class Operations: A class to group the operations """ + NEGATIVE_DAYS_OFFSET_ERROR_MESSAGE = "Negative days offset must be less than or equal to -7 when used with `day_of_week` offset" + def __init__( self, language: str, @@ -108,7 +109,7 @@ def evaluate_or(values: Iterable[bool]) -> bool: return any(iter(values)) @staticmethod - def evaluate_count(values: Optional[Sized]) -> int: + def evaluate_count(values: Sized | None) -> int: return len(values or []) @staticmethod @@ -132,10 +133,10 @@ def evaluate_any_in(lhs: Sequence, rhs: Sequence) -> bool: @staticmethod def resolve_date_from_string( - date_string: Optional[str], - offset: Optional[DateOffset] = None, + date_string: str | None, + offset: DateOffset | None = None, offset_by_full_weeks: bool = False, - ) -> Optional[date]: + ) -> date | None: datetime_value = parse_datetime(date_string) if not datetime_value: return None @@ -147,9 +148,7 @@ def resolve_date_from_string( if day_of_week_offset := offset.get("day_of_week"): if 0 > days_offset > -7: - raise ValueError( - "Negative days offset must be less than or equal to -7 when used with `day_of_week` offset" - ) + raise ValueError(Operations.NEGATIVE_DAYS_OFFSET_ERROR_MESSAGE) days_difference = ( value_as_date.weekday() - DAYS_OF_WEEK[day_of_week_offset] @@ -186,14 +185,12 @@ def format_date(self, date_to_format: date, date_format: str) -> str: def _resolve_self_reference( self, - self_reference_value: Union[ValueSourceTypes, date], - operands: Sequence[Union[ValueSourceTypes, date]], - ) -> list[Union[ValueSourceTypes, date]]: + self_reference_value: ValueSourceTypes | date, + operands: Sequence[ValueSourceTypes | date], + ) -> list[ValueSourceTypes | date]: resolved_operands = [] for operand in operands: - if isinstance(operand, dict) and any( - operator in operand for operator in OPERATION_MAPPING - ): + if isinstance(operand, dict) and QuestionnaireSchema.has_operator(operand): operator_name = next(iter(operand)) resolved_nested_operands = self._resolve_self_reference( self_reference_value, operand[operator_name] @@ -201,7 +198,6 @@ def _resolve_self_reference( resolved_value = getattr(self, OPERATION_MAPPING[operator_name])( *resolved_nested_operands ) - else: resolved_value = ( self_reference_value if operand == SELF_REFERENCE_KEY else operand @@ -214,7 +210,7 @@ def _resolve_self_reference( def evaluate_map( self, function: Mapping[str, list], - iterables: Sequence[Union[ValueSourceTypes, date]], + iterables: Sequence[ValueSourceTypes | date], ) -> list[str]: function_operator = next(iter(function)) function_operands = deepcopy(function[function_operator]) @@ -231,7 +227,7 @@ def evaluate_map( def evaluate_option_label_from_value(self, value: str, answer_id: str) -> str: answers = self.schema.get_answers_by_answer_id(answer_id) - label_options: Union[str, dict] = [ + label_options: str | dict = [ options["label"] for answer in answers for options in answer["options"] @@ -243,5 +239,13 @@ def evaluate_option_label_from_value(self, value: str, answer_id: str) -> str: else: label = self.renderer.render_placeholder(label_options, list_item_id=None) - return label + + def evaluate_sum(self, *args: tuple) -> int | float | Decimal: + """recursively evaluate the sum of any list-like arguments""" + return sum( + # Cannot use Iterable or Sequence as the type check for value as this would include primitive types like str + self.evaluate_sum(*value) if isinstance(value, (list, tuple)) else value + for value in args + if isinstance(value, (int, float, Decimal, list, tuple)) + ) diff --git a/app/questionnaire/rules/operations_helper.py b/app/questionnaire/rules/operations_helper.py index 40dd7c686a..9bce3a555c 100644 --- a/app/questionnaire/rules/operations_helper.py +++ b/app/questionnaire/rules/operations_helper.py @@ -3,8 +3,9 @@ The methods here invoke operations methods, so it can be imported in placeholder transformer this is a temporary solution until placeholder transformer is refactored. """ + from datetime import date -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from app.questionnaire.questionnaire_schema import QuestionnaireSchema from app.questionnaire.rules.operations import DateOffset, Operations @@ -29,10 +30,10 @@ def __init__( def string_to_datetime( self, - date_string: Optional[str], - offset: Optional[DateOffset] = None, + date_string: str | None, + offset: DateOffset | None = None, offset_by_full_weeks: bool = False, - ) -> Optional[date]: + ) -> date | None: return self.ops.resolve_date_from_string( date_string, offset, offset_by_full_weeks ) diff --git a/app/questionnaire/rules/operator.py b/app/questionnaire/rules/operator.py index 12150c86bf..b387db11f3 100644 --- a/app/questionnaire/rules/operator.py +++ b/app/questionnaire/rules/operator.py @@ -1,5 +1,5 @@ from datetime import date -from typing import TYPE_CHECKING, Generator, Iterable, Optional, Sequence, Union +from typing import TYPE_CHECKING, Generator, Iterable, Sequence from app.questionnaire.rules.helpers import ValueTypes @@ -26,6 +26,7 @@ class Operator: FORMAT_DATE = "format-date" MAP = "map" OPTION_LABEL_FROM_VALUE = "option-label-from-value" + SUM = "+" def __init__(self, name: str, operations: "Operations") -> None: self.name = name @@ -39,15 +40,13 @@ def __init__(self, name: str, operations: "Operations") -> None: Operator.ANY_IN, } - def evaluate( - self, operands: Union[Generator, Iterable] - ) -> Union[bool, Optional[date]]: + def evaluate(self, operands: Generator | Iterable) -> bool | date | None: if self._ensure_operands_not_none: operands = list(operands) if self._any_operands_none(*operands): return False - value: Union[bool, Optional[date]] = ( + value: bool | date | None = ( self._operation(operands) if self.name in {Operator.AND, Operator.OR} else self._operation(*operands) @@ -55,7 +54,7 @@ def evaluate( return value @staticmethod - def _any_operands_none(*operands: Union[Sequence, ValueTypes]) -> bool: + def _any_operands_none(*operands: Sequence | ValueTypes) -> bool: return any(operand is None for operand in operands) @@ -78,4 +77,5 @@ def _any_operands_none(*operands: Union[Sequence, ValueTypes]) -> bool: Operator.FORMAT_DATE: "format_date", Operator.MAP: "evaluate_map", Operator.OPTION_LABEL_FROM_VALUE: "evaluate_option_label_from_value", + Operator.SUM: "evaluate_sum", } diff --git a/app/questionnaire/rules/rule_evaluator.py b/app/questionnaire/rules/rule_evaluator.py index c6f0be0d39..6abff8f571 100644 --- a/app/questionnaire/rules/rule_evaluator.py +++ b/app/questionnaire/rules/rule_evaluator.py @@ -1,53 +1,49 @@ from dataclasses import dataclass from datetime import date -from typing import Generator, Iterable, Mapping, Optional, Sequence, Union +from decimal import Decimal +from typing import Generator, Iterable, Sequence, TypeAlias -from app.data_models import AnswerStore, ListStore -from app.questionnaire import Location, QuestionnaireSchema +from app.data_models.data_stores import DataStores +from app.questionnaire import QuestionnaireSchema from app.questionnaire.placeholder_renderer import PlaceholderRenderer from app.questionnaire.questionnaire_schema import DEFAULT_LANGUAGE_CODE -from app.questionnaire.relationship_location import RelationshipLocation from app.questionnaire.rules.operations import Operations -from app.questionnaire.rules.operator import OPERATION_MAPPING, Operator +from app.questionnaire.rules.operator import Operator from app.questionnaire.value_source_resolver import ( ValueSourceResolver, ValueSourceTypes, ) +from app.utilities.types import LocationType -RuleEvaluatorTypes = Union[bool, Optional[date], list[str], list[date]] +RuleEvaluatorTypes: TypeAlias = ( + bool | date | list[str] | list[date] | int | float | Decimal | None +) +ResolvedOperand: TypeAlias = bool | date | ValueSourceTypes | None @dataclass class RuleEvaluator: schema: QuestionnaireSchema - answer_store: AnswerStore - list_store: ListStore - metadata: Mapping - response_metadata: Mapping - location: Union[None, Location, RelationshipLocation] - routing_path_block_ids: Optional[list] = None + data_stores: DataStores + location: LocationType | None + routing_path_block_ids: Iterable[str] | None = None language: str = DEFAULT_LANGUAGE_CODE # pylint: disable=attribute-defined-outside-init def __post_init__(self) -> None: list_item_id = self.location.list_item_id if self.location else None self.value_source_resolver = ValueSourceResolver( - answer_store=self.answer_store, - list_store=self.list_store, - metadata=self.metadata, - response_metadata=self.response_metadata, + data_stores=self.data_stores, schema=self.schema, location=self.location, list_item_id=list_item_id, routing_path_block_ids=self.routing_path_block_ids, use_default_answer=True, ) + renderer: PlaceholderRenderer = PlaceholderRenderer( language=self.language, - answer_store=self.answer_store, - list_store=self.list_store, - metadata=self.metadata, - response_metadata=self.response_metadata, + data_stores=self.data_stores, schema=self.schema, location=self.location, ) @@ -55,17 +51,16 @@ def __post_init__(self) -> None: language=self.language, schema=self.schema, renderer=renderer ) - def _evaluate(self, rule: dict[str, Sequence]) -> Union[bool, Optional[date]]: + def _evaluate(self, rule: dict[str, Sequence]) -> bool | date | None: operator_name = next(iter(rule)) operator = Operator(operator_name, self.operations) operands = rule[operator_name] + operands_rule_error_message = f"The rule is invalid, operands should be of type Sequence and not {type(operands)}" if not isinstance(operands, Sequence): - raise TypeError( - f"The rule is invalid, operands should be of type Sequence and not {type(operands)}" - ) + raise TypeError(operands_rule_error_message) - resolved_operands: Iterable[Union[bool, Optional[date], ValueSourceTypes]] + resolved_operands: Iterable[ResolvedOperand] if operator_name == Operator.MAP: resolved_iterables = self._resolve_operand(operands[1]) @@ -75,22 +70,18 @@ def _evaluate(self, rule: dict[str, Sequence]) -> Union[bool, Optional[date]]: return operator.evaluate(resolved_operands) - def _resolve_operand( - self, operand: ValueSourceTypes - ) -> Union[bool, Optional[date], ValueSourceTypes]: + def _resolve_operand(self, operand: ValueSourceTypes) -> ResolvedOperand: if isinstance(operand, dict) and "source" in operand: return self.value_source_resolver.resolve(operand) - if isinstance(operand, dict) and any( - operator in operand for operator in OPERATION_MAPPING - ): + if QuestionnaireSchema.has_operator(operand) and isinstance(operand, dict): return self._evaluate(operand) return operand def get_resolved_operands( self, operands: Sequence[ValueSourceTypes] - ) -> Generator[Union[bool, Optional[date], ValueSourceTypes], None, None]: + ) -> Generator[ResolvedOperand, None, None]: for operand in operands: yield self._resolve_operand(operand) diff --git a/app/questionnaire/rules/utils.py b/app/questionnaire/rules/utils.py index c23d2d4769..cac041a93e 100644 --- a/app/questionnaire/rules/utils.py +++ b/app/questionnaire/rules/utils.py @@ -1,5 +1,5 @@ from datetime import datetime, timezone -from typing import Optional, overload +from typing import overload from dateutil import parser @@ -9,16 +9,14 @@ def parse_iso_8601_datetime(iso_8601_date_string: str) -> datetime: @overload -def parse_datetime(date_string: None) -> None: - ... # pragma: no cover +def parse_datetime(date_string: None) -> None: ... # pragma: no cover @overload -def parse_datetime(date_string: str) -> datetime: - ... # pragma: no cover +def parse_datetime(date_string: str) -> datetime: ... # pragma: no cover -def parse_datetime(date_string: Optional[str]) -> Optional[datetime]: +def parse_datetime(date_string: str | None) -> datetime | None: """ :param date_string: string representing a date :return: datetime of that date string @@ -26,6 +24,7 @@ def parse_datetime(date_string: Optional[str]) -> Optional[datetime]: Convert `date` from string into `datetime` object. `date` can be 'YYYY-MM-DD', 'YYYY-MM','now' or ISO 8601 format. Note that in the shorthand YYYY-MM format, day_of_month is assumed to be 1. """ + date_format_error_message = f"'{date_string}' is not in a valid date format" if not date_string: return None @@ -35,4 +34,4 @@ def parse_datetime(date_string: Optional[str]) -> Optional[datetime]: try: return parse_iso_8601_datetime(date_string) except ValueError as ex: - raise ValueError(f"'{date_string}' is not in a valid date format") from ex + raise ValueError(date_format_error_message) from ex diff --git a/app/questionnaire/schema_utils.py b/app/questionnaire/schema_utils.py index 985ba02015..967c5fa1c8 100644 --- a/app/questionnaire/schema_utils.py +++ b/app/questionnaire/schema_utils.py @@ -1,4 +1,11 @@ -def find_pointers_containing(input_data, search_key, pointer=None): +from typing import Generator, Iterable, Mapping + + +def find_pointers_containing( + input_data: Mapping | Iterable[Mapping], + search_key: str, + pointer: str | None = None, +) -> Generator[str, None, None]: """ Recursive function which lists pointers which contain a search key @@ -11,9 +18,7 @@ def find_pointers_containing(input_data, search_key, pointer=None): if search_key in input_data: yield pointer or "" for k, v in input_data.items(): - if (isinstance(v, dict)) and search_key in v: - yield pointer + "/" + k if pointer else "/" + k - else: + if isinstance(v, (list, tuple, dict)): yield from find_pointers_containing( v, search_key, pointer + "/" + k if pointer else "/" + k ) @@ -22,10 +27,13 @@ def find_pointers_containing(input_data, search_key, pointer=None): yield from find_pointers_containing(item, search_key, f"{pointer}/{index}") -def get_answer_ids_in_block(block): - question = block["question"] - answer_ids = [] - for answer in question["answers"]: - answer_ids.append(answer["id"]) +def get_answers_from_question(question: Mapping) -> list: + static_answers = question.get("answers", []) + dynamic_answers = question.get("dynamic_answers", {}).get("answers", []) + return [*dynamic_answers, *static_answers] + +def get_answer_ids_in_block(block: Mapping) -> list[str]: + question = block["question"] + answer_ids = [answer["id"] for answer in get_answers_from_question(question)] return answer_ids diff --git a/app/questionnaire/value_source_resolver.py b/app/questionnaire/value_source_resolver.py index 028c58852b..d00363090a 100644 --- a/app/questionnaire/value_source_resolver.py +++ b/app/questionnaire/value_source_resolver.py @@ -1,51 +1,78 @@ from dataclasses import dataclass from decimal import Decimal -from typing import Any, Callable, Iterable, Mapping, Optional, Union +from typing import Callable, Iterable, Mapping, TypeAlias from markupsafe import Markup - -from app.data_models.answer import AnswerValueTypes, escape_answer_value -from app.data_models.answer_store import AnswerStore -from app.data_models.list_store import ListModel, ListStore -from app.questionnaire import Location, QuestionnaireSchema -from app.questionnaire.location import InvalidLocationException -from app.questionnaire.relationship_location import RelationshipLocation - -ValueSourceTypes = Union[None, str, int, Decimal, list] -ValueSourceEscapedTypes = Union[ - Markup, - list[Markup], -] -IntOrDecimal = Union[int, Decimal] +from werkzeug.datastructures import ImmutableDict + +from app.data_models.answer import ( + AnswerValueEscapedTypes, + AnswerValueTypes, + escape_answer_value, +) +from app.data_models.data_stores import DataStores +from app.data_models.list_store import ListModel +from app.data_models.metadata_proxy import NoMetadataException +from app.questionnaire import QuestionnaireSchema +from app.questionnaire.location import InvalidLocationException, SectionKey +from app.questionnaire.rules import rule_evaluator # pylint: disable=cyclic-import +from app.utilities.types import LocationType + +ValueSourceTypes: TypeAlias = None | str | int | Decimal | list | dict +ValueSourceEscapedTypes: TypeAlias = Markup | list[Markup] +IntOrDecimal: TypeAlias = int | Decimal +ResolvedAnswerList: TypeAlias = list[AnswerValueTypes | AnswerValueEscapedTypes | None] @dataclass class ValueSourceResolver: - answer_store: AnswerStore - list_store: ListStore - metadata: Mapping - response_metadata: Mapping + SELECTOR_LOCATION_ERROR_MESSAGE = ( + "list_item_selector source location used without location" + ) + LOCATION_REQUIRED_ERROR_MESSAGE = "location is required to resolve block progress" + + data_stores: DataStores schema: QuestionnaireSchema - location: Union[None, Location, RelationshipLocation] - list_item_id: Optional[str] - routing_path_block_ids: Optional[list] = None + location: LocationType | None + list_item_id: str | None + routing_path_block_ids: Iterable[str] | None = None use_default_answer: bool = False escape_answer_values: bool = False + assess_routing_path: bool | None = True def _is_answer_on_path(self, answer_id: str) -> bool: if self.routing_path_block_ids: block = self.schema.get_block_for_answer_id(answer_id) - return block is not None and block["id"] in self.routing_path_block_ids - + return block is not None and self._is_block_on_path(block["id"]) return True + def _is_block_on_path(self, block_id: str) -> bool: + # other usages of this function than _is_answer_on_path don't have this check so require it here + if not self.routing_path_block_ids: + return True + + # repeating blocks aren't on the path, so check the parent list collector + if block_id in self.schema.list_collector_repeating_block_ids: + return self.schema.parent_id_map[block_id] in self.routing_path_block_ids + + return block_id in self.routing_path_block_ids + def _get_answer_value( - self, answer_id: str, list_item_id: Optional[str] - ) -> Optional[AnswerValueTypes]: - if not self._is_answer_on_path(answer_id): + self, + answer_id: str, + list_item_id: str | None, + assess_routing_path: bool | None = None, + ) -> AnswerValueTypes | None: + assess_routing_path = ( + assess_routing_path + if assess_routing_path is not None + else self.assess_routing_path + ) + + if assess_routing_path and not self._is_answer_on_path(answer_id): return None - if answer := self.answer_store.get_answer(answer_id, list_item_id): + if answer := self.data_stores.answer_store.get_answer(answer_id, list_item_id): return answer.value if self.use_default_answer and ( @@ -53,44 +80,112 @@ def _get_answer_value( ): return answer.value + def _resolve_list_item_id_for_answer_id(self, answer_id: str) -> str | None: + """ + If there's a list item id and the answer is repeating, return the list item id to resolve the instance of the answer + However if the answer is repeating for a different list, return None so that the repeating answer id resolves to a list + """ + if self.list_item_id and ( + list_name_for_answer := self.schema.get_list_name_for_answer_id(answer_id) + ): + # if there is a current list, and it differs to the repeating answer one, return None + if ( + self.location + and self.location.list_name + and self.location.list_name != list_name_for_answer + ): + return None + return self.list_item_id + def _resolve_list_item_id_for_value_source( self, value_source: Mapping - ) -> Optional[str]: - list_item_id: Optional[str] = None - + ) -> str | None: if list_item_selector := value_source.get("list_item_selector"): if list_item_selector["source"] == "location": if not self.location: - raise InvalidLocationException( - "list_item_selector source location used without location" - ) - - list_item_id = getattr(self.location, list_item_selector["identifier"]) + raise InvalidLocationException(self.SELECTOR_LOCATION_ERROR_MESSAGE) + # Type ignore: the identifier is a string, same below + return getattr(self.location, list_item_selector["identifier"]) # type: ignore - elif list_item_selector["source"] == "list": - list_item_id = getattr( - self.list_store[list_item_selector["identifier"]], + if list_item_selector["source"] == "list": + return getattr( # type: ignore + self.data_stores.list_store[list_item_selector["identifier"]], list_item_selector["selector"], ) - if list_item_id: - return list_item_id + if value_source["source"] == "supplementary_data": + return ( + self.list_item_id + if self.data_stores.supplementary_data_store.is_data_repeating( + value_source["identifier"] + ) + else None + ) + + if value_source["source"] == "answers": + return self._resolve_list_item_id_for_answer_id(value_source["identifier"]) + + def _resolve_repeating_answers_for_list( + self, *, answer_id: str, list_name: str + ) -> ResolvedAnswerList: + """Return the list of answers in answer store that correspond to the given list name and dynamic/repeating answer_id""" + answer_values: ResolvedAnswerList = [] + for list_item_id in self.data_stores.list_store[list_name]: + answer_value = self._get_answer_value( + answer_id=answer_id, list_item_id=list_item_id + ) + if answer_value is not None: + answer_values.append( + escape_answer_value(answer_value) + if self.escape_answer_values + else answer_value + ) + return answer_values + + def _resolve_dynamic_answers( + self, + answer_id: str, + ) -> ResolvedAnswerList | None: + # Type ignore: block must exist for this function to be called + question = self.schema.get_block_for_answer_id(answer_id).get("question", {}) # type: ignore + dynamic_answers = question["dynamic_answers"] + values = dynamic_answers["values"] + if values["source"] == "list": + return self._resolve_repeating_answers_for_list( + answer_id=answer_id, list_name=values["identifier"] + ) - return ( - self.list_item_id - if self.list_item_id - and self.schema.is_repeating_answer(value_source["identifier"]) - else None + def _resolve_list_repeating_block_answers( + self, answer_id: str + ) -> ResolvedAnswerList: + # Type ignore: block must exist for this function to be called + repeating_block: ImmutableDict = self.schema.get_block_for_answer_id(answer_id) # type: ignore + list_name = self.schema.list_names_by_list_repeating_block_id[ + repeating_block["id"] + ] + return self._resolve_repeating_answers_for_list( + answer_id=answer_id, list_name=list_name ) def _resolve_answer_value_source( self, value_source: Mapping - ) -> Union[ValueSourceEscapedTypes, ValueSourceTypes]: + ) -> ValueSourceEscapedTypes | ValueSourceTypes: + """resolves answer value by first checking if the answer is dynamic whilst not in a repeating section, + which indicates that it is a repeating answer resolving to a list. Otherwise, retrieve answer value as normal. + """ list_item_id = self._resolve_list_item_id_for_value_source(value_source) answer_id = value_source["identifier"] + # if not in a repeating section and the id is for a list of dynamic/repeating block answers, then return the list of values + if not list_item_id: + if self.schema.is_answer_dynamic(answer_id): + return self._resolve_dynamic_answers(answer_id) + if self.schema.is_answer_in_list_collector_repeating_block(answer_id): + return self._resolve_list_repeating_block_answers(answer_id) + answer_value = self._get_answer_value( - answer_id=answer_id, list_item_id=list_item_id + answer_id=answer_id, + list_item_id=list_item_id, ) if isinstance(answer_value, Mapping): @@ -105,65 +200,145 @@ def _resolve_answer_value_source( return answer_value - def _resolve_list_value_source( + def _resolve_progress_value_source( self, value_source: Mapping - ) -> Union[int, str, list]: + ) -> ValueSourceEscapedTypes | ValueSourceTypes | None: + identifier = value_source["identifier"] + selector = value_source["selector"] + if selector == "section": + # List item id is set to None here as we do not support checking progress value sources for + # repeating sections + return self.data_stores.progress_store.get_section_status( + SectionKey(identifier) + ) + + if selector == "block": + if not self.location: + raise ValueError(self.LOCATION_REQUIRED_ERROR_MESSAGE) + + if not self._is_block_on_path(identifier): + return None + + # Type ignore: Section id will exist at this point + section_id_for_block: str = self.schema.get_section_id_for_block_id(identifier) # type: ignore + + return self.data_stores.progress_store.get_block_status( + block_id=identifier, + section_key=SectionKey( + section_id=section_id_for_block, + list_item_id=( + self.location.list_item_id + if self.location.section_id == section_id_for_block + else None + ), + ), + ) + + def _resolve_list_value_source(self, value_source: Mapping) -> int | str | list: identifier = value_source["identifier"] - list_model: ListModel = self.list_store[identifier] + list_model: ListModel = self.data_stores.list_store[identifier] if selector := value_source.get("selector"): - value: Union[str, list, int] = getattr(list_model, selector) + value: str | list | int = getattr(list_model, selector) return value return list(list_model) - def _resolve_calculated_summary_value_source( - self, value_source: Mapping - ) -> IntOrDecimal: - """Calculates the value for the 'calculation' used by the provided Calculated Summary. + def _resolve_summary_with_calculation( + self, value_source: Mapping, *, assess_routing_path: bool + ) -> IntOrDecimal | None: + """Calculates the value for the 'calculation' used by the provided Calculated or Grand Calculated Summary. - The caller is responsible for ensuring the provided Calculated Summary and its answers are on the path. + The caller is responsible for ensuring the provided summary and its components are on the path + or providing routing_path_block_ids when initialising the value source resolver. """ - calculated_summary_block: Mapping[str, Any] = self.schema.get_block(value_source["identifier"]) # type: ignore - calculation = calculated_summary_block["calculation"] - operator = self.get_calculation_operator(calculation["calculation_type"]) + summary_block: ImmutableDict = self.schema.get_block(value_source["identifier"]) # type: ignore + if not self._is_block_on_path(summary_block["id"]): + return None + + calculation = summary_block["calculation"] + # the calculation object for the old type of calculated summary block may contain answers_to_calculate instead of operation + if calculation.get("answers_to_calculate"): + operator = self.get_calculation_operator(calculation["calculation_type"]) + values = [ + self._get_answer_value( + answer_id=answer_id, + list_item_id=self._resolve_list_item_id_for_answer_id(answer_id), + assess_routing_path=assess_routing_path, + ) + for answer_id in calculation["answers_to_calculate"] + ] + return operator([value for value in values if value]) # type: ignore + + evaluator = rule_evaluator.RuleEvaluator( + self.schema, + data_stores=self.data_stores, + location=self.location, + routing_path_block_ids=self.routing_path_block_ids, + ) + + return evaluator.evaluate(calculation["operation"]) # type: ignore + + def _resolve_metadata_source(self, value_source: Mapping) -> str | None: + if not self.data_stores.metadata: + raise NoMetadataException + identifier = value_source["identifier"] + return self.data_stores.metadata[identifier] + + def _resolve_location_source(self, value_source: Mapping) -> str | None: + if value_source.get("identifier") == "list_item_id": + return self.list_item_id + + def _resolve_response_metadata_source(self, value_source: Mapping) -> str | None: + return self.data_stores.response_metadata.get(value_source.get("identifier")) + + def resolve_list(self, value_source_list: list[Mapping]) -> list[ValueSourceTypes]: + values: list[ValueSourceTypes] = [] + for value_source in value_source_list: + value = self.resolve(value_source) + if isinstance(value, list): + values.extend(value) + else: + values.append(value) + return values + + def _resolve_supplementary_data_source( + self, value_source: Mapping + ) -> ValueSourceTypes: list_item_id = self._resolve_list_item_id_for_value_source(value_source) - values = [ - self._get_answer_value(answer_id=answer_id, list_item_id=list_item_id) - for answer_id in calculation["answers_to_calculate"] - ] - return operator([value for value in values if value]) # type: ignore + + return self.data_stores.supplementary_data_store.get_data( + identifier=value_source["identifier"], + selectors=value_source.get("selectors"), + list_item_id=list_item_id, + ) @staticmethod def get_calculation_operator( calculation_type: str, ) -> Callable[[Iterable[IntOrDecimal]], IntOrDecimal]: + calculation_type_error_message = f"Invalid calculation_type: {calculation_type}" if calculation_type == "sum": return sum - - raise NotImplementedError(f"Invalid calculation_type: {calculation_type}") + raise NotImplementedError(calculation_type_error_message) def resolve( self, value_source: Mapping - ) -> Union[ValueSourceEscapedTypes, ValueSourceTypes]: + ) -> ValueSourceEscapedTypes | ValueSourceTypes: source = value_source["source"] - if source == "answers": - return self._resolve_answer_value_source(value_source) - - if source == "list": - return self._resolve_list_value_source(value_source) - - if source == "metadata": - return self.metadata.get(value_source.get("identifier")) - - if source == "location" and value_source.get("identifier") == "list_item_id": - # This does not use the location object because - # routes such as individual response does not have the concept of location. - return self.list_item_id - - if source == "response_metadata": - return self.response_metadata.get(value_source.get("identifier")) - - if source == "calculated_summary": - return self._resolve_calculated_summary_value_source(value_source) + if source in {"calculated_summary", "grand_calculated_summary"}: + return self._resolve_summary_with_calculation( + value_source=value_source, assess_routing_path=True + ) + resolve_method_mapping = { + "answers": self._resolve_answer_value_source, + "list": self._resolve_list_value_source, + "metadata": self._resolve_metadata_source, + "location": self._resolve_location_source, + "response_metadata": self._resolve_response_metadata_source, + "progress": self._resolve_progress_value_source, + "supplementary_data": self._resolve_supplementary_data_source, + } + + return resolve_method_mapping[source](value_source) diff --git a/app/questionnaire/variants.py b/app/questionnaire/variants.py index 8a42360d1b..971b9a6fd0 100644 --- a/app/questionnaire/variants.py +++ b/app/questionnaire/variants.py @@ -1,64 +1,49 @@ +from typing import Mapping + from werkzeug.datastructures import ImmutableDict +from app.data_models.data_stores import DataStores +from app.questionnaire.questionnaire_schema import QuestionnaireSchema from app.questionnaire.rules.rule_evaluator import RuleEvaluator -from app.questionnaire.when_rules import evaluate_when_rules - - -def choose_variant( - block, - schema, - metadata, - response_metadata, - answer_store, - list_store, - variants_key, - single_key, - current_location, -): +from app.utilities.types import LocationType + + +# Type ignore: validation should ensure the variant exists when this is called +def choose_variant( # type: ignore + block: Mapping, + schema: QuestionnaireSchema, + data_stores: DataStores, + variants_key: str, + single_key: str, + current_location: LocationType, +) -> dict: if block.get(single_key): - return block[single_key] + # Type ignore: the key passed in will be for a dictionary + return block[single_key] # type: ignore for variant in block.get(variants_key, []): - when_rules = variant.get("when", []) - - if isinstance(when_rules, dict): - when_rule_evaluator = RuleEvaluator( - schema, - answer_store, - list_store, - metadata, - response_metadata, - location=current_location, - ) - - if when_rule_evaluator.evaluate(when_rules): - return variant[single_key] - elif evaluate_when_rules( - when_rules, + when_rules = variant["when"] + + when_rule_evaluator = RuleEvaluator( schema, - metadata, - answer_store, - list_store, - current_location=current_location, - ): - return variant[single_key] + data_stores=data_stores, + location=current_location, + ) + + if when_rule_evaluator.evaluate(when_rules): + # Type ignore: question/content key is for a dictionary + return variant[single_key] # type: ignore def choose_question_to_display( - block, - schema, - metadata, - response_metadata, - answer_store, - list_store, - current_location, -): + block: ImmutableDict, + schema: QuestionnaireSchema, + data_stores: DataStores, + current_location: LocationType, +) -> dict: return choose_variant( block, schema, - metadata, - response_metadata, - answer_store, - list_store, + data_stores, variants_key="question_variants", single_key="question", current_location=current_location, @@ -66,21 +51,15 @@ def choose_question_to_display( def choose_content_to_display( - block, - schema, - metadata, - response_metadata, - answer_store, - list_store, - current_location, -): + block: ImmutableDict, + schema: QuestionnaireSchema, + data_stores: DataStores, + current_location: LocationType, +) -> dict: return choose_variant( block, schema, - metadata, - response_metadata, - answer_store, - list_store, + data_stores, variants_key="content_variants", single_key="content", current_location=current_location, @@ -88,23 +67,17 @@ def choose_content_to_display( def transform_variants( - block, - schema, - metadata, - response_metadata, - answer_store, - list_store, - current_location, -): + block: ImmutableDict, + schema: QuestionnaireSchema, + data_stores: DataStores, + current_location: LocationType, +) -> ImmutableDict: output_block = dict(block) if "question_variants" in block: question = choose_question_to_display( block, schema, - metadata, - response_metadata, - answer_store, - list_store, + data_stores, current_location, ) output_block.pop("question_variants", None) @@ -116,10 +89,7 @@ def transform_variants( content = choose_content_to_display( block, schema, - metadata, - response_metadata, - answer_store, - list_store, + data_stores, current_location, ) output_block.pop("content_variants", None) @@ -127,17 +97,14 @@ def transform_variants( output_block["content"] = content - if block["type"] in ("ListCollector", "PrimaryPersonListCollector"): + if block["type"] in {"ListCollector", "PrimaryPersonListCollector"}: list_operations = ["add_block", "edit_block", "remove_block"] for list_operation in list_operations: if block.get(list_operation): output_block[list_operation] = transform_variants( block[list_operation], schema, - metadata, - response_metadata, - answer_store, - list_store, + data_stores, current_location, ) diff --git a/app/questionnaire/when_rules.py b/app/questionnaire/when_rules.py deleted file mode 100644 index b999179798..0000000000 --- a/app/questionnaire/when_rules.py +++ /dev/null @@ -1,322 +0,0 @@ -import logging -from datetime import datetime, timezone -from typing import Optional - -from dateutil.relativedelta import relativedelta - -from app.data_models.answer import AnswerValueTypes -from app.questionnaire.rules.utils import parse_datetime - -MAX_REPEATS = 25 - -logger = logging.getLogger(__name__) - - -def evaluate_comparison_rule(when, answer_value, comparison_value): - """ - Determine whether a comparison rule will be satisfied based on an - answer value, and a value to compare it to. - :param when: The when clause to evaluate - :param answer_value: The value of the answer - :param comparison_value: The value to compare the answer to. - :return (bool): The result of the evaluation - """ - condition = when["condition"] - - return evaluate_condition(condition, answer_value, comparison_value) - - -def evaluate_rule(when, answer_value): - """ - Determine whether a rule will be satisfied based on a given answer - :param when: The when clause to evaluate - :param answer_value: The value of the answer - :return (bool): The result of the evaluation - """ - - match_value = when.get("value", when.get("values")) - - condition = when["condition"] - # Evaluate the condition on the routing rule - return evaluate_condition(condition, answer_value, match_value) - - -def evaluate_date_rule(when, answer_store, schema, metadata, answer_value): - date_comparison = when["date_comparison"] - - answer_value = parse_datetime(answer_value) - match_value = get_date_match_value(date_comparison, answer_store, schema, metadata) - condition = when.get("condition") - - if not answer_value or not match_value or not condition: - return False - - # Evaluate the condition on the routing rule - return evaluate_condition(condition, answer_value, match_value) - - -def evaluate_condition(condition, answer_value, match_value): - """ - :param condition: string representation of comparison operator - :param answer_value: the left hand side operand in the comparison - :param match_value: the right hand side operand in the comparison - :return: boolean value of comparing lhs and rhs using the specified operator - """ - answer_and_match = answer_value is not None and match_value is not None - - if condition in {"equals", "not equals", "equals any", "not equals any"}: - - answer_value = casefold(answer_value) - - if isinstance(match_value, (list, tuple)): - match_value = list(map(casefold, match_value)) - else: - match_value = casefold(match_value) - - comparison_operators = { - "equals": lambda answer_value, match_value: answer_value == match_value, - "not equals": lambda answer_value, match_value: answer_value != match_value, - "equals any": lambda answer_value, match_values: answer_value in match_values, - "not equals any": lambda answer_value, match_values: answer_value - not in match_values, - "contains": lambda answer_values, match_value: answer_and_match - and match_value in answer_values, - "not contains": lambda answer_values, match_value: answer_and_match - and match_value not in answer_values, - "contains any": lambda answer_values, match_values: answer_and_match - and any(match_value in answer_values for match_value in match_values), - "contains all": lambda answer_values, match_values: answer_and_match - and all(match_value in answer_values for match_value in match_values), - "set": lambda answer_value, _: answer_value not in (None, []), - "not set": lambda answer_value, _: answer_value in (None, []), - "greater than": lambda answer_value, match_value: answer_and_match - and answer_value > match_value, - "greater than or equal to": lambda answer_value, match_value: answer_and_match - and answer_value >= match_value, - "less than": lambda answer_value, match_value: answer_and_match - and answer_value < match_value, - "less than or equal to": lambda answer_value, match_value: answer_and_match - and answer_value <= match_value, - } - - match_function = comparison_operators[condition] - - return match_function(answer_value, match_value) - - -def casefold(value): - try: - return value.casefold() - except AttributeError: - return value - - -def get_date_match_value(date_comparison, answer_store, schema, metadata): - match_value = None - - if "value" in date_comparison: - if date_comparison["value"] == "now": - match_value = datetime.now(timezone.utc).strftime("%Y-%m-%d") - else: - match_value = date_comparison["value"] - elif "id" in date_comparison: - match_value = get_answer_value(date_comparison["id"], answer_store, schema) - elif "meta" in date_comparison: - match_value = get_metadata_value(metadata, date_comparison["meta"]) - - match_value = parse_datetime(match_value) - - if "offset_by" in date_comparison and match_value: - offset = date_comparison["offset_by"] - match_value = match_value + relativedelta( - days=offset.get("days", 0), - months=offset.get("months", 0), - years=offset.get("years", 0), - ) - - return match_value - - -def evaluate_goto( - goto_rule, - schema, - metadata, - answer_store, - list_store, - current_location, - routing_path_block_ids=None, -): - """ - Determine whether a goto rule will be satisfied based on a given answer - :param goto_rule: goto rule to evaluate - :param schema: survey schema - :param metadata: metadata for evaluating rules with metadata conditions - :param answer_store: store of answers to evaluate - :param list_store: store of lists to evaluate - :param current_location: the location to use when evaluating when rules - :param routing_path_block_ids: the routing path block ids used to evaluate if answer is on the path - :return: True if the when condition has been met otherwise False - """ - if "when" in goto_rule: - return evaluate_when_rules( - goto_rule["when"], - schema, - metadata, - answer_store, - list_store, - current_location, - routing_path_block_ids=routing_path_block_ids, - ) - return True - - -def _is_answer_on_path(schema, answer, routing_path_block_ids): - block_id = schema.get_block_for_answer_id(answer.answer_id)["id"] - return block_id in routing_path_block_ids - - -def _get_comparison_id_value( - when_rule, answer_store, schema, current_location=None, routing_path_block_ids=None -): - """ - Gets the value of a comparison id specified as an operand in a comparator - """ - if current_location and when_rule["comparison"]["source"] == "location": - try: - return getattr(current_location, when_rule["comparison"]["id"]) - except AttributeError: - return None - - answer_id = when_rule["comparison"]["id"] - list_item_id = current_location.list_item_id if current_location else None - - return get_answer_value( - answer_id, - answer_store, - schema, - list_item_id=list_item_id, - routing_path_block_ids=routing_path_block_ids, - ) - - -def _get_when_rule_value( - when_rule, - answer_store, - list_store, - schema, - metadata, - list_item_id=None, - routing_path_block_ids=None, -): - """ - Get the value from a when rule. - :raises: Exception if none of `id` or `meta` are provided. - :return: The value to use in a when rule - """ - if "id" in when_rule: - value = get_answer_value( - when_rule["id"], - answer_store, - schema, - list_item_id=list_item_id, - routing_path_block_ids=routing_path_block_ids, - ) - elif "meta" in when_rule: - value = get_metadata_value(metadata, when_rule["meta"]) - elif "id_selector" in when_rule: - value = getattr(list_store.get(when_rule["list"]), when_rule["id_selector"]) - elif "list" in when_rule: - value = get_list_count(list_store, when_rule["list"]) - else: - raise Exception("The when rule is invalid") - - return value - - -def evaluate_when_rules( - when_rules, - schema, - metadata, - answer_store, - list_store, - current_location=None, - routing_path_block_ids=None, -): - """ - Whether the skip condition has been met. - :param when_rules: when rules to evaluate - :param schema: survey schema - :param metadata: metadata for evaluating rules with metadata conditions - :param answer_store: store of answers to evaluate - :param list_store: store of lists to evaluate - :param current_location: The location to use when evaluating when rules - :param routing_path_block_ids: The routing path block ids to use when evaluating when rules - :return: True if the when condition has been met otherwise False - """ - for when_rule in when_rules: - - list_item_id = current_location.list_item_id if current_location else None - - value = _get_when_rule_value( - when_rule, - answer_store, - list_store, - schema, - metadata, - list_item_id=list_item_id, - routing_path_block_ids=routing_path_block_ids, - ) - - if "date_comparison" in when_rule: - if not evaluate_date_rule(when_rule, answer_store, schema, metadata, value): - return False - elif "comparison" in when_rule: - comparison_id_value = _get_comparison_id_value( - when_rule, - answer_store, - schema, - current_location, - routing_path_block_ids, - ) - if not evaluate_comparison_rule(when_rule, value, comparison_id_value): - return False - else: - if not evaluate_rule(when_rule, value): - return False - - return True - - -def get_answer_for_answer_id(answer_id, answer_store, schema, list_item_id): - list_item_id = ( - list_item_id if list_item_id and schema.is_repeating_answer(answer_id) else None - ) - - answer = answer_store.get_answer( - answer_id, list_item_id - ) or schema.get_default_answer(answer_id) - - return answer - - -def get_answer_value( - answer_id, answer_store, schema, list_item_id=None, routing_path_block_ids=None -) -> Optional[AnswerValueTypes]: - answer = get_answer_for_answer_id(answer_id, answer_store, schema, list_item_id) - - if not answer: - return None - - if routing_path_block_ids: - if _is_answer_on_path(schema, answer, routing_path_block_ids): - return answer.value - else: - return answer.value - - -def get_metadata_value(metadata, key): - return metadata.get(key) - - -def get_list_count(list_store, list_name): - return len(list_store[list_name].items) diff --git a/app/routes/dump.py b/app/routes/dump.py index 91ed88cfc3..db5dc0f1f9 100644 --- a/app/routes/dump.py +++ b/app/routes/dump.py @@ -1,35 +1,23 @@ -from functools import wraps - -from flask import Blueprint, g +from flask import Blueprint from flask_login import current_user, login_required from app.authentication.roles import role_required -from app.globals import get_questionnaire_store, get_session_store +from app.data_models import QuestionnaireStore +from app.globals import get_questionnaire_store +from app.helpers.schema_helpers import with_schema from app.helpers.session_helpers import with_questionnaire_store +from app.questionnaire import QuestionnaireSchema from app.questionnaire.router import Router from app.utilities.json import json_dumps -from app.utilities.schema import load_schema_from_session_data from app.views.handlers.submission import SubmissionHandler dump_blueprint = Blueprint("dump", __name__) -def requires_schema(func): - @wraps(func) - def wrapper(*args, **kwargs): - session = get_session_store() - # pylint: disable=assigning-non-slot - g.schema = load_schema_from_session_data(session.session_data) - result = func(g.schema, *args, **kwargs) - return result - - return wrapper - - @dump_blueprint.route("/dump/debug", methods=["GET"]) @login_required @role_required("dumper") -def dump_debug(): +def dump_debug() -> str: questionnaire_store = get_questionnaire_store( current_user.user_id, current_user.user_ik ) @@ -40,15 +28,13 @@ def dump_debug(): @login_required @role_required("dumper") @with_questionnaire_store -@requires_schema -def dump_routing(schema, questionnaire_store): +@with_schema +def dump_routing( + schema: QuestionnaireSchema, questionnaire_store: QuestionnaireStore +) -> tuple[str, int]: router = Router( - schema, - questionnaire_store.answer_store, - questionnaire_store.list_store, - questionnaire_store.progress_store, - questionnaire_store.metadata, - questionnaire_store.response_metadata, + schema=schema, + data_stores=questionnaire_store.data_stores, ) response = [ @@ -67,21 +53,16 @@ def dump_routing(schema, questionnaire_store): @login_required @role_required("dumper") @with_questionnaire_store -@requires_schema -def dump_submission(schema, questionnaire_store): +@with_schema +def dump_submission( + schema: QuestionnaireSchema, questionnaire_store: QuestionnaireStore +) -> tuple[str, int]: router = Router( - schema, - questionnaire_store.answer_store, - questionnaire_store.list_store, - questionnaire_store.progress_store, - questionnaire_store.metadata, - questionnaire_store.response_metadata, + schema=schema, + data_stores=questionnaire_store.data_stores, ) routing_path = router.full_routing_path() - questionnaire_store = get_questionnaire_store( - current_user.user_id, current_user.user_ik - ) submission_handler = SubmissionHandler(schema, questionnaire_store, routing_path) diff --git a/app/routes/errors.py b/app/routes/errors.py index 76c0f67260..4b182e6cd5 100644 --- a/app/routes/errors.py +++ b/app/routes/errors.py @@ -1,4 +1,4 @@ -from typing import Tuple +from typing import Any from flask import Blueprint, request from flask.helpers import url_for @@ -6,8 +6,14 @@ from flask_login import current_user from flask_wtf.csrf import CSRFError from sdc.crypto.exceptions import InvalidTokenException -from structlog import get_logger -from werkzeug.exceptions import BadRequestKeyError +from structlog import contextvars, get_logger +from werkzeug.exceptions import ( + BadRequest, + Forbidden, + MethodNotAllowed, + NotFound, + Unauthorized, +) from app.authentication.no_questionnaire_state_exception import ( NoQuestionnaireStateException, @@ -15,9 +21,16 @@ from app.authentication.no_token_exception import NoTokenException from app.globals import get_metadata from app.helpers.language_helper import handle_language -from app.helpers.template_helpers import render_template +from app.helpers.template_helpers import get_survey_config, render_template +from app.services.supplementary_data import ( + InvalidSupplementaryData, + MissingSupplementaryDataKey, + SupplementaryDataRequestFailed, +) +from app.settings import ACCOUNT_SERVICE_BASE_URL_SOCIAL from app.submitter.previously_submitted_exception import PreviouslySubmittedException from app.submitter.submission_failed import SubmissionFailedException +from app.survey_config.survey_type import SurveyType from app.views.handlers.confirm_email import ( ConfirmationEmailFulfilmentRequestPublicationFailed, ) @@ -32,10 +45,9 @@ errors_blueprint = Blueprint("errors", __name__) -def log_exception(exception, status_code): - metadata = get_metadata(current_user) - if metadata: - logger.bind(tx_id=metadata["tx_id"]) +def log_exception(exception: Exception, status_code: int) -> None: + if metadata := get_metadata(current_user): + contextvars.bind_contextvars(tx_id=metadata.tx_id) log = logger.warning if status_code < 500 else logger.error @@ -47,19 +59,39 @@ def log_exception(exception, status_code): ) -def _render_error_page(status_code, template=None, **kwargs): +def _render_error_page( + status_code: int, template: str | int | None = None, **kwargs: Any +) -> tuple[str, int]: handle_language() + business_survey_config = get_survey_config(theme=SurveyType.BUSINESS) + other_survey_config = get_survey_config( + theme=SurveyType.SOCIAL, base_url=ACCOUNT_SERVICE_BASE_URL_SOCIAL + ) + + business_logout_url = business_survey_config.account_service_log_out_url + other_logout_url = other_survey_config.account_service_log_out_url + business_contact_us_url = business_survey_config.contact_us_url + other_contact_us_url = other_survey_config.contact_us_url template = template or status_code return ( - render_template(template=f"errors/{template}", **kwargs), + render_template( + template=f"errors/{template}", + business_logout_url=business_logout_url, + other_logout_url=other_logout_url, + business_contact_us_url=business_contact_us_url, + other_contact_us_url=other_contact_us_url, + **kwargs, + ), status_code, ) @errors_blueprint.app_errorhandler(400) -@errors_blueprint.app_errorhandler(BadRequestKeyError) -def bad_request(exception=None): +@errors_blueprint.app_errorhandler(BadRequest) +def bad_request( + exception: BadRequest, +) -> tuple[str, int]: log_exception(exception, 400) return _render_error_page(400, template="500") @@ -68,38 +100,45 @@ def bad_request(exception=None): @errors_blueprint.app_errorhandler(CSRFError) @errors_blueprint.app_errorhandler(NoTokenException) @errors_blueprint.app_errorhandler(NoQuestionnaireStateException) -def unauthorized(exception=None) -> Tuple[str, int]: +@errors_blueprint.app_errorhandler(Unauthorized) +def unauthorized( + exception: ( + CSRFError | NoTokenException | NoQuestionnaireStateException | Unauthorized + ), +) -> tuple[str, int]: log_exception(exception, 401) return _render_error_page(401, template="401") @errors_blueprint.app_errorhandler(PreviouslySubmittedException) -def previously_submitted(exception=None): +def previously_submitted( + exception: PreviouslySubmittedException, +) -> tuple[str, int]: log_exception(exception, 401) return _render_error_page(401, "previously-submitted") @errors_blueprint.app_errorhandler(InvalidTokenException) -def forbidden(exception=None): +def forbidden(exception: InvalidTokenException) -> tuple[str, int]: log_exception(exception, 403) return _render_error_page(403) @errors_blueprint.app_errorhandler(405) -def method_not_allowed(exception=None): +def method_not_allowed(exception: MethodNotAllowed) -> tuple[str, int]: log_exception(exception, 405) return _render_error_page(405, template="404") @errors_blueprint.app_errorhandler(403) @errors_blueprint.app_errorhandler(404) -def http_exception(exception): +def http_exception(exception: NotFound | Forbidden) -> tuple[str, int]: log_exception(exception, exception.code) return _render_error_page(exception.code) @errors_blueprint.app_errorhandler(Exception) -def internal_server_error(exception=None): +def internal_server_error(exception: Exception) -> tuple[str, int]: try: log_exception(exception, 500) return _render_error_page(500) @@ -113,7 +152,9 @@ def internal_server_error(exception=None): @errors_blueprint.app_errorhandler(IndividualResponseLimitReached) -def too_many_individual_response_requests(exception=None): +def too_many_individual_response_requests( + exception: IndividualResponseLimitReached, +) -> tuple[str, int]: log_exception(exception, 429) title = lazy_gettext( "You have reached the maximum number of individual access codes" @@ -132,7 +173,9 @@ def too_many_individual_response_requests(exception=None): @errors_blueprint.app_errorhandler(FeedbackLimitReached) -def too_many_feedback_requests(exception=None): +def too_many_feedback_requests( + exception: FeedbackLimitReached, +) -> tuple[str, int]: log_exception(exception, 429) title = lazy_gettext( "You have reached the maximum number of times for submitting feedback" @@ -150,14 +193,32 @@ def too_many_feedback_requests(exception=None): ) +@errors_blueprint.app_errorhandler(SupplementaryDataRequestFailed) +@errors_blueprint.app_errorhandler(MissingSupplementaryDataKey) +@errors_blueprint.app_errorhandler(InvalidSupplementaryData) +def supplementary_data_request_failed( + exception: ( + SupplementaryDataRequestFailed + | MissingSupplementaryDataKey + | InvalidSupplementaryData + ), +) -> tuple[str, int]: + log_exception(exception, 500) + return _render_error_page(500, template=500) + + @errors_blueprint.app_errorhandler(SubmissionFailedException) -def submission_failed(exception=None): +def submission_failed( + exception: SubmissionFailedException, +) -> tuple[str, int]: log_exception(exception, 500) return _render_error_page(500, template="submission-failed") @errors_blueprint.app_errorhandler(IndividualResponseFulfilmentRequestPublicationFailed) -def individual_response_fulfilment_request_publication_failed(exception): +def individual_response_fulfilment_request_publication_failed( + exception: IndividualResponseFulfilmentRequestPublicationFailed, +) -> tuple[str, int]: log_exception(exception, 500) if "mobile_number" in request.args: @@ -172,8 +233,9 @@ def individual_response_fulfilment_request_publication_failed(exception): title = lazy_gettext("Sorry, there was a problem sending the access code") retry_url = url_for( blueprint_method, - list_item_id=request.view_args["list_item_id"], - **request.args, + # Type ignore: Request will be not None as this function will only run when a request handle raises and exception + list_item_id=request.view_args["list_item_id"], # type: ignore + **request.args, # type: ignore ) retry_message = lazy_gettext( "You can try to request a new access code again." @@ -194,7 +256,9 @@ def individual_response_fulfilment_request_publication_failed(exception): @errors_blueprint.app_errorhandler(ConfirmationEmailFulfilmentRequestPublicationFailed) -def confirmation_email_fulfilment_request_publication_failed(exception): +def confirmation_email_fulfilment_request_publication_failed( + exception: ConfirmationEmailFulfilmentRequestPublicationFailed, +) -> tuple[str, int]: log_exception(exception, 500) title = lazy_gettext("Sorry, there was a problem sending the confirmation email") @@ -217,7 +281,7 @@ def confirmation_email_fulfilment_request_publication_failed(exception): @errors_blueprint.app_errorhandler(FeedbackUploadFailed) -def feedback_upload_failed(exception): +def feedback_upload_failed(exception: FeedbackUploadFailed) -> tuple[str, int]: log_exception(exception, 500) title = lazy_gettext("Sorry, there is a problem") retry_message = lazy_gettext( diff --git a/app/routes/flush.py b/app/routes/flush.py index 7aeee29536..7e7147e862 100644 --- a/app/routes/flush.py +++ b/app/routes/flush.py @@ -1,26 +1,38 @@ from datetime import datetime, timezone +from typing import Iterable, TypeAlias from flask import Blueprint, Response, current_app, request, session from sdc.crypto.decrypter import decrypt from sdc.crypto.encrypter import encrypt -from structlog import get_logger +from sdc.crypto.key_store import KeyStore +from structlog import contextvars, get_logger from app.authentication.user import User -from app.globals import get_answer_store, get_metadata, get_questionnaire_store +from app.authentication.user_id_generator import UserIDGenerator +from app.data_models import QuestionnaireStore +from app.data_models.metadata_proxy import MetadataProxy +from app.globals import get_metadata, get_questionnaire_store from app.keys import KEY_PURPOSE_AUTHENTICATION, KEY_PURPOSE_SUBMISSION +from app.questionnaire import QuestionnaireSchema from app.questionnaire.router import Router -from app.submitter.converter import convert_answers +from app.questionnaire.routing_path import RoutingPath +from app.submitter import GCSSubmitter, LogSubmitter, RabbitMQSubmitter +from app.submitter.converter_v2 import convert_answers_v2 from app.submitter.submission_failed import SubmissionFailedException +from app.utilities.bind_context import bind_contextvars_schema_from_metadata from app.utilities.json import json_dumps from app.utilities.schema import load_schema_from_metadata +from app.views.handlers.submission import get_receipting_metadata flush_blueprint = Blueprint("flush", __name__) logger = get_logger() +Submitter: TypeAlias = GCSSubmitter | LogSubmitter | RabbitMQSubmitter + @flush_blueprint.route("/flush", methods=["POST"]) -def flush_data(): +def flush_data() -> Response: if session: session.clear() @@ -31,7 +43,7 @@ def flush_data(): decrypted_token = decrypt( token=encrypted_token, - key_store=current_app.eq["key_store"], + key_store=_get_keystore(), key_purpose=KEY_PURPOSE_AUTHENTICATION, leeway=current_app.config["EQ_JWT_LEEWAY_IN_SECONDS"], ) @@ -40,56 +52,56 @@ def flush_data(): if roles and "flusher" in roles: user = _get_user(decrypted_token["response_id"]) - metadata = get_metadata(user) - if "tx_id" in metadata: - logger.bind(tx_id=metadata["tx_id"]) + + if metadata := get_metadata(user): + contextvars.bind_contextvars( + tx_id=metadata.tx_id, + ce_id=metadata.collection_exercise_sid, + ) + bind_contextvars_schema_from_metadata(metadata) + if _submit_data(user): return Response(status=200) return Response(status=404) return Response(status=403) -def _submit_data(user): - answer_store = get_answer_store(user) +def _submit_data(user: User) -> bool: + questionnaire_store = get_questionnaire_store(user.user_id, user.user_ik) - if answer_store: - questionnaire_store = get_questionnaire_store(user.user_id, user.user_ik) - answer_store = questionnaire_store.answer_store - metadata = questionnaire_store.metadata - response_metadata = questionnaire_store.response_metadata - progress_store = questionnaire_store.progress_store - list_store = questionnaire_store.list_store + if questionnaire_store and questionnaire_store.data_stores.answer_store: + # Type ignore: The presence of an answer_store implicitly verifies that there must be metadata populated and thus can safely be used non-optionally. + metadata: MetadataProxy = questionnaire_store.data_stores.metadata # type: ignore submitted_at = datetime.now(timezone.utc) - schema = load_schema_from_metadata(metadata) + schema = load_schema_from_metadata( + metadata=metadata, language_code=metadata.language_code + ) router = Router( - schema, - answer_store, - list_store, - progress_store, - metadata, - response_metadata, + schema=schema, + data_stores=questionnaire_store.data_stores, ) full_routing_path = router.full_routing_path() - message = json_dumps( - convert_answers( - schema, - questionnaire_store, - full_routing_path, - submitted_at, - flushed=True, - ) + message: str = _get_converted_answers_message( + full_routing_path=full_routing_path, + questionnaire_store=questionnaire_store, + schema=schema, + submitted_at=submitted_at, ) - encrypted_message = encrypt( - message, current_app.eq["key_store"], KEY_PURPOSE_SUBMISSION - ) + encrypted_message = encrypt(message, _get_keystore(), KEY_PURPOSE_SUBMISSION) + + additional_metadata = get_receipting_metadata(metadata) - sent = current_app.eq["submitter"].send_message( + # Type ignore: Instance attribute 'eq' is a dict with key "submitter" with value of type GCSSubmitter, LogSubmitter or RabbitMQSubmitter + submitter: Submitter = current_app.eq["submitter"] # type: ignore + + sent = submitter.send_message( encrypted_message, - tx_id=metadata.get("tx_id"), - case_id=metadata["case_id"], + tx_id=metadata.tx_id, + case_id=metadata.case_id, + **additional_metadata, ) if not sent: @@ -103,8 +115,37 @@ def _submit_data(user): return False -def _get_user(response_id): - id_generator = current_app.eq["id_generator"] +def _get_converted_answers_message( + full_routing_path: Iterable[RoutingPath], + questionnaire_store: QuestionnaireStore, + schema: QuestionnaireSchema, + submitted_at: datetime, +) -> str: + """ + This gets converted answer message based on the selected version, currently only v2 is supported so `app.submitter.converter_v2.convert_answers_v2` is used + Returns: + object: str + """ + return json_dumps( + convert_answers_v2( + schema=schema, + questionnaire_store=questionnaire_store, + full_routing_path=full_routing_path, + submitted_at=submitted_at, + flushed=True, + ) + ) + + +def _get_user(response_id: str) -> User: + # Type ignore: Instance attribute 'eq' is a dict with key "id_generator" with value of type UserIDGenerator + id_generator: UserIDGenerator = current_app.eq["id_generator"] # type: ignore user_id = id_generator.generate_id(response_id) user_ik = id_generator.generate_ik(response_id) return User(user_id, user_ik) + + +def _get_keystore() -> KeyStore: + # Type ignore: Instance attribute 'eq' is a dict with key "key_store" with value of type KeyStore + key_store: KeyStore = current_app.eq["key_store"] # type: ignore + return key_store diff --git a/app/routes/individual_response.py b/app/routes/individual_response.py index 5fb9334f4b..991acd7893 100644 --- a/app/routes/individual_response.py +++ b/app/routes/individual_response.py @@ -1,20 +1,25 @@ from flask import Blueprint, g, redirect, request, url_for -from flask_babel import lazy_gettext +from flask_babel import get_locale, lazy_gettext from flask_login import current_user, login_required from itsdangerous import BadSignature -from structlog import get_logger +from structlog import contextvars, get_logger from werkzeug.exceptions import BadRequest +from werkzeug.wrappers.response import Response from app.authentication.no_questionnaire_state_exception import ( NoQuestionnaireStateException, ) -from app.globals import get_metadata, get_questionnaire_store, get_session_store +from app.data_models import QuestionnaireStore +from app.data_models.metadata_proxy import MetadataProxy +from app.globals import get_metadata, get_questionnaire_store from app.helpers import url_safe_serializer from app.helpers.language_helper import handle_language from app.helpers.schema_helpers import with_schema from app.helpers.session_helpers import with_questionnaire_store from app.helpers.template_helpers import render_template -from app.utilities.schema import load_schema_from_session_data +from app.questionnaire import QuestionnaireSchema +from app.utilities.bind_context import bind_contextvars_schema_from_metadata +from app.utilities.schema import load_schema_from_metadata from app.views.handlers.individual_response import ( IndividualResponseChangeHandler, IndividualResponseHandler, @@ -35,7 +40,7 @@ @login_required @individual_response_blueprint.before_request -def before_individual_response_request(): +def before_individual_response_request() -> Response | None: if request.method == "OPTIONS": return None @@ -50,29 +55,34 @@ def before_individual_response_request(): if questionnaire_store.submitted_at: return redirect(url_for("post_submission.get_thank_you")) - logger.bind( - tx_id=metadata["tx_id"], - schema_name=metadata["schema_name"], - ce_id=metadata["collection_exercise_sid"], + contextvars.bind_contextvars( + tx_id=metadata.tx_id, + ce_id=metadata.collection_exercise_sid, ) + bind_contextvars_schema_from_metadata(metadata) + logger.info( "individual-response request", method=request.method, url_path=request.full_path ) - handle_language() + # Ensures langauge is set in the SessionStore + handle_language(metadata) - session_store = get_session_store() - # pylint: disable=assigning-non-slot - g.schema = load_schema_from_session_data(session_store.session_data) + g.schema = load_schema_from_metadata( + metadata=metadata, + language_code=get_locale().language, + ) @individual_response_blueprint.route("/", methods=["GET"]) @with_questionnaire_store @with_schema -def request_individual_response(schema, questionnaire_store): - language_code = get_session_store().session_data.language_code - list_item_id = request.args.get("list_item_id") +def request_individual_response( + schema: QuestionnaireSchema, questionnaire_store: QuestionnaireStore +) -> str: + language_code: str = get_locale().language + list_item_id: str | None = request.args.get("list_item_id") individual_response_handler = IndividualResponseHandler( schema=schema, @@ -89,9 +99,12 @@ def request_individual_response(schema, questionnaire_store): @individual_response_blueprint.route("//how", methods=["GET", "POST"]) @with_questionnaire_store @with_schema -def individual_response_how(schema, questionnaire_store, list_item_id): - language_code = get_session_store().session_data.language_code - +def individual_response_how( + schema: QuestionnaireSchema, + questionnaire_store: QuestionnaireStore, + list_item_id: str, +) -> Response | str: + language_code: str = get_locale().language individual_response_handler = IndividualResponseHowHandler( schema=schema, questionnaire_store=questionnaire_store, @@ -110,8 +123,12 @@ def individual_response_how(schema, questionnaire_store, list_item_id): @individual_response_blueprint.route("//change", methods=["GET", "POST"]) @with_questionnaire_store @with_schema -def individual_response_change(schema, questionnaire_store, list_item_id): - language_code = get_session_store().session_data.language_code +def individual_response_change( + schema: QuestionnaireSchema, + questionnaire_store: QuestionnaireStore, + list_item_id: str, +) -> Response | str | None: + language_code: str = get_locale().language individual_response_handler = IndividualResponseChangeHandler( schema=schema, questionnaire_store=questionnaire_store, @@ -132,9 +149,12 @@ def individual_response_change(schema, questionnaire_store, list_item_id): ) @with_questionnaire_store @with_schema -def individual_response_post_address_confirm(schema, questionnaire_store, list_item_id): - language_code = get_session_store().session_data.language_code - +def individual_response_post_address_confirm( + schema: QuestionnaireSchema, + questionnaire_store: QuestionnaireStore, + list_item_id: str, +) -> Response | str: + language_code: str = get_locale().language try: individual_response_handler = IndividualResponsePostAddressConfirmHandler( schema=schema, @@ -156,8 +176,10 @@ def individual_response_post_address_confirm(schema, questionnaire_store, list_i @individual_response_blueprint.route("/post/confirmation", methods=["GET", "POST"]) @with_questionnaire_store @with_schema -def individual_response_post_address_confirmation(schema, questionnaire_store): - language_code = get_session_store().session_data.language_code +def individual_response_post_address_confirmation( + schema: QuestionnaireSchema, questionnaire_store: QuestionnaireStore +) -> Response | str: + language_code: str = get_locale().language individual_response_handler = IndividualResponseHandler( schema=schema, questionnaire_store=questionnaire_store, @@ -170,9 +192,12 @@ def individual_response_post_address_confirmation(schema, questionnaire_store): if request.method == "POST": return redirect(url_for("questionnaire.get_questionnaire")) + # Type ignore: @with_schema guarantees that metadata is present via QuestionnaireStore + metadata: MetadataProxy = questionnaire_store.data_stores.metadata # type: ignore + return render_template( template="individual_response/confirmation-post", - display_address=questionnaire_store.metadata.get("display_address"), + display_address=metadata["display_address"], page_title=individual_response_handler.page_title( lazy_gettext("An individual access code has been sent by post") ), @@ -182,9 +207,10 @@ def individual_response_post_address_confirmation(schema, questionnaire_store): @individual_response_blueprint.route("/who", methods=["GET", "POST"]) @with_questionnaire_store @with_schema -def individual_response_who(schema, questionnaire_store): - language_code = get_session_store().session_data.language_code - +def individual_response_who( + schema: QuestionnaireSchema, questionnaire_store: QuestionnaireStore +) -> Response | str: + language_code: str = get_locale().language individual_response_handler = IndividualResponseWhoHandler( schema=schema, questionnaire_store=questionnaire_store, @@ -204,8 +230,12 @@ def individual_response_who(schema, questionnaire_store): ) @with_questionnaire_store @with_schema -def individual_response_text_message(schema, questionnaire_store, list_item_id): - language_code = get_session_store().session_data.language_code +def individual_response_text_message( + schema: QuestionnaireSchema, + questionnaire_store: QuestionnaireStore, + list_item_id: str, +) -> Response | str: + language_code: str = get_locale().language individual_response_handler = IndividualResponseTextHandler( schema=schema, questionnaire_store=questionnaire_store, @@ -226,8 +256,12 @@ def individual_response_text_message(schema, questionnaire_store, list_item_id): ) @with_questionnaire_store @with_schema -def individual_response_text_message_confirm(schema, questionnaire_store, list_item_id): - language_code = get_session_store().session_data.language_code +def individual_response_text_message_confirm( + schema: QuestionnaireSchema, + questionnaire_store: QuestionnaireStore, + list_item_id: str, +) -> Response | str: + language_code: str = get_locale().language individual_response_handler = IndividualResponseTextConfirmHandler( schema=schema, questionnaire_store=questionnaire_store, @@ -246,8 +280,10 @@ def individual_response_text_message_confirm(schema, questionnaire_store, list_i @individual_response_blueprint.route("/text/confirmation", methods=["GET", "POST"]) @with_questionnaire_store @with_schema -def individual_response_text_message_confirmation(schema, questionnaire_store): - language_code = get_session_store().session_data.language_code +def individual_response_text_message_confirmation( + schema: QuestionnaireSchema, questionnaire_store: QuestionnaireStore +) -> Response | str: + language_code: str = get_locale().language individual_response_handler = IndividualResponseHandler( schema=schema, questionnaire_store=questionnaire_store, diff --git a/app/routes/questionnaire.py b/app/routes/questionnaire.py index b907e2d372..814bcb30fd 100644 --- a/app/routes/questionnaire.py +++ b/app/routes/questionnaire.py @@ -1,25 +1,25 @@ from __future__ import annotations -from typing import Union +from typing import Mapping import flask_babel from flask import Blueprint, g, redirect, request, send_file from flask import session as cookie_session from flask import url_for +from flask_babel import get_locale from flask_login import current_user, login_required from itsdangerous import BadSignature -from structlog import get_logger +from structlog import contextvars, get_logger from werkzeug import Response from werkzeug.exceptions import BadRequest, NotFound from app.authentication.no_questionnaire_state_exception import ( NoQuestionnaireStateException, ) -from app.data_models import QuestionnaireStore +from app.data_models import QuestionnaireStore, SessionStore from app.globals import ( get_metadata, get_questionnaire_store, - get_session_store, get_session_timeout_in_seconds, ) from app.helpers import url_safe_serializer @@ -31,8 +31,13 @@ from app.questionnaire.location import InvalidLocationException from app.questionnaire.router import Router from app.submitter.previously_submitted_exception import PreviouslySubmittedException -from app.utilities.schema import load_schema_from_session_data +from app.utilities.bind_context import bind_contextvars_schema_from_metadata +from app.utilities.schema import load_schema_from_metadata from app.views.contexts import HubContext +from app.views.contexts.preview_context import ( + PreviewContext, + PreviewNotEnabledException, +) from app.views.handlers.block_factory import get_block_handler from app.views.handlers.confirm_email import ConfirmEmail from app.views.handlers.confirmation_email import ( @@ -41,6 +46,7 @@ ConfirmationEmailNotEnabled, ) from app.views.handlers.feedback import Feedback, FeedbackNotEnabled +from app.views.handlers.preview_questions_pdf import PreviewQuestionsPDF from app.views.handlers.section import SectionHandler from app.views.handlers.submission import SubmissionHandler from app.views.handlers.submit_questionnaire import SubmitQuestionnaireHandler @@ -62,17 +68,17 @@ name="post_submission", import_name=__name__, url_prefix="/submitted/" ) +REPEATED_SUBMISSION_ERROR_MESSAGE = "The Questionnaire has been previously submitted" + @questionnaire_blueprint.before_request @login_required -def before_questionnaire_request(): +def before_questionnaire_request() -> Response | None: if request.method == "OPTIONS": return None if cookie_session.get("submitted"): - raise PreviouslySubmittedException( - "The Questionnaire has been previously submitted" - ) + raise PreviouslySubmittedException(REPEATED_SUBMISSION_ERROR_MESSAGE) metadata = get_metadata(current_user) if not metadata: @@ -85,26 +91,26 @@ def before_questionnaire_request(): if questionnaire_store.submitted_at: return redirect(url_for("post_submission.get_thank_you")) - logger.bind( - tx_id=metadata["tx_id"], - schema_name=metadata["schema_name"], - ce_id=metadata["collection_exercise_sid"], + contextvars.bind_contextvars( + tx_id=metadata.tx_id, + ce_id=metadata.collection_exercise_sid, ) + bind_contextvars_schema_from_metadata(metadata) logger.info( "questionnaire request", method=request.method, url_path=request.full_path ) - handle_language() + handle_language(metadata) - session_store = get_session_store() - # pylint: disable=assigning-non-slot - g.schema = load_schema_from_session_data(session_store.session_data) + g.schema = load_schema_from_metadata( + metadata=metadata, language_code=get_locale().language + ) @post_submission_blueprint.before_request @login_required -def before_post_submission_request(): +def before_post_submission_request() -> None: if request.method == "OPTIONS": return None @@ -119,14 +125,14 @@ def before_post_submission_request(): if not questionnaire_store.submitted_at: raise NotFound - handle_language() + handle_language(questionnaire_store.data_stores.metadata) - session_store = get_session_store() - session_data = session_store.session_data - # pylint: disable=assigning-non-slot - g.schema = load_schema_from_session_data(session_data) + g.schema = load_schema_from_metadata( + metadata=metadata, language_code=get_locale().language + ) - logger.bind(tx_id=session_data.tx_id, schema_name=session_data.schema_name) + contextvars.bind_contextvars(tx_id=metadata.tx_id) + bind_contextvars_schema_from_metadata(metadata) logger.info( "questionnaire request", method=request.method, url_path=request.full_path @@ -136,14 +142,12 @@ def before_post_submission_request(): @questionnaire_blueprint.route("/", methods=["GET", "POST"]) @with_questionnaire_store @with_schema -def get_questionnaire(schema, questionnaire_store): +def get_questionnaire( + schema: QuestionnaireSchema, questionnaire_store: QuestionnaireStore +) -> Response | str: router = Router( - schema, - questionnaire_store.answer_store, - questionnaire_store.list_store, - questionnaire_store.progress_store, - questionnaire_store.metadata, - questionnaire_store.response_metadata, + schema=schema, + data_stores=questionnaire_store.data_stores, ) if not router.can_access_hub(): @@ -164,11 +168,7 @@ def get_questionnaire(schema, questionnaire_store): hub_context = HubContext( language=flask_babel.get_locale().language, schema=schema, - answer_store=questionnaire_store.answer_store, - list_store=questionnaire_store.list_store, - progress_store=questionnaire_store.progress_store, - metadata=questionnaire_store.metadata, - response_metadata=questionnaire_store.response_metadata, + data_stores=questionnaire_store.data_stores, ) context = hub_context( survey_complete=router.is_questionnaire_complete, @@ -186,7 +186,7 @@ def get_questionnaire(schema, questionnaire_store): @with_schema def submit_questionnaire( schema: QuestionnaireSchema, questionnaire_store: QuestionnaireStore -) -> Union[Response, str]: +) -> Response | str: try: submit_questionnaire_handler = SubmitQuestionnaireHandler( schema, questionnaire_store, flask_babel.get_locale().language @@ -212,13 +212,46 @@ def submit_questionnaire( ) +@questionnaire_blueprint.route("/preview", methods=["GET"]) +@with_questionnaire_store +@with_schema +def get_preview( + schema: QuestionnaireSchema, questionnaire_store: QuestionnaireStore +) -> str: + try: + preview_context = PreviewContext( + language=flask_babel.get_locale().language, + schema=schema, + data_stores=questionnaire_store.data_stores, + ) + except PreviewNotEnabledException as exc: + raise NotFound from exc + + schema_type = schema.json["questionnaire_flow"].get("type") + + context = { + "schema_type": schema_type, + "preview": preview_context(), + "pdf_url": url_for(".get_preview_questions_pdf"), + } + + return render_template( + template="preview", content=context, page_title=preview_context.get_page_title() + ) + + @questionnaire_blueprint.route("sections//", methods=["GET", "POST"]) @questionnaire_blueprint.route( "sections///", methods=["GET", "POST"] ) @with_questionnaire_store @with_schema -def get_section(schema, questionnaire_store, section_id, list_item_id=None): +def get_section( + schema: QuestionnaireSchema, + questionnaire_store: QuestionnaireStore, + section_id: str, + list_item_id: str | None = None, +) -> Response | str: try: section_handler = SectionHandler( schema=schema, @@ -246,7 +279,6 @@ def get_section(schema, questionnaire_store, section_id, list_item_id=None): return redirect(section_handler.get_next_location_url()) -# pylint: disable=too-many-return-statements @questionnaire_blueprint.route("/", methods=["GET", "POST"]) @questionnaire_blueprint.route("//", methods=["GET", "POST"]) @questionnaire_blueprint.route( @@ -254,7 +286,13 @@ def get_section(schema, questionnaire_store, section_id, list_item_id=None): ) @with_questionnaire_store @with_schema -def block(schema, questionnaire_store, block_id, list_name=None, list_item_id=None): +def block( + schema: QuestionnaireSchema, + questionnaire_store: QuestionnaireStore, + block_id: str, + list_name: str | None = None, + list_item_id: str | None = None, +) -> Response | str: try: block_handler = get_block_handler( schema=schema, @@ -303,13 +341,13 @@ def block(schema, questionnaire_store, block_id, list_name=None, list_item_id=No @with_questionnaire_store @with_schema def relationships( - schema, - questionnaire_store, - list_name=None, - list_item_id=None, - to_list_item_id=None, - block_id="relationships", -): + schema: QuestionnaireSchema, + questionnaire_store: QuestionnaireStore, + list_name: str | None = None, + list_item_id: str | None = None, + to_list_item_id: str | None = None, + block_id: str = "relationships", +) -> Response | str: try: block_handler = get_block_handler( schema=schema, @@ -350,8 +388,13 @@ def relationships( @with_questionnaire_store @with_session_store @with_schema -def get_thank_you(schema, session_store, questionnaire_store): - thank_you = ThankYou(schema, session_store, questionnaire_store.submitted_at) +def get_thank_you( + schema: QuestionnaireSchema, + session_store: SessionStore, + questionnaire_store: QuestionnaireStore, +) -> Response | str: + # Type ignore: Endpoint is only called upon submission + thank_you = ThankYou(schema, session_store, questionnaire_store.submitted_at) # type: ignore if request.method == "POST": confirmation_email = thank_you.confirmation_email @@ -366,16 +409,17 @@ def get_thank_you(schema, session_store, questionnaire_store): ) ) - # pylint: disable=no-member - # wtforms Form parents are not discoverable in the 2.3.3 implementation logger.info( "email validation error", error_message=str(confirmation_email.form.errors["email"][0]), ) + # Type ignore: Session data exists at point of submission show_feedback_call_to_action = Feedback.is_enabled( schema - ) and not Feedback.is_limit_reached(session_store.session_data) + ) and not Feedback.is_limit_reached( + session_store.session_data # type: ignore + ) return render_template( template=thank_you.template, @@ -391,7 +435,9 @@ def get_thank_you(schema, session_store, questionnaire_store): @post_submission_blueprint.route("view-response/", methods=["GET"]) @with_questionnaire_store @with_schema -def get_view_submitted_response(schema, questionnaire_store): +def get_view_submitted_response( + schema: QuestionnaireSchema, questionnaire_store: QuestionnaireStore +) -> str: try: view_submitted_response = ViewSubmittedResponse( schema, @@ -405,6 +451,31 @@ def get_view_submitted_response(schema, questionnaire_store): return view_submitted_response.get_rendered_html() +@questionnaire_blueprint.route("/preview/download-pdf", methods=["GET"]) +@with_questionnaire_store +@with_schema +def get_preview_questions_pdf( + schema: QuestionnaireSchema, questionnaire_store: QuestionnaireStore +) -> Response: + view_preview_questions_pdf = PreviewQuestionsPDF( + schema, + questionnaire_store, + flask_babel.get_locale().language, + ) + + try: + path_or_file = view_preview_questions_pdf.get_pdf() + except PreviewNotEnabledException as exc: + raise NotFound from exc + + return send_file( + path_or_file=path_or_file, + mimetype=view_preview_questions_pdf.mimetype, + as_attachment=True, + download_name=view_preview_questions_pdf.filename, + ) + + @post_submission_blueprint.route("download-pdf", methods=["GET"]) @with_questionnaire_store @with_schema @@ -442,7 +513,9 @@ def get_view_submitted_response_pdf( @post_submission_blueprint.route("confirmation-email/send", methods=["GET", "POST"]) @with_schema @with_session_store -def send_confirmation_email(session_store, schema): +def send_confirmation_email( + session_store: SessionStore, schema: QuestionnaireSchema +) -> Response | str: try: confirmation_email = ConfirmationEmail( session_store, schema, serialised_email=request.args.get("email") @@ -459,8 +532,6 @@ def send_confirmation_email(session_store, schema): ) ) - # pylint: disable=no-member - # wtforms Form parents are not discoverable in the 2.3.3 implementation logger.info( "email validation error", error_message=str(confirmation_email.form.errors["email"][0]), @@ -478,7 +549,11 @@ def send_confirmation_email(session_store, schema): @with_questionnaire_store @with_schema @with_session_store -def confirm_confirmation_email(session_store, schema, questionnaire_store): +def confirm_confirmation_email( + session_store: SessionStore, + schema: QuestionnaireSchema, + questionnaire_store: QuestionnaireStore, +) -> Response | str: try: confirm_email = ConfirmEmail( questionnaire_store, @@ -505,8 +580,11 @@ def confirm_confirmation_email(session_store, schema, questionnaire_store): @post_submission_blueprint.route("confirmation-email/sent", methods=["GET"]) @with_schema @with_session_store -def get_confirmation_email_sent(session_store, schema): - if not session_store.session_data.confirmation_email_count: +def get_confirmation_email_sent( + session_store: SessionStore, schema: QuestionnaireSchema +) -> str: + # Type ignore: Session data exists for routes decorated with @login_required, which this is + if not session_store.session_data.confirmation_email_count: # type: ignore raise NotFound try: @@ -515,11 +593,13 @@ def get_confirmation_email_sent(session_store, schema): raise BadRequest from exc show_send_another_email_guidance = not ConfirmationEmail.is_limit_reached( - session_store.session_data + session_store.session_data # type: ignore ) show_feedback_call_to_action = Feedback.is_enabled( schema - ) and not Feedback.is_limit_reached(session_store.session_data) + ) and not Feedback.is_limit_reached( + session_store.session_data # type: ignore + ) return render_template( template="confirmation-email-sent", @@ -540,7 +620,11 @@ def get_confirmation_email_sent(session_store, schema): @with_questionnaire_store @with_session_store @with_schema -def send_feedback(schema, session_store, questionnaire_store): +def send_feedback( + schema: QuestionnaireSchema, + session_store: SessionStore, + questionnaire_store: QuestionnaireStore, +) -> Response | str: try: feedback = Feedback( questionnaire_store, schema, session_store, form_data=request.form @@ -561,8 +645,9 @@ def send_feedback(schema, session_store, questionnaire_store): @post_submission_blueprint.route("feedback/sent", methods=["GET"]) @with_session_store -def get_feedback_sent(session_store): - if not session_store.session_data.feedback_count: +def get_feedback_sent(session_store: SessionStore) -> str: + # Type ignore: Session data exists for routes decorated with @login_required, which this is + if not session_store.session_data.feedback_count: # type: ignore raise NotFound return render_template( @@ -574,7 +659,13 @@ def get_feedback_sent(session_store): ) -def _render_page(template, context, previous_location_url, schema, page_title): +def _render_page( + template: str, + context: Mapping, + previous_location_url: str, + schema: QuestionnaireSchema, + page_title: str, +) -> str: session_timeout = get_session_timeout_in_seconds(schema) return render_template( diff --git a/app/routes/schema.py b/app/routes/schema.py index f6de20da95..27dfe5bba9 100644 --- a/app/routes/schema.py +++ b/app/routes/schema.py @@ -1,4 +1,4 @@ -from flask import Blueprint, jsonify +from flask import Blueprint, Response, jsonify from app.utilities.schema import get_schema_list, load_schema_from_name @@ -6,7 +6,7 @@ @schema_blueprint.route("/schemas/", methods=["GET"]) -def get_schema_json_from_name(schema_name): +def get_schema_json_from_name(schema_name: str) -> Response | tuple[str, int]: try: schema = load_schema_from_name(schema_name) return jsonify(schema.json) @@ -15,5 +15,5 @@ def get_schema_json_from_name(schema_name): @schema_blueprint.route("/schemas", methods=["GET"]) -def list_schemas(): +def list_schemas() -> Response: return jsonify(get_schema_list()) diff --git a/app/routes/session.py b/app/routes/session.py index f841013faa..0213a0031f 100644 --- a/app/routes/session.py +++ b/app/routes/session.py @@ -1,21 +1,39 @@ from datetime import datetime, timezone +from typing import Any, Iterable, Mapping from flask import Blueprint, g, jsonify, redirect, request from flask import session as cookie_session from flask import url_for from flask_login import login_required, logout_user -from marshmallow import ValidationError +from marshmallow import INCLUDE, ValidationError from sdc.crypto.exceptions import InvalidTokenException -from structlog import get_logger +from structlog import contextvars, get_logger from werkzeug.exceptions import Unauthorized +from werkzeug.wrappers.response import Response -from app.authentication.authenticator import decrypt_token, store_session +from app.authentication.authenticator import ( + create_session_questionnaire_store, + decrypt_token, +) from app.authentication.jti_claim_storage import JtiTokenUsed, use_jti_claim +from app.data_models import QuestionnaireStore +from app.data_models.metadata_proxy import MetadataProxy from app.globals import get_session_store, get_session_timeout_in_seconds -from app.helpers.template_helpers import get_survey_config, render_template -from app.utilities.metadata_parser import ( +from app.helpers.metadata_helpers import get_ru_ref_without_check_letter +from app.helpers.template_helpers import ( + DATA_LAYER_KEYS, + get_survey_config, + render_template, +) +from app.questionnaire import QuestionnaireSchema +from app.questionnaire.questionnaire_schema import DEFAULT_LANGUAGE_CODE +from app.questionnaire.questionnaire_store_updater import QuestionnaireStoreUpdaterBase +from app.questionnaire.router import Router +from app.routes.errors import _render_error_page +from app.services.supplementary_data import get_supplementary_data_v1 +from app.utilities.metadata_parser_v2 import ( validate_questionnaire_claims, - validate_runner_claims, + validate_runner_claims_v2, ) from app.utilities.schema import load_schema_from_metadata @@ -23,20 +41,29 @@ session_blueprint = Blueprint("session", __name__) +RUNNER_CLAIMS_ERROR_MESSAGE = "Invalid runner claims" +QUESTIONNAIRE_CLAIMS_ERROR_MESSAGE = "Invalid questionnaire claims" + @session_blueprint.after_request -def add_cache_control(response): +def add_cache_control(response: Response) -> Response: response.cache_control.no_cache = True return response @session_blueprint.route("/session", methods=["HEAD"]) -def login_head(): +def login_head() -> tuple[str, int]: return "", 204 +def set_schema_context_in_cookie(schema: QuestionnaireSchema) -> None: + for key in [*DATA_LAYER_KEYS, "theme"]: + if value := schema.json.get(key): + cookie_session[key] = value + + @session_blueprint.route("/session", methods=["GET", "POST"]) -def login(): +def login() -> Response: """ Initial url processing - expects a token parameter and then will authenticate this token. Once authenticated it will be placed in the users session @@ -50,62 +77,176 @@ def login(): validate_jti(decrypted_token) - try: - runner_claims = validate_runner_claims(decrypted_token) - except ValidationError as e: - raise InvalidTokenException("Invalid runner claims") from e - # pylint: disable=assigning-non-slot - g.schema = load_schema_from_metadata(runner_claims) + _data = ( + survey_metadata.get("data", {}) + if (survey_metadata := decrypted_token.get("survey_metadata")) + else decrypted_token + ) + ru_ref, qid = _data.get("ru_ref"), _data.get("qid") + + logger_args = { + key: value + for key, value in { + "tx_id": decrypted_token.get("tx_id"), + "case_id": decrypted_token.get("case_id"), + "schema_name": decrypted_token.get("schema_name"), + "schema_url": decrypted_token.get("schema_url"), + "cir_instrument_id": decrypted_token.get("cir_instrument_id"), + "ru_ref": ru_ref, + "qid": qid, + }.items() + if value + } + contextvars.bind_contextvars(**logger_args) + + runner_claims = get_runner_claims(decrypted_token) + + metadata = MetadataProxy.from_dict(runner_claims) + + g.schema = load_schema_from_metadata( + metadata=metadata, language_code=metadata.language_code + ) schema_metadata = g.schema.json["metadata"] - try: - questionnaire_claims = validate_questionnaire_claims( - decrypted_token, schema_metadata - ) - except ValidationError as e: - raise InvalidTokenException("Invalid questionnaire claims") from e - - claims = {**runner_claims, **questionnaire_claims} + questionnaire_claims = get_questionnaire_claims( + decrypted_token=decrypted_token, schema_metadata=schema_metadata + ) - schema_name = claims["schema_name"] - tx_id = claims["tx_id"] - ru_ref = claims["ru_ref"] - case_id = claims["case_id"] + runner_claims["survey_metadata"]["data"] = questionnaire_claims - logger.bind( - schema_name=schema_name, - tx_id=tx_id, - ru_ref=ru_ref, - case_id=case_id, - ) logger.info("decrypted token and parsed metadata") - store_session(claims) + with create_session_questionnaire_store(runner_claims) as questionnaire_store: + _set_questionnaire_supplementary_data( + questionnaire_store=questionnaire_store, metadata=metadata, schema=g.schema + ) - cookie_session["theme"] = g.schema.json["theme"] - cookie_session["survey_title"] = g.schema.json["title"] cookie_session["expires_in"] = get_session_timeout_in_seconds(g.schema) - if account_service_url := claims.get("account_service_url"): + set_schema_context_in_cookie(g.schema) + + if account_service_url := runner_claims.get("account_service_url"): cookie_session["account_service_base_url"] = account_service_url - if claims.get("account_service_log_out_url"): - cookie_session["account_service_log_out_url"] = claims.get( # pragma: no cover + if runner_claims.get("account_service_log_out_url"): + cookie_session["account_service_log_out_url"] = runner_claims.get( "account_service_log_out_url" - ) + ) # pragma: no cover + + cookie_session["language_code"] = metadata.language_code or DEFAULT_LANGUAGE_CODE return redirect(url_for("questionnaire.get_questionnaire")) -def validate_jti(decrypted_token): - expires_at = datetime.fromtimestamp(decrypted_token["exp"], tz=timezone.utc) +def _set_questionnaire_supplementary_data( + *, + questionnaire_store: QuestionnaireStore, + metadata: MetadataProxy, + schema: QuestionnaireSchema, +) -> None: + """ + If the survey metadata has an sds dataset id, and it either doesn't match what it stored, or there is no stored supplementary data + then fetch it, verify any schema supplementary lists are included in the fetched data, and add it to the questionnaire store + + Validation of the supplementary lists must be performed every time a survey launches, not just when the supplementary data is fetched + as it is possible that the survey has changed but the dataset hasn't so the validity could have changed. + """ + existing_sds_dataset_id = ( + questionnaire_store.data_stores.metadata.survey_metadata["sds_dataset_id"] + if questionnaire_store.data_stores.metadata + and questionnaire_store.data_stores.metadata.survey_metadata + else None + ) + + if ( + not (new_sds_dataset_id := metadata["sds_dataset_id"]) + or existing_sds_dataset_id == new_sds_dataset_id + ): + sds_dataset_id = existing_sds_dataset_id or new_sds_dataset_id + if sds_dataset_id: + logger.info( + "validating stored supplementary data", + sds_dataset_id=sds_dataset_id, + ) + # no need to fetch: either no supplementary data or it hasn't changed, just validate lists + _validate_supplementary_data_lists( + supplementary_data=questionnaire_store.data_stores.supplementary_data_store.raw_data, + schema=schema, + ) + return + + identifier = ( + get_ru_ref_without_check_letter(metadata["ru_ref"]) + if metadata["ru_ref"] + else metadata["qid"] + ) + + supplementary_data = get_supplementary_data_v1( + # Type ignore: survey_id and either ru_ref or qid are required for schemas that use supplementary data + dataset_id=new_sds_dataset_id, + identifier=identifier, # type: ignore + survey_id=metadata["survey_id"], # type: ignore + sds_schema_version=schema.json.get("sds_schema_version"), + ) + logger.info( + "fetched supplementary data", + survey_id=metadata["survey_id"], + sds_dataset_id=new_sds_dataset_id, + ) + _validate_supplementary_data_lists( + supplementary_data=supplementary_data["data"], schema=schema + ) + _set_supplementary_data( + questionnaire_store=questionnaire_store, + schema=schema, + supplementary_data=supplementary_data["data"], + ) + + +def _set_supplementary_data( + *, + questionnaire_store: QuestionnaireStore, + schema: QuestionnaireSchema, + supplementary_data: dict, +) -> None: + """ + Adds the supplementary data to the questionnaire store which: + 1) removes any old list items and answers + 2) Updates block and section progress to reflect any newly unlocked questions due to supplementary data list changes + """ + router = Router(schema=schema, data_stores=questionnaire_store.data_stores) + base_questionnaire_store_updater = QuestionnaireStoreUpdaterBase( + questionnaire_store=questionnaire_store, schema=schema, router=router + ) + base_questionnaire_store_updater.set_supplementary_data(to_set=supplementary_data) + base_questionnaire_store_updater.remove_dependent_blocks_and_capture_dependent_sections() + base_questionnaire_store_updater.update_progress_for_dependent_sections() + + +def _validate_supplementary_data_lists( + *, supplementary_data: dict, schema: QuestionnaireSchema +) -> None: + """ + Validates that any lists the schema requires (which are those in the supplementary_data.lists property) + are included in the supplementary data + """ + supplementary_lists = supplementary_data.get("items", {}).keys() + if missing := schema.supplementary_lists - supplementary_lists: + missing_schema_lists_error_message = f"Supplementary data does not include the following lists required for the schema: {', '.join(missing)}" + raise ValidationError(missing_schema_lists_error_message) + + +def validate_jti(decrypted_token: dict[str, str | list | int]) -> None: + # Type ignore: decrypted_token["exp"] will return a valid timestamp with compatible typing + expires_at = datetime.fromtimestamp(decrypted_token["exp"], tz=timezone.utc) # type: ignore jwt_expired = expires_at < datetime.now(tz=timezone.utc) if jwt_expired: raise Unauthorized jti_claim = decrypted_token.get("jti") try: - use_jti_claim(jti_claim, expires_at) + # Type ignore: decrypted_token.get("jti") will return a valid JTI with compatible typing + use_jti_claim(jti_claim, expires_at) # type: ignore except JtiTokenUsed as e: raise Unauthorized from e except (TypeError, ValueError) as e: @@ -113,29 +254,32 @@ def validate_jti(decrypted_token): @session_blueprint.route("/session-expired", methods=["GET"]) -def get_session_expired(): +def get_session_expired() -> tuple[str, int]: # Check for GET as we don't want to log out for HEAD requests if request.method == "GET": logout_user() - return render_template("errors/401") + return _render_error_page(200, template="401") @session_blueprint.route("/session-expiry", methods=["GET", "PATCH"]) @login_required -def session_expiry(): - return jsonify(expires_at=get_session_store().expiration_time.isoformat()) +def session_expiry() -> Response: + # Type ignore: @login_required endpoint will ensure a session store exists + return jsonify(expires_at=get_session_store().expiration_time.isoformat()) # type: ignore @session_blueprint.route("/sign-out", methods=["GET"]) -def get_sign_out(): +def get_sign_out() -> Response: """ Signs the user out of eQ and redirects to the log out url. """ survey_config = get_survey_config() - log_out_url = ( - survey_config.account_service_todo_url if "todo" in request.args else None - ) + log_out_url = None + if "internal_redirect" in request.args: + log_out_url = url_for("session.get_signed_out") + elif "todo" in request.args: + log_out_url = survey_config.account_service_todo_url if not log_out_url: log_out_url = survey_config.account_service_log_out_url @@ -144,9 +288,41 @@ def get_sign_out(): if request.method == "GET": logout_user() - return redirect(log_out_url) + # Type ignore: Logic above to set log_out_url ensures it is not None + return redirect(log_out_url) # type: ignore @session_blueprint.route("/signed-out", methods=["GET"]) -def get_signed_out(): - return render_template(template="signed-out") +def get_signed_out() -> Response | str: + if not cookie_session: + return redirect(url_for("session.get_session_expired")) + + survey_config = get_survey_config() + redirect_url = ( + survey_config.account_service_todo_url + or survey_config.account_service_log_out_url + ) + return render_template( + template="signed-out", + redirect_url=redirect_url, + ) + + +def get_runner_claims(decrypted_token: Mapping[str, Any]) -> dict: + try: + return validate_runner_claims_v2(decrypted_token) + + except ValidationError as e: + raise InvalidTokenException(RUNNER_CLAIMS_ERROR_MESSAGE) from e + + +def get_questionnaire_claims( + decrypted_token: Mapping, schema_metadata: Iterable[Mapping[str, str]] +) -> dict: + + try: + claims = decrypted_token.get("survey_metadata", {}).get("data", {}) + return validate_questionnaire_claims(claims, schema_metadata, unknown=INCLUDE) + + except ValidationError as e: + raise InvalidTokenException(QUESTIONNAIRE_CLAIMS_ERROR_MESSAGE) from e diff --git a/app/secrets.py b/app/secrets.py index d993171848..e50228d467 100644 --- a/app/secrets.py +++ b/app/secrets.py @@ -1,3 +1,7 @@ +from typing import Mapping + +SecretsType = Mapping[str, Mapping[str, str]] + REQUIRED_SECRETS = [ "EQ_SERVER_SIDE_STORAGE_USER_ID_SALT", "EQ_SERVER_SIDE_STORAGE_USER_IK_SALT", @@ -8,7 +12,9 @@ ] -def validate_required_secrets(secrets, additional_required_secrets=None): +def validate_required_secrets( + secrets: SecretsType, additional_required_secrets: list[str] | None = None +) -> None: all_required_secrets = ( REQUIRED_SECRETS + additional_required_secrets if additional_required_secrets @@ -16,12 +22,13 @@ def validate_required_secrets(secrets, additional_required_secrets=None): ) for required_secret in all_required_secrets: if required_secret not in secrets["secrets"]: - raise Exception(f"Missing Secret [{required_secret}]") + missing_secret_error_message = f"Missing Secret [{required_secret}]" + raise ValueError(missing_secret_error_message) class SecretStore: - def __init__(self, secrets): + def __init__(self, secrets: SecretsType) -> None: self.secrets = secrets.get("secrets", {}) - def get_secret_by_name(self, secret_name): + def get_secret_by_name(self, secret_name: str) -> str | None: return self.secrets.get(secret_name) diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/services/supplementary_data.py b/app/services/supplementary_data.py new file mode 100644 index 0000000000..59c03d9a45 --- /dev/null +++ b/app/services/supplementary_data.py @@ -0,0 +1,150 @@ +import json +from typing import Mapping, MutableMapping +from urllib.parse import urlencode + +from flask import current_app +from marshmallow import ValidationError +from requests import RequestException +from sdc.crypto.jwe_helper import InvalidTokenException, JWEHelper +from sdc.crypto.key_store import KeyStore +from structlog import get_logger + +from app.keys import KEY_PURPOSE_SDS +from app.settings import SDS_OAUTH2_CLIENT_ID +from app.utilities.credentials import fetch_and_apply_oidc_credentials +from app.utilities.request_session import get_retryable_session +from app.utilities.supplementary_data_parser import validate_supplementary_data_v1 + +SUPPLEMENTARY_DATA_REQUEST_BACKOFF_FACTOR = 0.2 +SUPPLEMENTARY_DATA_REQUEST_MAX_RETRIES = 2 # Totals no. of request should be 3. The initial request + SUPPLEMENTARY_DATA_REQUEST_MAX_RETRIES +SUPPLEMENTARY_DATA_REQUEST_TIMEOUT = 3 +SUPPLEMENTARY_DATA_REQUEST_RETRY_STATUS_CODES = [ + 408, + 429, + 500, + 502, + 503, + 504, +] + +logger = get_logger() + + +class SupplementaryDataRequestFailed(Exception): + SUPPLEMENTARY_DATA_EMPTY_ERROR_MESSAGE = "Supplementary data has no data to decrypt" + SUPPLEMENTARY_DATA_ERROR_MESSAGE = "Invalid supplementary data" + + def __str__(self) -> str: + return "Supplementary Data request failed" + + +class MissingSupplementaryDataKey(Exception): + pass + + +class InvalidSupplementaryData(Exception): + pass + + +def get_supplementary_data_v1( + *, + dataset_id: str, + identifier: str, + survey_id: str, + sds_schema_version: str | None = None, +) -> dict: + # Type ignore: current_app is a singleton in this application and has the key_store key in its eq attribute. + key_store = current_app.eq["key_store"] # type: ignore + if not key_store.get_key(purpose=KEY_PURPOSE_SDS, key_type="private"): + raise MissingSupplementaryDataKey + + supplementary_data_url = f"{current_app.config['SDS_API_BASE_URL']}/v1/unit_data" + + parameters = {"dataset_id": dataset_id, "identifier": identifier} + + encoded_parameters = urlencode(parameters) + constructed_supplementary_data_url = ( + f"{supplementary_data_url}?{encoded_parameters}" + ) + + session = get_retryable_session( + max_retries=SUPPLEMENTARY_DATA_REQUEST_MAX_RETRIES, + retry_status_codes=SUPPLEMENTARY_DATA_REQUEST_RETRY_STATUS_CODES, + backoff_factor=SUPPLEMENTARY_DATA_REQUEST_BACKOFF_FACTOR, + ) + + # Type ignore: SDS_OAUTH2_CLIENT_ID is an env var which must exist as it is verified in setup.py + fetch_and_apply_oidc_credentials(session=session, client_id=SDS_OAUTH2_CLIENT_ID) # type: ignore + + try: + response = session.get( + constructed_supplementary_data_url, + timeout=SUPPLEMENTARY_DATA_REQUEST_TIMEOUT, + ) + except RequestException as exc: + logger.exception( + "Error requesting supplementary data", + supplementary_data_url=constructed_supplementary_data_url, + ) + raise SupplementaryDataRequestFailed from exc + + if response.status_code == 200: + supplementary_data_response_content = response.content.decode() + supplementary_data = decrypt_supplementary_data( + key_store=key_store, + supplementary_data=json.loads(supplementary_data_response_content), + ) + + return validate_supplementary_data( + supplementary_data=supplementary_data, + dataset_id=dataset_id, + identifier=identifier, + survey_id=survey_id, + sds_schema_version=sds_schema_version, + ) + + logger.error( + "got a non-200 response for supplementary data request", + status_code=response.status_code, + schema_url=constructed_supplementary_data_url, + ) + + raise SupplementaryDataRequestFailed + + +def decrypt_supplementary_data( + *, key_store: KeyStore, supplementary_data: MutableMapping +) -> Mapping: + if encrypted_data := supplementary_data.get("data"): + try: + decrypted_data = JWEHelper.decrypt( + encrypted_data, key_store=key_store, purpose=KEY_PURPOSE_SDS + ) + supplementary_data["data"] = json.loads(decrypted_data) + return supplementary_data + except InvalidTokenException as e: + raise InvalidSupplementaryData from e + raise ValidationError( + SupplementaryDataRequestFailed.SUPPLEMENTARY_DATA_EMPTY_ERROR_MESSAGE + ) + + +def validate_supplementary_data( + supplementary_data: Mapping, + dataset_id: str, + identifier: str, + survey_id: str, + sds_schema_version: str | None = None, +) -> dict[str, str | dict | int | list]: + try: + return validate_supplementary_data_v1( + supplementary_data=supplementary_data, + dataset_id=dataset_id, + identifier=identifier, + survey_id=survey_id, + sds_schema_version=sds_schema_version, + ) + except ValidationError as e: + raise ValidationError( + SupplementaryDataRequestFailed.SUPPLEMENTARY_DATA_ERROR_MESSAGE + ) from e diff --git a/app/settings.py b/app/settings.py index 6758bfd266..1e559a83ed 100644 --- a/app/settings.py +++ b/app/settings.py @@ -35,16 +35,18 @@ def read_file(file_name): def get_env_or_fail(key): + missing_key_error_message = f"Setting '{key}' Missing" value = os.getenv(key) if value is None: - raise Exception(f"Setting '{key}' Missing") + raise ValueError(missing_key_error_message) return value def utcoffset_or_fail(date_value, key): + datetime_offset_error_message = f"'{key}' datetime offset missing" if date_value.utcoffset() is None: - raise Exception(f"'{key}' datetime offset missing") + raise ValueError(datetime_offset_error_message) return date_value @@ -90,8 +92,7 @@ def utcoffset_or_fail(date_value, key): EQ_SESSION_TIMEOUT_SECONDS = int(os.getenv("EQ_SESSION_TIMEOUT_SECONDS", str(45 * 60))) -EQ_GOOGLE_TAG_MANAGER_ID = os.getenv("EQ_GOOGLE_TAG_MANAGER_ID") -EQ_GOOGLE_TAG_MANAGER_AUTH = os.getenv("EQ_GOOGLE_TAG_MANAGER_AUTH") +EQ_GOOGLE_TAG_ID = os.getenv("EQ_GOOGLE_TAG_ID") EQ_APPLICATION_VERSION_PATH = ".application-version" EQ_APPLICATION_VERSION = read_file(EQ_APPLICATION_VERSION_PATH) @@ -126,7 +127,7 @@ def utcoffset_or_fail(date_value, key): EQ_SESSION_ID = "eq-session-id" EQ_LIST_ITEM_ID_LENGTH = 6 -MAX_NUMBER = 9999999999 +MAX_NUMBER = 999_999_999_999_999 CONFIRMATION_EMAIL_LIMIT = int(os.getenv("CONFIRMATION_EMAIL_LIMIT", "10")) @@ -144,13 +145,33 @@ def utcoffset_or_fail(date_value, key): SURVEY_TYPE = os.getenv("SURVEY_TYPE", "business") +SDS_API_BASE_URL = os.getenv("SDS_API_BASE_URL") + +CIR_API_BASE_URL = os.getenv("CIR_API_BASE_URL") + +OIDC_TOKEN_BACKEND = os.getenv("OIDC_TOKEN_BACKEND") + +OIDC_TOKEN_VALIDITY_IN_SECONDS = int( + os.getenv("OIDC_TOKEN_VALIDITY_IN_SECONDS", "3600") +) + +OIDC_TOKEN_LEEWAY_IN_SECONDS = int(os.getenv("OIDC_TOKEN_LEEWAY_IN_SECONDS", "300")) + +SDS_OAUTH2_CLIENT_ID = os.getenv("SDS_OAUTH2_CLIENT_ID") +CIR_OAUTH2_CLIENT_ID = os.getenv("CIR_OAUTH2_CLIENT_ID") ACCOUNT_SERVICE_BASE_URL = os.getenv( "ACCOUNT_SERVICE_BASE_URL", "https://surveys.ons.gov.uk" ) +ACCOUNT_SERVICE_BASE_URL_SOCIAL = os.getenv( + "ACCOUNT_SERVICE_BASE_URL_SOCIAL", "https://start.surveys.ons.gov.uk" +) + PRINT_STYLE_SHEET_FILE_PATH = os.getenv( "PRINT_STYLE_SHEET_FILEPATH", "templates/assets/styles" ) ONS_URL = os.getenv("ONS_URL", "https://www.ons.gov.uk") + +ONS_URL_CY = os.getenv("ONS_URL_CY", "https://cy.ons.gov.uk") diff --git a/app/setup.py b/app/setup.py index e3ce2637f1..73cfe54c67 100644 --- a/app/setup.py +++ b/app/setup.py @@ -1,5 +1,4 @@ from copy import deepcopy -from typing import Dict from uuid import uuid4 import boto3 @@ -15,18 +14,20 @@ from flask_wtf.csrf import CSRFProtect from google.cloud import datastore from htmlmin.main import minify +from jinja2 import ChainableUndefined from sdc.crypto.key_store import KeyStore, validate_required_keys -from structlog import get_logger +from structlog import contextvars, get_logger from app import settings from app.authentication.authenticator import login_manager from app.authentication.cookie_session import SHA256SecureCookieSessionInterface from app.authentication.user_id_generator import UserIDGenerator from app.cloud_tasks import CloudTaskPublisher, LogCloudTaskPublisher -from app.globals import get_session_store from app.helpers import get_span_and_trace from app.jinja_filters import blueprint as filter_blueprint -from app.keys import KEY_PURPOSE_SUBMISSION +from app.keys import KEY_PURPOSE_AUTHENTICATION, KEY_PURPOSE_SUBMISSION +from app.oidc.gcp_oidc import OIDCCredentialsServiceGCP +from app.oidc.local_oidc import OIDCCredentialsServiceLocal from app.publisher import LogPublisher, PubSubPublisher from app.routes.dump import dump_blueprint from app.routes.errors import errors_blueprint @@ -36,6 +37,7 @@ from app.routes.schema import schema_blueprint from app.routes.session import session_blueprint from app.secrets import SecretStore, validate_required_secrets +from app.settings import DEFAULT_LOCALE from app.storage import Datastore, Dynamodb, Redis from app.submitter import ( GCSFeedbackSubmitter, @@ -57,25 +59,22 @@ "font-src": ["'self'", "data:", "https://fonts.gstatic.com"], "script-src": [ "'self'", - "https://www.googletagmanager.com", - "https://www.google-analytics.com", - "https://ssl.google-analytics.com", - "'unsafe-inline'", + "https://*.googletagmanager.com", ], - "style-src": [ + "style-src": ["'self'", "https://fonts.googleapis.com"], + "connect-src": [ "'self'", - "https://tagmanager.google.com", - "https://fonts.googleapis.com", - "'unsafe-inline'", + "https://*.google-analytics.com", + "https://*.analytics.google.com", + "https://*.googletagmanager.com", ], - "connect-src": ["'self'", "https://www.google-analytics.com"], - "frame-src": ["https://www.googletagmanager.com"], "img-src": [ "'self'", "data:", - "https://www.google-analytics.com", "https://ssl.gstatic.com", "https://www.gstatic.com", + "https://*.google-analytics.com", + "https://*.googletagmanager.com", ], "object-src": ["'none'"], "base-uri": ["'none'"], @@ -85,6 +84,26 @@ logger = get_logger() +BUCKET_ID_ERROR_MESSAGE = "Setting EQ_GCS_SUBMISSION_BUCKET_ID Missing" +HOST_ERROR_MESSAGE = "Setting EQ_RABBITMQ_HOST Missing" +SECONDARY_HOST_ERROR_MESSAGE = "Setting EQ_RABBITMQ_HOST_SECONDARY Missing" +SDS_CLIENT_ID_ERROR_MESSAGE = "Setting SDS_OAUTH2_CLIENT_ID Missing" +CIR_CLIENT_ID_ERROR_MESSAGE = "Setting CIR_OAUTH2_CLIENT_ID Missing" +TOKEN_BACKEND_ERROR_MESSAGE = "Setting OIDC_TOKEN_BACKEND Missing" +FEEDBACK_BUCKET_ID_ERROR_MESSAGE = "Setting EQ_GCS_FEEDBACK_BUCKET_ID Missing" +SECRET_KEY_ERROR_MESSAGE = "Application secret key does not exist" + +STORAGE_BACKEND_ERROR_MESSAGE = "Unknown EQ_STORAGE_BACKEND" +SUBMISSION_ERROR_MESSAGE = "Unknown EQ_SUBMISSION_BACKEND" +SUBMIT_CONFIRMATION_ERROR_MESSAGE = "Unknown EQ_SUBMISSION_CONFIRMATION_BACKEND" +TOKEN_BACKEND_UNKNOWN_ERROR_MESSAGE = "Unknown OIDC_TOKEN_BACKEND" +PUBLISHER_BACKEND_ERROR_MESSAGE = "Unknown EQ_PUBLISHER_BACKEND" +FEEDBACK_BACKEND_ERROR_MESSAGE = "Unknown EQ_FEEDBACK_BACKEND" + + +class MissingEnvironmentVariable(Exception): + pass + class AWSReverseProxied: def __init__(self, app): @@ -117,6 +136,7 @@ def create_app( # noqa: C901 pylint: disable=too-complex, too-many-statements with open(application.config["EQ_KEYS_FILE"], encoding="UTF-8") as keys_file: keys = yaml.safe_load(keys_file) validate_required_keys(keys, KEY_PURPOSE_SUBMISSION) + validate_required_keys(keys, KEY_PURPOSE_AUTHENTICATION) application.eq["key_store"] = KeyStore(keys) if application.config["EQ_APPLICATION_VERSION"]: @@ -129,13 +149,16 @@ def create_app( # noqa: C901 pylint: disable=too-complex, too-many-statements # before_request hooks. Otherwise any logging by the plugin in their before # request will use the logger context of the previous request. @application.before_request - def before_request(): # pylint: disable=unused-variable + def before_request(): + contextvars.clear_contextvars() + request_id = str(uuid4()) - logger.new(request_id=request_id) + + contextvars.bind_contextvars(request_id=request_id) span, trace = get_span_and_trace(flask_request.headers) if span and trace: - logger.bind(span=span, trace=trace) + contextvars.bind_contextvars(span=span, trace=trace) logger.info( "request", @@ -156,6 +179,8 @@ def before_request(): # pylint: disable=unused-variable setup_task_client(application) + setup_oidc(application) + application.eq["id_generator"] = UserIDGenerator( application.config["EQ_SERVER_SIDE_STORAGE_USER_ID_ITERATIONS"], application.eq["secret_store"].get_secret_by_name( @@ -189,7 +214,7 @@ def before_request(): # pylint: disable=unused-variable setup_jinja_env(application) @application.after_request - def apply_caching(response): # pylint: disable=unused-variable + def apply_caching(response): if "text/html" in response.content_type: for k, v in CACHE_HEADERS.items(): response.headers[k] = v @@ -199,7 +224,7 @@ def apply_caching(response): # pylint: disable=unused-variable return response @application.after_request - def response_minify(response): # pylint: disable=unused-variable + def response_minify(response): """ minify html response to decrease site traffic """ @@ -220,7 +245,7 @@ def response_minify(response): # pylint: disable=unused-variable return response @application.after_request - def after_request(response): # pylint: disable=unused-variable + def after_request(response): # We're using the stringified version of the Flask session to get a rough # length for the cookie. The real length won't be known yet as Flask # serializes and adds the cookie header after this method is called. @@ -238,15 +263,15 @@ def setup_jinja_env(application): # Enable whitespace removal application.jinja_env.trim_blocks = True application.jinja_env.lstrip_blocks = True + application.jinja_env.undefined = ChainableUndefined # Switch off flask default autoescaping as schema content can contain html application.jinja_env.autoescape = False - # pylint: disable=no-member application.jinja_env.add_extension("jinja2.ext.do") -def _add_cdn_url_to_csp_policy(cdn_url) -> Dict: +def _add_cdn_url_to_csp_policy(cdn_url) -> dict: csp_policy = deepcopy(CSP_POLICY) for directive in csp_policy: if directive not in ["frame-src", "object-src", "base-uri"]: @@ -284,7 +309,7 @@ def setup_storage(application): elif application.config["EQ_STORAGE_BACKEND"] == "dynamodb": setup_dynamodb(application) else: - raise Exception("Unknown EQ_STORAGE_BACKEND") + raise NotImplementedError(STORAGE_BACKEND_ERROR_MESSAGE) setup_redis(application) @@ -321,11 +346,8 @@ def setup_redis(application): def setup_submitter(application): if application.config["EQ_SUBMISSION_BACKEND"] == "gcs": - bucket_name = application.config.get("EQ_GCS_SUBMISSION_BUCKET_ID") - - if not bucket_name: - raise Exception("Setting EQ_GCS_SUBMISSION_BUCKET_ID Missing") - + if not (bucket_name := application.config.get("EQ_GCS_SUBMISSION_BUCKET_ID")): + raise MissingEnvironmentVariable(BUCKET_ID_ERROR_MESSAGE) application.eq["submitter"] = GCSSubmitter(bucket_name=bucket_name) elif application.config["EQ_SUBMISSION_BACKEND"] == "rabbitmq": @@ -333,9 +355,9 @@ def setup_submitter(application): secondary_host = application.config.get("EQ_RABBITMQ_HOST_SECONDARY") if not host: - raise Exception("Setting EQ_RABBITMQ_HOST Missing") + raise MissingEnvironmentVariable(HOST_ERROR_MESSAGE) if not secondary_host: - raise Exception("Setting EQ_RABBITMQ_HOST_SECONDARY Missing") + raise MissingEnvironmentVariable(SECONDARY_HOST_ERROR_MESSAGE) application.eq["submitter"] = RabbitMQSubmitter( host=host, @@ -354,7 +376,7 @@ def setup_submitter(application): application.eq["submitter"] = LogSubmitter() else: - raise Exception("Unknown EQ_SUBMISSION_BACKEND") + raise NotImplementedError(SUBMISSION_ERROR_MESSAGE) def setup_task_client(application): @@ -363,7 +385,29 @@ def setup_task_client(application): elif application.config["EQ_SUBMISSION_CONFIRMATION_BACKEND"] == "log": application.eq["cloud_tasks"] = LogCloudTaskPublisher() else: - raise Exception("Unknown EQ_SUBMISSION_CONFIRMATION_BACKEND") + raise NotImplementedError(SUBMIT_CONFIRMATION_ERROR_MESSAGE) + + +def setup_oidc(application): + def client_ids_exist(): + if not application.config.get("SDS_OAUTH2_CLIENT_ID"): + raise MissingEnvironmentVariable(SDS_CLIENT_ID_ERROR_MESSAGE) + + if not application.config.get("CIR_OAUTH2_CLIENT_ID"): + raise MissingEnvironmentVariable(CIR_CLIENT_ID_ERROR_MESSAGE) + + if not (oidc_token_backend := application.config.get("OIDC_TOKEN_BACKEND")): + raise MissingEnvironmentVariable(TOKEN_BACKEND_ERROR_MESSAGE) + + if oidc_token_backend == "gcp": + client_ids_exist() + application.eq["oidc_credentials_service"] = OIDCCredentialsServiceGCP() + + elif oidc_token_backend == "local": + application.eq["oidc_credentials_service"] = OIDCCredentialsServiceLocal() + + else: + raise NotImplementedError(TOKEN_BACKEND_UNKNOWN_ERROR_MESSAGE) def setup_publisher(application): @@ -374,15 +418,13 @@ def setup_publisher(application): application.eq["publisher"] = LogPublisher() else: - raise Exception("Unknown EQ_PUBLISHER_BACKEND") + raise NotImplementedError(PUBLISHER_BACKEND_ERROR_MESSAGE) def setup_feedback(application): if application.config["EQ_FEEDBACK_BACKEND"] == "gcs": - bucket_name = application.config.get("EQ_GCS_FEEDBACK_BUCKET_ID") - - if not bucket_name: - raise Exception("Setting EQ_GCS_FEEDBACK_BUCKET_ID Missing") + if not (bucket_name := application.config.get("EQ_GCS_FEEDBACK_BUCKET_ID")): + raise MissingEnvironmentVariable(FEEDBACK_BUCKET_ID_ERROR_MESSAGE) application.eq["feedback_submitter"] = GCSFeedbackSubmitter( bucket_name=bucket_name @@ -391,7 +433,7 @@ def setup_feedback(application): elif application.config["EQ_FEEDBACK_BACKEND"] == "log": application.eq["feedback_submitter"] = LogFeedbackSubmitter() else: - raise Exception("Unknown EQ_FEEDBACK_BACKEND") + raise NotImplementedError(FEEDBACK_BACKEND_ERROR_MESSAGE) def add_blueprints(application): @@ -427,9 +469,10 @@ def add_blueprints(application): def setup_secure_cookies(application): - application.secret_key = application.eq["secret_store"].get_secret_by_name( - "EQ_SECRET_KEY" - ) + secret_key = application.eq["secret_store"].get_secret_by_name("EQ_SECRET_KEY") + if not secret_key: + raise ValueError(SECRET_KEY_ERROR_MESSAGE) + application.secret_key = secret_key application.session_interface = SHA256SecureCookieSessionInterface() @@ -437,15 +480,9 @@ def setup_babel(application): application.babel = Babel(application) application.jinja_env.add_extension("jinja2.ext.i18n") - @application.babel.localeselector - def get_locale(): # pylint: disable=unused-variable - session = get_session_store() - return session.session_data.language_code if session else None - - @application.babel.timezoneselector - def get_timezone(): # pylint: disable=unused-variable - # For now regardless of locale we will show times in GMT/BST - return "Europe/London" + application.babel.init_app( + application, locale_selector=get_locale, timezone_selector=get_timezone + ) def setup_compression(application): @@ -455,7 +492,7 @@ def setup_compression(application): def add_safe_health_check(application): @application.route("/status") - def safe_health_check(): # pylint: disable=unused-variable + def safe_health_check(): data = {"status": "OK", "version": application.config["EQ_APPLICATION_VERSION"]} return json_dumps(data) @@ -472,3 +509,16 @@ def get_minimized_asset(filename): elif "js" in filename: filename = filename.replace(".js", ".min.js") return filename + + +def get_locale(): + return ( + DEFAULT_LOCALE + if cookie_session.get("language_code") == "en" + else cookie_session.get("language_code") + ) + + +def get_timezone(): + # For now regardless of locale we will show times in GMT/BST + return "Europe/London" diff --git a/app/storage/__init__.py b/app/storage/__init__.py index 70f8db306e..bada4fbfe3 100644 --- a/app/storage/__init__.py +++ b/app/storage/__init__.py @@ -1,5 +1,5 @@ -from .datastore import Datastore -from .dynamodb import Dynamodb -from .redis import Redis +from app.storage.datastore import Datastore +from app.storage.dynamodb import Dynamodb +from app.storage.redis import Redis __all__ = ["Datastore", "Dynamodb", "Redis"] diff --git a/app/storage/datastore.py b/app/storage/datastore.py index b2254d82fd..d32d9798ef 100644 --- a/app/storage/datastore.py +++ b/app/storage/datastore.py @@ -1,5 +1,3 @@ -from typing import Optional, Type - from google.api_core.retry import Retry from google.cloud import datastore from google.cloud.datastore import Entity @@ -11,13 +9,15 @@ class Datastore(StorageHandler): + UNIQUE_KEY_ERROR_MESSAGE = "Unique key checking not supported" + def __init__(self, client: datastore.Client) -> None: super().__init__(client) @Retry() def put(self, model: ModelTypes, overwrite: bool = True) -> bool: if not overwrite: - raise NotImplementedError("Unique key checking not supported") + raise NotImplementedError(self.UNIQUE_KEY_ERROR_MESSAGE) storage_model = StorageModel(model_type=type(model)) serialized_item = storage_model.serialize(model) @@ -36,7 +36,7 @@ def put(self, model: ModelTypes, overwrite: bool = True) -> bool: return True @Retry() - def get(self, model_type: Type[ModelTypes], key_value: str) -> Optional[ModelTypes]: + def get(self, model_type: type[ModelTypes], key_value: str) -> ModelTypes | None: storage_model = StorageModel(model_type=model_type) key = self.client.key(storage_model.table_name, key_value) diff --git a/app/storage/dynamodb.py b/app/storage/dynamodb.py index c9eb4c7184..f3f9335d31 100644 --- a/app/storage/dynamodb.py +++ b/app/storage/dynamodb.py @@ -1,11 +1,8 @@ -from typing import Optional, Type - import boto3 from botocore.exceptions import ClientError from app.storage.errors import ItemAlreadyExistsError - -from .storage import ModelTypes, StorageHandler, StorageModel +from app.storage.storage import ModelTypes, StorageHandler, StorageModel class Dynamodb(StorageHandler): @@ -18,9 +15,9 @@ def put(self, model: ModelTypes, overwrite: bool = True) -> bool: put_kwargs: dict = {"Item": storage_model.serialize(model)} if not overwrite: - put_kwargs[ - "ConditionExpression" - ] = f"attribute_not_exists({storage_model.key_field})" + put_kwargs["ConditionExpression"] = ( + f"attribute_not_exists({storage_model.key_field})" + ) try: response = table.put_item(**put_kwargs)["ResponseMetadata"][ @@ -34,7 +31,7 @@ def put(self, model: ModelTypes, overwrite: bool = True) -> bool: raise # pragma: no cover - def get(self, model_type: Type[ModelTypes], key_value: str) -> Optional[ModelTypes]: + def get(self, model_type: type[ModelTypes], key_value: str) -> ModelTypes | None: storage_model = StorageModel(model_type=model_type) table = self.client.Table(storage_model.table_name) key = {storage_model.key_field: key_value} diff --git a/app/storage/encrypted_questionnaire_storage.py b/app/storage/encrypted_questionnaire_storage.py index 49c66b8c46..c83abddcce 100644 --- a/app/storage/encrypted_questionnaire_storage.py +++ b/app/storage/encrypted_questionnaire_storage.py @@ -1,5 +1,4 @@ from datetime import datetime -from typing import Optional, Union import snappy from flask import current_app @@ -21,8 +20,8 @@ def save( self, data: str, collection_exercise_sid: str, - submitted_at: Optional[datetime] = None, - expires_at: Optional[datetime] = None, + submitted_at: datetime | None = None, + expires_at: datetime | None = None, ) -> None: compressed_data = snappy.compress(data) encrypted_data = self.encrypter.encrypt_data(compressed_data) @@ -39,7 +38,7 @@ def save( def get_user_data( self, - ) -> Union[tuple[None, None, None, None], tuple[str, str, int, Optional[datetime]]]: + ) -> tuple[None, None, None, None] | tuple[str, str, int, datetime | None]: questionnaire_state = self._find_questionnaire_state() if questionnaire_state and questionnaire_state.state_data: version = questionnaire_state.version @@ -58,7 +57,7 @@ def delete(self) -> None: if questionnaire_state: current_app.eq["storage"].delete(questionnaire_state) # type: ignore - def _find_questionnaire_state(self) -> Optional[QuestionnaireState]: + def _find_questionnaire_state(self) -> QuestionnaireState | None: logger.debug("getting questionnaire data", user_id=self._user_id) state: QuestionnaireState = current_app.eq["storage"].get(QuestionnaireState, self._user_id) # type: ignore return state diff --git a/app/storage/redis.py b/app/storage/redis.py index 2253caf16f..b9aa1e2d7b 100644 --- a/app/storage/redis.py +++ b/app/storage/redis.py @@ -1,15 +1,13 @@ from datetime import datetime, timezone -from typing import Optional, Type import redis from redis.exceptions import ConnectionError as RedisConnectionError from structlog import get_logger from app.storage.errors import ItemAlreadyExistsError +from app.storage.storage import ModelTypes, StorageHandler, StorageModel from app.utilities.json import json_dumps, json_loads -from .storage import ModelTypes, StorageHandler, StorageModel - logger = get_logger() @@ -57,7 +55,7 @@ def put(self, model: ModelTypes, overwrite: bool = True) -> bool: return True - def get(self, model_type: Type[ModelTypes], key_value: str) -> Optional[ModelTypes]: + def get(self, model_type: type[ModelTypes], key_value: str) -> ModelTypes | None: storage_model = StorageModel(model_type=model_type) try: item = self.client.get(key_value) diff --git a/app/storage/storage.py b/app/storage/storage.py index 323ceca8f8..9b88715d19 100644 --- a/app/storage/storage.py +++ b/app/storage/storage.py @@ -2,34 +2,35 @@ from abc import ABC, abstractmethod from functools import cached_property -from typing import Any, Optional, Type, TypedDict, Union +from typing import Any, TypedDict from flask import current_app from google.cloud import datastore from app.data_models import app_models -ModelSchemaTypes = Union[ - app_models.QuestionnaireStateSchema, - app_models.EQSessionSchema, - app_models.UsedJtiClaimSchema, -] +ModelSchemaTypes = ( + app_models.QuestionnaireStateSchema + | app_models.EQSessionSchema + | app_models.UsedJtiClaimSchema +) -ModelTypes = Union[ - app_models.QuestionnaireState, app_models.EQSession, app_models.UsedJtiClaim -] +ModelTypes = ( + app_models.QuestionnaireState | app_models.EQSession | app_models.UsedJtiClaim +) class TableConfig(TypedDict, total=False): key_field: str table_name_key: str - schema: Type[ModelSchemaTypes] + schema: type[ModelSchemaTypes] expiry_field: str index_fields: list[str] class StorageModel: - TABLE_CONFIG_BY_TYPE: dict[Type[ModelTypes], TableConfig] = { + MODEL_TYPE_ERROR_MESSAGE = "Invalid model_type provided" + TABLE_CONFIG_BY_TYPE: dict[type[ModelTypes], TableConfig] = { app_models.QuestionnaireState: { "key_field": "user_id", "table_name_key": "EQ_QUESTIONNAIRE_STATE_TABLE_NAME", @@ -51,11 +52,11 @@ class StorageModel: }, } - def __init__(self, model_type: Type[ModelTypes]) -> None: + def __init__(self, model_type: type[ModelTypes]) -> None: self._model_type = model_type if self._model_type not in self.TABLE_CONFIG_BY_TYPE: - raise KeyError("Invalid model_type provided") + raise KeyError(self.MODEL_TYPE_ERROR_MESSAGE) self._config = self.TABLE_CONFIG_BY_TYPE[self._model_type] self._schema = self._config["schema"]() @@ -65,7 +66,7 @@ def key_field(self) -> str: return self._config["key_field"] @cached_property - def expiry_field(self) -> Optional[str]: + def expiry_field(self) -> str | None: return self._config.get("expiry_field") @cached_property @@ -81,7 +82,7 @@ def serialize(self, model_to_serialize: ModelTypes) -> dict: serialized_data: dict = self._schema.dump(model_to_serialize) return serialized_data - def deserialize(self, serialized_item: Union[datastore.Entity]) -> ModelTypes: + def deserialize(self, serialized_item: datastore.Entity) -> ModelTypes: deserialized_data: ModelTypes = self._schema.load(serialized_item) return deserialized_data @@ -95,7 +96,7 @@ def put(self, model: ModelTypes, overwrite: bool = True) -> bool: pass # pragma: no cover @abstractmethod - def get(self, model_type: Type[ModelTypes], key_value: str) -> Optional[ModelTypes]: + def get(self, model_type: type[ModelTypes], key_value: str) -> ModelTypes | None: pass # pragma: no cover @abstractmethod diff --git a/app/storage/storage_encryption.py b/app/storage/storage_encryption.py index 9b9311a0e1..73f23c533b 100644 --- a/app/storage/storage_encryption.py +++ b/app/storage/storage_encryption.py @@ -1,5 +1,4 @@ import hashlib -from typing import Optional, Union from jwcrypto import jwe, jwk from jwcrypto.common import base64url_encode @@ -12,15 +11,19 @@ class StorageEncryption: + USER_ID_ERROR_MESSAGE = "user_id not provided" + USER_IK_ERROR_MESSAGE = "user_ik not provided" + PEPPER_ERROR_MESSAGE = "pepper not provided" + def __init__( - self, user_id: Optional[str], user_ik: Optional[str], pepper: Optional[str] + self, user_id: str | None, user_ik: str | None, pepper: str | None ) -> None: if not user_id: - raise ValueError("user_id not provided") + raise ValueError(self.USER_ID_ERROR_MESSAGE) if not user_ik: - raise ValueError("user_ik not provided") + raise ValueError(self.USER_IK_ERROR_MESSAGE) if not pepper: - raise ValueError("pepper not provided") + raise ValueError(self.PEPPER_ERROR_MESSAGE) self.key = self._generate_key(user_id, user_ik, pepper) @@ -38,7 +41,7 @@ def _generate_key(user_id: str, user_ik: str, pepper: str) -> jwk.JWK: return jwk.JWK(**password) - def encrypt_data(self, data: Union[str, dict]) -> str: + def encrypt_data(self, data: str | dict) -> str: if isinstance(data, dict): data = json_dumps(data) diff --git a/app/submitter/__init__.py b/app/submitter/__init__.py index 1e32035a9d..faa6996e15 100644 --- a/app/submitter/__init__.py +++ b/app/submitter/__init__.py @@ -1,4 +1,4 @@ -from .submitter import ( +from app.submitter.submitter import ( GCSFeedbackSubmitter, GCSSubmitter, LogFeedbackSubmitter, @@ -7,9 +7,9 @@ ) __all__ = [ + "GCSFeedbackSubmitter", "GCSSubmitter", + "LogFeedbackSubmitter", "LogSubmitter", "RabbitMQSubmitter", - "GCSFeedbackSubmitter", - "LogFeedbackSubmitter", ] diff --git a/app/submitter/convert_payload_0_0_1.py b/app/submitter/convert_payload_0_0_1.py index c3c2cf79d5..64ce678da7 100644 --- a/app/submitter/convert_payload_0_0_1.py +++ b/app/submitter/convert_payload_0_0_1.py @@ -1,25 +1,26 @@ from collections import OrderedDict from datetime import datetime, timezone -from typing import Any, Mapping, Optional, Union +from typing import Any, Iterable, Mapping, MutableMapping -from app.data_models import AnswerStore, ListStore +from werkzeug.datastructures import ImmutableDict + +from app.data_models import AnswerStore from app.data_models.answer import AnswerValueTypes, ListAnswer +from app.data_models.data_stores import DataStores from app.questionnaire import QuestionnaireSchema from app.questionnaire.location import Location from app.questionnaire.routing_path import RoutingPath from app.questionnaire.variants import choose_question_to_display -MetadataType = Mapping[str, Union[str, int, list]] +MetadataType = MutableMapping[str, str | int | list] # pylint: disable=too-many-locals,too-many-nested-blocks def convert_answers_to_payload_0_0_1( - metadata: MetadataType, - response_metadata: MetadataType, - answer_store: AnswerStore, - list_store: ListStore, + *, + data_stores: DataStores, schema: QuestionnaireSchema, - full_routing_path: RoutingPath, + full_routing_path: Iterable[RoutingPath], ) -> OrderedDict[str, Any]: """ Convert answers into the data format below @@ -28,10 +29,7 @@ def convert_answers_to_payload_0_0_1( '001': '01-01-2016', '002': '30-03-2016' } - :param metadata: questionnaire metadata - :param response_metadata: response metadata - :param answer_store: questionnaire answers - :param list_store: list store + :param data_stores: questionnaire data stores :param schema: QuestionnaireSchema class with populated schema json :param full_routing_path: a list of section routing paths followed in the questionnaire :return: data in a formatted form @@ -40,14 +38,14 @@ def convert_answers_to_payload_0_0_1( for routing_path in full_routing_path: for block_id in routing_path: answer_ids = schema.get_answer_ids_for_block(block_id) - answers_in_block = answer_store.get_answers_by_answer_id( + answers_in_block = data_stores.answer_store.get_answers_by_answer_id( answer_ids, routing_path.list_item_id ) for answer_in_block in answers_in_block: answer_schema = None - block = schema.get_block_for_answer_id(answer_in_block.answer_id) + block: ImmutableDict = schema.get_block_for_answer_id(answer_in_block.answer_id) # type: ignore current_location = Location( block_id=block_id, section_id=routing_path.section_id, @@ -56,15 +54,15 @@ def convert_answers_to_payload_0_0_1( question = choose_question_to_display( block, schema, - metadata, - response_metadata, - answer_store, - list_store, + data_stores, current_location=current_location, ) - for answer in question["answers"]: - if answer["id"] == answer_in_block.answer_id: + for answer_id, answer in schema.get_answers_for_question_by_id( + question + ).items(): + if answer_id == answer_in_block.answer_id: answer_schema = answer + break value = answer_in_block.value @@ -72,7 +70,7 @@ def convert_answers_to_payload_0_0_1( if answer_schema["type"] == "Checkbox": data.update( _get_checkbox_answer_data( - answer_store, answer_schema, value # type: ignore + data_stores.answer_store, answer_schema, value # type: ignore ) ) elif "q_code" in answer_schema: @@ -126,9 +124,9 @@ def _get_checkbox_answer_data( if option: if "detail_answer" in option: detail_answer = answer_store.get_answer(option["detail_answer"]["id"]) - # if the user has selected an option with a detail answer we need to find the detail answer value it refers to. - # the detail answer value can be empty, in this case we just use the main value (e.g. other) - user_answer = detail_answer.value or user_answer # type: ignore + if detail_answer: + # Ignore mypy type because the answer type can be any non strings, but user_answer is expected to be a string. + user_answer = detail_answer.value # type: ignore qcodes_and_values.append((option.get("q_code"), user_answer)) @@ -144,7 +142,7 @@ def _get_checkbox_answer_data( return checkbox_answer_data -def _encode_value(value: AnswerValueTypes) -> Optional[str]: +def _encode_value(value: AnswerValueTypes) -> str | None: if isinstance(value, str): if value == "": return None diff --git a/app/submitter/convert_payload_0_0_3.py b/app/submitter/convert_payload_0_0_3.py index 25e75ce445..fa6ccfe62f 100644 --- a/app/submitter/convert_payload_0_0_3.py +++ b/app/submitter/convert_payload_0_0_3.py @@ -1,4 +1,4 @@ -from typing import Mapping, Optional +from typing import Iterable, Mapping from app.data_models import Answer, ListStore from app.data_models.answer_store import AnswerStore @@ -10,10 +10,11 @@ # pylint: disable=too-many-locals def convert_answers_to_payload_0_0_3( + *, answer_store: AnswerStore, list_store: ListStore, schema: QuestionnaireSchema, - full_routing_path: RoutingPath, + full_routing_path: Iterable[RoutingPath], ) -> list[Answer]: """ Convert answers into the data format below @@ -69,8 +70,23 @@ def convert_answers_to_payload_0_0_3( ) answer_ids = schema.get_answer_ids_for_block(block_id) + static_answer_ids = [ + answer_id + for answer_id in answer_ids + if not schema.is_answer_dynamic(answer_id) + ] + has_dynamic_answers = len(static_answer_ids) != len(answer_ids) + + if answer_ids and has_dynamic_answers: + resolve_dynamic_answers( + question=schema.get_all_questions_for_block(block)[0], + answer_store=answer_store, + answers_payload=answers_payload, + list_store=list_store, + ) + answers_in_block = answer_store.get_answers_by_answer_id( - answer_ids, list_item_id=routing_path.list_item_id + static_answer_ids, list_item_id=routing_path.list_item_id ) for answer_in_block in answers_in_block: answers_payload.add_or_update(answer_in_block) @@ -108,7 +124,7 @@ def add_relationships_unrelated_answers( section_id: str, relationships_block: Mapping, answers_payload: AnswerStore, -) -> Optional[RelationshipStore]: +) -> RelationshipStore | None: relationships_answer_id = schema.get_first_answer_id_for_block( relationships_block["id"] ) @@ -144,3 +160,20 @@ def add_relationships_unrelated_answers( ) ): answers_payload.add_or_update(unrelated_answer) + + +def resolve_dynamic_answers( + question: Mapping, + answer_store: AnswerStore, + answers_payload: AnswerStore, + list_store: ListStore, +) -> None: + dynamic_answers = question["dynamic_answers"] + values = dynamic_answers["values"] + for answer in dynamic_answers["answers"]: + if values["source"] == "list": + for list_item_id in list_store[values["identifier"]].items: + if extracted_answer := answer_store.get_answer( + answer["id"], list_item_id=list_item_id + ): + answers_payload.add_or_update(extracted_answer) diff --git a/app/submitter/converter.py b/app/submitter/converter.py deleted file mode 100644 index 8d19bf706a..0000000000 --- a/app/submitter/converter.py +++ /dev/null @@ -1,153 +0,0 @@ -from datetime import datetime -from typing import Any, Mapping, Union - -from structlog import get_logger - -from app.data_models import QuestionnaireStore -from app.questionnaire.questionnaire_schema import ( - DEFAULT_LANGUAGE_CODE, - QuestionnaireSchema, -) -from app.questionnaire.routing_path import RoutingPath -from app.submitter.convert_payload_0_0_1 import convert_answers_to_payload_0_0_1 -from app.submitter.convert_payload_0_0_3 import convert_answers_to_payload_0_0_3 - -logger = get_logger() - -MetadataType = Mapping[str, Union[str, int, list]] - - -class DataVersionError(Exception): - def __init__(self, version: str): - super().__init__() - self.version = version - - def __str__(self) -> str: - return f"Data version {self.version} not supported" - - -def convert_answers( - schema: QuestionnaireSchema, - questionnaire_store: QuestionnaireStore, - routing_path: RoutingPath, - submitted_at: datetime, - flushed: bool = False, -) -> dict[str, Any]: - """ - Create the JSON answer format for down stream processing in the following format: - ``` - { - 'tx_id': '0f534ffc-9442-414c-b39f-a756b4adc6cb', - 'type' : 'uk.gov.ons.edc.eq:surveyresponse', - 'version' : '0.0.1', - 'origin' : 'uk.gov.ons.edc.eq', - 'survey_id': '021', - 'flushed': true|false - 'collection':{ - 'exercise_sid': 'hfjdskf', - 'schema_name': 'yui789', - 'period': '2016-02-01' - }, - 'started_at': '2016-03-06T15:28:05Z', - 'submitted_at': '2016-03-07T15:28:05Z', - 'launch_language_code': 'en', - 'channel': 'RH', - 'metadata': { - 'user_id': '789473423', - 'ru_ref': '432423423423' - }, - 'data': [ - ... - ], - } - ``` - - Args: - schema: QuestionnaireSchema instance with populated schema json - questionnaire_store: EncryptedQuestionnaireStorage instance for accessing current questionnaire data - routing_path: The full routing path followed by the user when answering the questionnaire - submitted_at: The date and time of submission - flushed: True when system submits the users answers, False when submitted by user. - Returns: - Data payload - """ - metadata = questionnaire_store.metadata - response_metadata = questionnaire_store.response_metadata - answer_store = questionnaire_store.answer_store - list_store = questionnaire_store.list_store - - survey_id = schema.json["survey_id"] - - payload = { - "case_id": metadata["case_id"], - "tx_id": metadata["tx_id"], - "type": "uk.gov.ons.edc.eq:surveyresponse", - "version": schema.json["data_version"], - "origin": "uk.gov.ons.edc.eq", - "survey_id": survey_id, - "flushed": flushed, - "submitted_at": submitted_at.isoformat(), - "collection": build_collection(metadata), - "metadata": build_metadata(metadata), - "launch_language_code": metadata.get("language_code", DEFAULT_LANGUAGE_CODE), - } - - optional_properties = get_optional_payload_properties(metadata, response_metadata) - - if schema.json["data_version"] == "0.0.3": - payload["data"] = { - "answers": convert_answers_to_payload_0_0_3( - answer_store, list_store, schema, routing_path - ), - "lists": list_store.serialize(), - } - elif schema.json["data_version"] == "0.0.1": - payload["data"] = convert_answers_to_payload_0_0_1( - metadata, response_metadata, answer_store, list_store, schema, routing_path - ) - else: - raise DataVersionError(schema.json["data_version"]) - - logger.info("converted answer ready for submission") - - return payload | optional_properties - - -def build_collection(metadata: MetadataType) -> MetadataType: - collection_metadata = { - "exercise_sid": metadata["collection_exercise_sid"], - "schema_name": metadata["schema_name"], - "period": metadata["period_id"], - } - - if form_type := metadata.get("form_type"): - collection_metadata["instrument_id"] = form_type - - return collection_metadata - - -def build_metadata(metadata: MetadataType) -> MetadataType: - downstream_metadata = {"user_id": metadata["user_id"], "ru_ref": metadata["ru_ref"]} - - if metadata.get("ref_p_start_date"): - downstream_metadata["ref_period_start_date"] = metadata["ref_p_start_date"] - if metadata.get("ref_p_end_date"): - downstream_metadata["ref_period_end_date"] = metadata["ref_p_end_date"] - if metadata.get("display_address"): - downstream_metadata["display_address"] = metadata["display_address"] - - return downstream_metadata - - -def get_optional_payload_properties( - metadata: MetadataType, response_metadata: Mapping -) -> MetadataType: - payload = {} - - for key in ["channel", "case_type", "form_type", "region_code", "case_ref"]: - if value := metadata.get(key): - payload[key] = value - if started_at := response_metadata.get("started_at"): - payload["started_at"] = started_at - - return payload diff --git a/app/submitter/converter_v2.py b/app/submitter/converter_v2.py new file mode 100644 index 0000000000..90905e09b0 --- /dev/null +++ b/app/submitter/converter_v2.py @@ -0,0 +1,167 @@ +from datetime import datetime +from typing import Any, Iterable, Mapping, MutableMapping, OrderedDict + +from structlog import get_logger + +from app.authentication.auth_payload_versions import AuthPayloadVersion +from app.data_models import QuestionnaireStore +from app.data_models.data_stores import DataStores +from app.data_models.metadata_proxy import MetadataProxy, NoMetadataException +from app.questionnaire.questionnaire_schema import ( + DEFAULT_LANGUAGE_CODE, + QuestionnaireSchema, +) +from app.questionnaire.routing_path import RoutingPath +from app.submitter.convert_payload_0_0_1 import convert_answers_to_payload_0_0_1 +from app.submitter.convert_payload_0_0_3 import convert_answers_to_payload_0_0_3 + +logger = get_logger() + +MetadataType = Mapping[str, str | list | None] + + +class DataVersionError(Exception): + def __init__(self, version: str): + super().__init__() + self.version = version + + def __str__(self) -> str: + return f"Data version {self.version} not supported" + + +def convert_answers_v2( + schema: QuestionnaireSchema, + questionnaire_store: QuestionnaireStore, + full_routing_path: Iterable[RoutingPath], + submitted_at: datetime, + flushed: bool = False, +) -> dict[str, Any]: + """ + Create the JSON answer format for down stream processing, the format can be found here: + https://github.com/ONSdigital/ons-schema-definitions/blob/main/docs/eq_runner_to_downstream_payload_v2.md + + Args: + schema: QuestionnaireSchema instance with populated schema json + questionnaire_store: EncryptedQuestionnaireStorage instance for accessing current questionnaire data + full_routing_path: The full routing path followed by the user when answering the questionnaire + submitted_at: The date and time of submission + flushed: True when system submits the users answers, False when submitted by user. + Returns: + Data payload + """ + metadata = questionnaire_store.data_stores.metadata + if not metadata: + raise NoMetadataException + + data_stores = questionnaire_store.data_stores + + survey_id = schema.json["survey_id"] + + payload: dict = { + "case_id": metadata.case_id, + "tx_id": metadata.tx_id, + "type": "uk.gov.ons.edc.eq:surveyresponse", + "version": AuthPayloadVersion.V2.value, + "data_version": schema.json["data_version"], + "origin": "uk.gov.ons.edc.eq", + "collection_exercise_sid": metadata.collection_exercise_sid, + "flushed": flushed, + "submitted_at": submitted_at.isoformat(), + "launch_language_code": metadata.language_code or DEFAULT_LANGUAGE_CODE, + "survey_metadata": {"survey_id": survey_id}, + } + + optional_properties = get_optional_payload_properties( + metadata, data_stores.response_metadata + ) + + if metadata.schema_name: + payload["schema_name"] = metadata.schema_name + elif metadata.schema_url: + payload["schema_url"] = metadata.schema_url + elif metadata.cir_instrument_id: + payload["cir_instrument_id"] = metadata.cir_instrument_id + + if metadata.survey_metadata: + payload["survey_metadata"].update(metadata.survey_metadata.data) + + payload["data"] = get_payload_data( + data_stores=data_stores, + schema=schema, + full_routing_path=full_routing_path, + ) + + logger.info("converted answer ready for submission") + + return payload | optional_properties + + +def get_optional_payload_properties( + metadata: MetadataProxy, response_metadata: MutableMapping +) -> dict: + payload = {} + + for key in ["channel", "region_code"]: + if value := metadata[key]: + payload[key] = value + if started_at := response_metadata.get("started_at"): + payload["started_at"] = started_at + + return payload + + +def get_payload_data( + data_stores: DataStores, + schema: QuestionnaireSchema, + full_routing_path: Iterable[RoutingPath], +) -> OrderedDict | dict[str, list | dict]: + if schema.json["data_version"] == "0.0.1": + return convert_answers_to_payload_0_0_1( + data_stores=data_stores, + schema=schema, + full_routing_path=full_routing_path, + ) + + if schema.json["data_version"] == "0.0.3": + answers = convert_answers_to_payload_0_0_3( + answer_store=data_stores.answer_store, + list_store=data_stores.list_store, + schema=schema, + full_routing_path=full_routing_path, + ) + + lists: list = data_stores.list_store.serialize() + for list_ in lists: + # for any lists that were populated by supplementary data, provide the identifier -> list_item_id mappings + if mapping := data_stores.supplementary_data_store.list_mappings.get( + list_["name"] + ): + list_["supplementary_data_mappings"] = mapping + + data: dict[str, list | dict] = {"answers": answers, "lists": lists} + + if data_stores.supplementary_data_store.raw_data: + data["supplementary_data"] = data_stores.supplementary_data_store.raw_data + + if answer_codes := schema.json.get("answer_codes"): + answer_ids_to_filter = {answer.answer_id for answer in answers} + if filtered_answer_codes := get_filtered_answer_codes( + answer_codes=answer_codes, answer_ids_to_filter=answer_ids_to_filter + ): + data["answer_codes"] = filtered_answer_codes + + return data + + raise DataVersionError(schema.json["data_version"]) + + +def get_filtered_answer_codes( + *, answer_codes: Iterable[dict], answer_ids_to_filter: set[str] +) -> list[dict[str, str]]: + filtered_answer_codes: list[dict] = [] + filtered_answer_codes.extend( + answer_code + for answer_code in answer_codes + if answer_code["answer_id"] in answer_ids_to_filter + ) + return filtered_answer_codes diff --git a/app/submitter/previously_submitted_exception.py b/app/submitter/previously_submitted_exception.py index f467943703..70249a63d7 100644 --- a/app/submitter/previously_submitted_exception.py +++ b/app/submitter/previously_submitted_exception.py @@ -1,8 +1,5 @@ -from typing import Union - - class PreviouslySubmittedException(Exception): - def __init__(self, value: Union[str, int]) -> None: + def __init__(self, value: str | int) -> None: super().__init__() self.value = value diff --git a/app/submitter/submitter.py b/app/submitter/submitter.py index c5779d59b4..48e6038511 100644 --- a/app/submitter/submitter.py +++ b/app/submitter/submitter.py @@ -1,8 +1,8 @@ -from typing import Mapping, Optional +from typing import Mapping from uuid import uuid4 +from google.api_core.exceptions import Forbidden from google.cloud import storage # type: ignore -from google.cloud.storage.retry import DEFAULT_RETRY from pika import BasicProperties, BlockingConnection, URLParameters from pika.exceptions import AMQPError, NackError, UnroutableError from structlog import get_logger @@ -14,13 +14,19 @@ class LogSubmitter: @staticmethod - def send_message(message: str, tx_id: str, case_id: str) -> bool: + def send_message( + message: str, + tx_id: str, + case_id: str, + **kwargs: Mapping[str, str | int], + ) -> bool: logger.info("sending message") logger.info( "message payload", message=message, case_id=case_id, tx_id=tx_id, + **kwargs, ) return True @@ -31,16 +37,33 @@ def __init__(self, bucket_name: str) -> None: client = storage.Client() self.bucket = client.get_bucket(bucket_name) - def send_message(self, message: str, tx_id: str, case_id: str) -> bool: + def send_message( + self, + message: str, + tx_id: str, + case_id: str, + **kwargs: dict, + ) -> bool: logger.info("sending message") blob = self.bucket.blob(tx_id) - blob.metadata = {"tx_id": tx_id, "case_id": case_id} - # DEFAULT_RETRY is not idempotent. - # However, this behaviour was deemed acceptable for our use case. - blob.upload_from_string(str(message).encode("utf8"), retry=DEFAULT_RETRY) + metadata: dict = {"tx_id": tx_id, "case_id": case_id, **kwargs} + + blob.metadata = metadata + + try: + blob.upload_from_string(str(message).encode("utf8")) + except Forbidden as e: + # If an object exists then the GCS Client will attempt to delete the existing object before reuploading. + # However, in an attempt to reduce duplicate receipts, runner does not have a delete permission. + # The first version of the object is acceptable as it is an extreme edge case for two submissions to contain different response data. + if "storage.objects.delete" not in e.message: + raise + logger.info( + "Questionnaire submission exists, ignoring delete operation error" + ) return True @@ -51,8 +74,8 @@ def __init__( secondary_host: str, port: int, queue: str, - username: Optional[str] = None, - password: Optional[str] = None, + username: str | None = None, + password: str | None = None, ) -> None: self.queue = queue if username and password: @@ -94,7 +117,7 @@ def _connect(self) -> BlockingConnection: raise err @staticmethod - def _disconnect(connection: Optional[BlockingConnection]) -> None: + def _disconnect(connection: BlockingConnection | None) -> None: try: if connection: logger.info("attempt to close connection", category="rabbitmq") @@ -153,9 +176,7 @@ def upload(self, metadata: MetadataType, payload: str) -> bool: blob = self.bucket.blob(str(uuid4())) blob.metadata = metadata - # DEFAULT_RETRY is not idempotent. - # However, this behaviour was deemed acceptable for our use case. - blob.upload_from_string(payload.encode("utf8"), retry=DEFAULT_RETRY) + blob.upload_from_string(payload.encode("utf8")) return True diff --git a/app/survey_config/__init__.py b/app/survey_config/__init__.py index 5b13ae4ef5..5246a21eb7 100644 --- a/app/survey_config/__init__.py +++ b/app/survey_config/__init__.py @@ -1,18 +1,35 @@ -from .business_config import BusinessSurveyConfig, NorthernIrelandBusinessSurveyConfig -from .census_config import ( - CensusNISRASurveyConfig, - CensusSurveyConfig, - WelshCensusSurveyConfig, +from app.survey_config.business_config import ( + BusinessSurveyConfig, + DBTBusinessSurveyConfig, + DBTDSITBusinessSurveyConfig, + DBTDSITNIBusinessSurveyConfig, + DBTNIBusinessSurveyConfig, + DESNZBusinessSurveyConfig, + DESNZNIBusinessSurveyConfig, + NIBusinessSurveyConfig, + ORRBusinessSurveyConfig, ) -from .link import Link -from .survey_config import SurveyConfig +from app.survey_config.link import Link +from app.survey_config.social_survey_config import ( + ONSNHSSocialSurveyConfig, + SocialSurveyConfig, + UKHSAONSSocialSurveyConfig, +) +from app.survey_config.survey_config import SurveyConfig __all__ = [ - "SurveyConfig", - "CensusSurveyConfig", - "CensusNISRASurveyConfig", - "WelshCensusSurveyConfig", "BusinessSurveyConfig", - "NorthernIrelandBusinessSurveyConfig", + "DBTBusinessSurveyConfig", + "DBTDSITBusinessSurveyConfig", + "DBTDSITNIBusinessSurveyConfig", + "DBTNIBusinessSurveyConfig", + "DESNZBusinessSurveyConfig", + "DESNZNIBusinessSurveyConfig", "Link", + "NIBusinessSurveyConfig", + "ONSNHSSocialSurveyConfig", + "ORRBusinessSurveyConfig", + "SocialSurveyConfig", + "SurveyConfig", + "UKHSAONSSocialSurveyConfig", ] diff --git a/app/survey_config/business_config.py b/app/survey_config/business_config.py index 5d73b67f95..bcd3dcb748 100644 --- a/app/survey_config/business_config.py +++ b/app/survey_config/business_config.py @@ -1,22 +1,23 @@ from dataclasses import dataclass, field -from typing import Iterable, Mapping, MutableMapping, Optional +from typing import Iterable, Mapping, MutableMapping +from urllib.parse import urlencode from warnings import warn from flask_babel import lazy_gettext +from app.helpers.metadata_helpers import get_ru_ref_without_check_letter +from app.settings import read_file from app.survey_config.link import HeaderLink, Link from app.survey_config.survey_config import SurveyConfig @dataclass -class BusinessSurveyConfig( - SurveyConfig, -): +class BusinessSurveyConfig(SurveyConfig): survey_title: str = "ONS Business Surveys" footer_links: Iterable[MutableMapping] = field(default_factory=list) footer_legal_links: Iterable[Mapping] = field(default_factory=list) - def __post_init__(self): + def __post_init__(self) -> None: self.base_url = self._stripped_base_url super().__post_init__() @@ -29,40 +30,90 @@ def __post_init__(self): if not self.account_service_todo_url: self.account_service_todo_url: str = f"{self.base_url}/surveys/todo" - self.footer_links = [ - Link(lazy_gettext("What we do"), self.what_we_do_url).__dict__, - Link(lazy_gettext("Contact us"), self.contact_us_url).__dict__, - Link( - lazy_gettext("Accessibility"), - self.accessibility_url, - ).__dict__, - ] - self.footer_legal_links = [ - Link(lazy_gettext("Cookies"), self.cookie_settings_url).__dict__, - Link( - lazy_gettext("Privacy and data protection"), - self.privacy_and_data_protection_url, - ).__dict__, - ] + def _get_account_service_help_url( + self, *, is_authenticated: bool, ru_ref: str | None + ) -> str: + if self.schema and is_authenticated and ru_ref: + request_data = { + "survey_ref": self.schema.json["survey_id"], + # This is a temporary fix to send upstream only the first 11 characters of the ru_ref. + # The ru_ref currently is concatenated with the check letter. Which upstream currently do not support. + # The first 11 characters represents the reporting unit reference. + # The 12th character is the check letter identifier. + "ru_ref": get_ru_ref_without_check_letter(ru_ref), + } + return f"{self.base_url}/surveys/surveys-help?{urlencode(request_data)}" + + return f"{self.base_url}/help" def get_service_links( - self, sign_out_url: str, *, is_authenticated: bool - ) -> Optional[list[dict]]: - return ( + self, + sign_out_url: str, + *, + is_authenticated: bool, + cookie_has_theme: bool, + ru_ref: str | None, + ) -> list[dict] | None: + links = ( [ HeaderLink( - lazy_gettext("My account"), - self.account_service_my_account_url, - id="header-link-my-account", - ).__dict__, - HeaderLink( - lazy_gettext("Sign out"), sign_out_url, id="header-link-sign-out" - ).__dict__, + lazy_gettext("Help"), + self._get_account_service_help_url( + is_authenticated=is_authenticated, ru_ref=ru_ref + ), + id="header-link-help", + ).__dict__ ] - if is_authenticated - else None + if cookie_has_theme + else [] + ) + if is_authenticated: + links.extend( + [ + HeaderLink( + lazy_gettext("My account"), + self.account_service_my_account_url, + id="header-link-my-account", + ).__dict__, + HeaderLink( + lazy_gettext("Sign out"), + sign_out_url, + id="header-link-sign-out", + ).__dict__, + ] + ) + + return links + + def get_footer_links(self, cookie_has_theme: bool) -> list[dict]: + links = [Link(lazy_gettext("What we do"), self.what_we_do_url).as_dict()] + + if cookie_has_theme: + links.append( + Link(lazy_gettext("Contact us"), self.contact_us_url).as_dict() + ) + + links.append( + Link( + lazy_gettext("Accessibility"), + self.accessibility_url, + ).as_dict() ) + return links + + def get_footer_legal_links(self, cookie_has_theme: bool) -> list[dict] | None: + if cookie_has_theme: + return [ + Link(lazy_gettext("Cookies"), self.cookie_settings_url).as_dict(), + Link( + lazy_gettext("Privacy and data protection"), + self.privacy_and_data_protection_url, + ).as_dict(), + ] + + return None + @property def _stripped_base_url(self) -> str: warn( @@ -72,11 +123,56 @@ def _stripped_base_url(self) -> str: @dataclass -class NorthernIrelandBusinessSurveyConfig(BusinessSurveyConfig): +class NIBusinessSurveyConfig(BusinessSurveyConfig): + masthead_logo: str = read_file("./templates/assets/images/finance-ni-logo.svg") + masthead_logo_mobile: str = read_file( + "./templates/assets/images/finance-ni-mobile-logo.svg" + ) + + +@dataclass +class DBTDSITBusinessSurveyConfig(BusinessSurveyConfig): + masthead_logo: str = read_file( + "./templates/assets/images/dbt-logo-stacked.svg" + ) + read_file("./templates/assets/images/dsit-logo-stacked.svg") + + +@dataclass +class DBTDSITNIBusinessSurveyConfig(BusinessSurveyConfig): + masthead_logo: str = ( + read_file("./templates/assets/images/dbt-logo-stacked.svg") + + read_file("./templates/assets/images/dsit-logo-stacked.svg") + + read_file("./templates/assets/images/finance-ni-logo-stacked.svg") + ) + + +@dataclass +class DBTBusinessSurveyConfig(BusinessSurveyConfig): + masthead_logo: str = read_file("./templates/assets/images/dbt-logo-stacked.svg") + + +@dataclass +class DBTNIBusinessSurveyConfig(BusinessSurveyConfig): + masthead_logo: str = read_file( + "./templates/assets/images/dbt-logo-stacked.svg" + ) + read_file("./templates/assets/images/finance-ni-logo-stacked.svg") + + +@dataclass +class DESNZBusinessSurveyConfig(BusinessSurveyConfig): + masthead_logo: str = read_file("./templates/assets/images/desnz-logo-stacked.svg") - page_header_logo: str = "ni-finance-logo" - page_header_logo_alt: str = lazy_gettext( - "Northern Ireland Department of Finance logo" + +@dataclass +class DESNZNIBusinessSurveyConfig(BusinessSurveyConfig): + masthead_logo: str = read_file( + "./templates/assets/images/desnz-logo-stacked.svg" + ) + read_file("./templates/assets/images/finance-ni-logo-stacked.svg") + + +@dataclass +class ORRBusinessSurveyConfig(BusinessSurveyConfig): + masthead_logo: str = read_file("./templates/assets/images/orr-logo.svg") + masthead_logo_mobile: str = read_file( + "./templates/assets/images/orr-mobile-logo.svg" ) - mobile_logo: str = "ni-finance-logo-mobile" - custom_header_logo: bool = True diff --git a/app/survey_config/census_config.py b/app/survey_config/census_config.py deleted file mode 100644 index 358196a588..0000000000 --- a/app/survey_config/census_config.py +++ /dev/null @@ -1,169 +0,0 @@ -from dataclasses import dataclass, field -from typing import Iterable, Mapping, MutableMapping - -from flask_babel import lazy_gettext -from flask_babel.speaklater import LazyString - -from app.survey_config.link import Link -from app.survey_config.survey_config import SurveyConfig - -EN_BASE_URL = "https://census.gov.uk" -CY_BASE_URL = "https://cyfrifiad.gov.uk" -NIR_BASE_URL = f"{EN_BASE_URL}/ni" - - -@dataclass -class CensusSurveyConfig( - SurveyConfig, -): - title_logo: str = "census-logo-en" - title_logo_alt: LazyString = lazy_gettext("Census 2021") - base_url: str = EN_BASE_URL - account_service_log_out_url: str = f"{base_url}/en/start" - design_system_theme: str = "census" - footer_links: Iterable[MutableMapping] = field( - default_factory=lambda: [ - Link( - lazy_gettext("Help"), - f"{EN_BASE_URL}/help/how-to-answer-questions/online-questions-help/", - ).__dict__, - Link(lazy_gettext("Contact us"), f"{EN_BASE_URL}/contact-us/").__dict__, - Link( - lazy_gettext("Languages"), - f"{EN_BASE_URL}/help/languages-and-accessibility/languages/", - ).__dict__, - Link( - lazy_gettext("BSL and audio videos"), - f"{EN_BASE_URL}/help/languages-and-accessibility/accessibility/accessible-videos-with-bsl/", - ).__dict__, - ], - compare=False, - ) - footer_legal_links: Iterable[Mapping] = field( - default_factory=lambda: [ - Link(lazy_gettext("Cookies"), f"{EN_BASE_URL}/cookies/").__dict__, - Link( - lazy_gettext("Accessibility statement"), - f"{EN_BASE_URL}/accessibility-statement/", - ).__dict__, - Link( - lazy_gettext("Privacy and data protection"), - f"{EN_BASE_URL}/privacy-and-data-protection/", - ).__dict__, - Link( - lazy_gettext("Terms and conditions"), - f"{EN_BASE_URL}/terms-and-conditions/", - ).__dict__, - ], - compare=False, - ) - data_layer: Iterable[Mapping] = field( - default_factory=lambda: [{"nisra": False}], compare=False - ) - survey_title: LazyString = lazy_gettext("Census 2021") - sign_out_button_text: str = lazy_gettext("Save and complete later") - - -@dataclass -class WelshCensusSurveyConfig( - CensusSurveyConfig, -): - title_logo: str = "census-logo-cy" - base_url: str = CY_BASE_URL - account_service_log_out_url: str = f"{base_url}/en/start" - footer_links: Iterable[MutableMapping] = field( - default_factory=lambda: [ - Link( - lazy_gettext("Help"), - f"{CY_BASE_URL}/help/sut-i-ateb-y-cwestiynau/help-y-cwestiynau-ar-lein/", - ).__dict__, - Link(lazy_gettext("Contact us"), f"{CY_BASE_URL}/cysylltu-a-ni/").__dict__, - Link( - lazy_gettext("Languages"), - f"{CY_BASE_URL}/help/ieithoedd-a-hygyrchedd/ieithoedd/", - ).__dict__, - Link( - lazy_gettext("BSL and audio videos"), - f"{CY_BASE_URL}/help/ieithoedd-a-hygyrchedd/hygyrchedd/fideos-hygyrch-gyda-bsl/", - ).__dict__, - ], - compare=False, - hash=False, - ) - footer_legal_links: Iterable[Mapping] = field( - default_factory=lambda: [ - Link(lazy_gettext("Cookies"), f"{CY_BASE_URL}/cwcis/").__dict__, - Link( - lazy_gettext("Accessibility statement"), - f"{CY_BASE_URL}/datganiad-hygyrchedd/", - ).__dict__, - Link( - lazy_gettext("Privacy and data protection"), - f"{CY_BASE_URL}/preifatrwydd-a-diogelu-data/", - ).__dict__, - Link( - lazy_gettext("Terms and conditions"), - f"{CY_BASE_URL}/telerau-ac-amodau/", - ).__dict__, - ], - compare=False, - hash=False, - ) - data_layer: Iterable[Mapping] = field( - default_factory=lambda: [{"nisra": False}], compare=False - ) - - -@dataclass -class CensusNISRASurveyConfig( - CensusSurveyConfig, -): - base_url: str = NIR_BASE_URL - account_service_log_out_url: str = base_url - page_header_logo: str = "nisra-logo-en" - page_header_logo_alt: str = lazy_gettext( - "Northern Ireland Statistics and Research Agency logo" - ) - custom_header_logo: bool = True - mobile_logo: str = "nisra-logo-en-mobile" - copyright_declaration: LazyString = lazy_gettext( - "Crown copyright and database rights 2021 NIMA MOU577.501." - ) - copyright_text: LazyString = lazy_gettext( - "Use of address data is subject to the terms and conditions." - ) - footer_links: Iterable[MutableMapping] = field( - default_factory=lambda: [ - Link( - lazy_gettext("Help"), - f"{NIR_BASE_URL}/help/help-with-the-questions/online-questions-help/", - ).__dict__, - Link(lazy_gettext("Contact us"), f"{NIR_BASE_URL}/contact-us/").__dict__, - ], - compare=False, - hash=False, - ) - footer_legal_links: Iterable[Mapping] = field( - default_factory=lambda: [ - Link(lazy_gettext("Cookies"), f"{NIR_BASE_URL}/cookies/").__dict__, - Link( - lazy_gettext("Accessibility statement"), - f"{NIR_BASE_URL}/accessibility-statement/", - ).__dict__, - Link( - lazy_gettext("Privacy and data protection"), - f"{NIR_BASE_URL}/privacy-and-data-protection/", - ).__dict__, - Link( - lazy_gettext("Terms and conditions"), - f"{NIR_BASE_URL}/terms-and-conditions/", - ).__dict__, - ], - compare=False, - hash=False, - ) - powered_by_logo: str = "nisra-logo-black-en" - powered_by_logo_alt: str = "NISRA - Northern Ireland Statistics and Research Agency" - data_layer: Iterable[Mapping] = field( - default_factory=lambda: [{"nisra": True}], compare=False - ) diff --git a/app/survey_config/link.py b/app/survey_config/link.py index cb5d05eaf2..c6460db774 100644 --- a/app/survey_config/link.py +++ b/app/survey_config/link.py @@ -1,5 +1,4 @@ -from dataclasses import dataclass -from typing import Optional +from dataclasses import dataclass, field from flask_babel.speaklater import LazyString @@ -8,7 +7,11 @@ class Link: text: LazyString url: str - target: Optional[str] = "_blank" + target: str | None = "_blank" + attributes: dict | None = field(default_factory=dict) + + def as_dict(self): + return {k: v for k, v in self.__dict__.items() if v} @dataclass diff --git a/app/survey_config/social_survey_config.py b/app/survey_config/social_survey_config.py new file mode 100644 index 0000000000..e3103231b0 --- /dev/null +++ b/app/survey_config/social_survey_config.py @@ -0,0 +1,82 @@ +from dataclasses import dataclass, field +from typing import Iterable, Mapping, MutableMapping + +from flask_babel import lazy_gettext + +from app.settings import ACCOUNT_SERVICE_BASE_URL_SOCIAL, ONS_URL, ONS_URL_CY, read_file +from app.survey_config.link import Link +from app.survey_config.survey_config import SurveyConfig + + +@dataclass +class SocialSurveyConfig( + SurveyConfig, +): + base_url: str = ACCOUNT_SERVICE_BASE_URL_SOCIAL + survey_title: str = "ONS Social Surveys" + footer_links: Iterable[MutableMapping] = field(default_factory=list) + footer_legal_links: Iterable[Mapping] = field(default_factory=list) + + def __post_init__(self) -> None: + super().__post_init__() + + upstream_base_url = f"{self.base_url}/{self.language_code}" + ons_url = ONS_URL_CY if self.language_code == "cy" else ONS_URL + + if not self.account_service_log_out_url: + self.account_service_log_out_url: str = f"{upstream_base_url}/start/" + + self.cookie_settings_url: str = f"{upstream_base_url}/cookies/" + self.privacy_and_data_protection_url: str = ( + f"{upstream_base_url}/privacy-and-data-protection/" + ) + + self.contact_us_url: str = f"{ons_url}/aboutus/contactus/surveyenquiries/" + self.accessibility_url: str = f"{ons_url}/help/accessibility/" + self.what_we_do_url: str = f"{ons_url}/aboutus/whatwedo/" + + def get_footer_links(self, cookie_has_theme: bool) -> list[dict]: + links = [Link(lazy_gettext("What we do"), self.what_we_do_url).as_dict()] + + if cookie_has_theme: + links.append( + Link(lazy_gettext("Contact us"), self.contact_us_url).as_dict() + ) + + links.append( + Link( + lazy_gettext("Accessibility"), + self.accessibility_url, + ).as_dict() + ) + + return links + + def get_footer_legal_links(self, cookie_has_theme: bool) -> list[dict] | None: + if cookie_has_theme: + return [ + Link(lazy_gettext("Cookies"), self.cookie_settings_url).as_dict(), + Link( + lazy_gettext("Privacy and data protection"), + self.privacy_and_data_protection_url, + ).as_dict(), + ] + + return None + + +@dataclass +class UKHSAONSSocialSurveyConfig(SocialSurveyConfig): + masthead_logo: str = read_file( + "./templates/assets/images/ukhsa-logo-stacked.svg" + ) + read_file("./templates/assets/images/ons-logo-stacked.svg") + masthead_logo_mobile: str = read_file( + "./templates/assets/images/ukhsa-logo-stacked.svg" + ) + read_file("./templates/assets/images/ons-logo-stacked.svg") + + +@dataclass +class ONSNHSSocialSurveyConfig(SocialSurveyConfig): + masthead_logo: str = read_file( + "./templates/assets/images/ons-logo-stacked.svg" + ) + read_file("./templates/assets/images/nhs-logo.svg") diff --git a/app/survey_config/survey_config.py b/app/survey_config/survey_config.py index 67cf8eab01..50d7876248 100644 --- a/app/survey_config/survey_config.py +++ b/app/survey_config/survey_config.py @@ -1,9 +1,11 @@ from dataclasses import dataclass, field -from typing import Iterable, Mapping, MutableMapping, Optional, Union +from typing import Iterable, Mapping, MutableMapping from flask_babel import lazy_gettext from flask_babel.speaklater import LazyString +from app.questionnaire import QuestionnaireSchema +from app.questionnaire.questionnaire_schema import DEFAULT_LANGUAGE_CODE from app.settings import ACCOUNT_SERVICE_BASE_URL, ONS_URL @@ -11,50 +13,60 @@ class SurveyConfig: """Valid options for defining survey-based configuration.""" - page_header_logo: Optional[str] = "ons-logo-en" - page_header_logo_alt: Optional[LazyString] = lazy_gettext( - "Office for National Statistics logo" - ) - copyright_declaration: Optional[LazyString] = lazy_gettext( + schema: QuestionnaireSchema | None = None + copyright_declaration: LazyString | None = lazy_gettext( "Crown copyright and database rights 2020 OS 100019153." ) - copyright_text: Optional[LazyString] = lazy_gettext( + copyright_text: LazyString | None = lazy_gettext( "Use of address data is subject to the terms and conditions." ) base_url: str = ACCOUNT_SERVICE_BASE_URL - account_service_my_account_url: Optional[str] = None - account_service_todo_url: Optional[str] = None - account_service_log_out_url: Optional[str] = None - title_logo: Optional[str] = None - title_logo_alt: Optional[str] = None + account_service_my_account_url: str | None = None + account_service_todo_url: str | None = None + account_service_log_out_url: str | None = None accessibility_url: str = f"{ONS_URL}/help/accessibility/" what_we_do_url: str = f"{ONS_URL}/aboutus/whatwedo/" - custom_header_logo: bool = False - mobile_logo: Optional[str] = None - powered_by_logo: Optional[str] = None - powered_by_logo_alt: Optional[str] = None + masthead_logo: str | None = None + masthead_logo_mobile: str | None = None crest: bool = True - footer_links: Optional[Iterable[MutableMapping]] = None - footer_legal_links: Optional[Iterable[Mapping]] = None - survey_title: Optional[LazyString] = None - design_system_theme: Optional[str] = None - data_layer: Iterable[Union[Mapping]] = field(default_factory=list, compare=False) + footer_links: Iterable[MutableMapping] | None = None + footer_legal_links: Iterable[Mapping] | None = None + survey_title: LazyString | None = None + design_system_theme: str | None = None sign_out_button_text: str = lazy_gettext("Save and exit survey") contact_us_url: str = field(init=False) cookie_settings_url: str = field(init=False) + cookie_domain: str = field(init=False) privacy_and_data_protection_url: str = field(init=False) + language_code: str | None = None - def __post_init__(self): + def __post_init__(self) -> None: self.contact_us_url: str = f"{self.base_url}/contact-us/" self.cookie_settings_url: str = f"{self.base_url}/cookies/" + self.cookie_domain: str = self.cookie_settings_url.split("://")[-1].split("/")[ + 0 + ] # get the FQDN of the cookie settings URL self.privacy_and_data_protection_url: str = ( f"{self.base_url}/privacy-and-data-protection/" ) + self.language_code: str = self.language_code or DEFAULT_LANGUAGE_CODE def get_service_links( # pylint: disable=unused-argument, no-self-use self, sign_out_url: str, *, is_authenticated: bool, - ) -> Optional[list[dict]]: + cookie_has_theme: bool, + ru_ref: str | None, + ) -> list[dict] | None: + return None + + def get_footer_links( # pylint: disable=unused-argument, no-self-use + self, cookie_has_theme: bool + ) -> list[dict] | None: + return None + + def get_footer_legal_links( # pylint: disable=unused-argument, no-self-use + self, cookie_has_theme: bool + ) -> list[dict] | None: return None diff --git a/app/survey_config/survey_type.py b/app/survey_config/survey_type.py new file mode 100644 index 0000000000..666b4afb1b --- /dev/null +++ b/app/survey_config/survey_type.py @@ -0,0 +1,18 @@ +from enum import Enum + + +class SurveyType(Enum): + BUSINESS = "business" + SOCIAL = "social" + DEFAULT = "default" + HEALTH = "health" + NORTHERN_IRELAND = "northernireland" + DBT = "dbt" + DBT_NI = "dbt-ni" + DBT_DSIT = "dbt-dsit" + DBT_DSIT_NI = "dbt-dsit-ni" + ORR = "orr" + DESNZ = "desnz" + DESNZ_NI = "desnz-ni" + UKHSA_ONS = "ukhsa-ons" + ONS_NHS = "ons-nhs" diff --git a/app/translations/cy/LC_MESSAGES/messages.po b/app/translations/cy/LC_MESSAGES/messages.po index c700c6b466..3af221ad79 100644 --- a/app/translations/cy/LC_MESSAGES/messages.po +++ b/app/translations/cy/LC_MESSAGES/messages.po @@ -1,54 +1,68 @@ msgid "" msgstr "" -"Project-Id-Version: eq-census\n" +"Project-Id-Version: phm\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2021-03-16 12:13+0000\n" -"PO-Revision-Date: 2021-03-16 15:52\n" +"POT-Creation-Date: 2023-04-14 12:54+0100\n" +"PO-Revision-Date: 2023-04-19 07:32\n" "Last-Translator: \n" "Language-Team: Welsh\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.9.0\n" +"Generated-By: Babel 2.11.0\n" "Plural-Forms: nplurals=6; plural=(n == 0) ? 0 : ((n == 1) ? 1 : ((n == 2) ? 2 : ((n == 3) ? 3 : ((n == 6) ? 4 : 5))));\n" -"X-Crowdin-Project: eq-census\n" -"X-Crowdin-Project-ID: 345379\n" +"X-Crowdin-Project: phm\n" +"X-Crowdin-Project-ID: 566447\n" "X-Crowdin-Language: cy\n" "X-Crowdin-File: messages.pot\n" -"X-Crowdin-File-ID: 20\n" +"X-Crowdin-File-ID: 1\n" "Language: cy_GB\n" -#: app/forms/validators.py:341 app/jinja_filters.py:94 +#: app/forms/validators.py:377 app/jinja_filters.py:115 #, python-format msgid "%(num)s year" msgid_plural "%(num)s years" msgstr[0] "%(num)s blwyddyn" -msgstr[1] "%(num)s blwyddyn" -msgstr[2] "%(num)s blwyddyn" -msgstr[3] "%(num)s blwyddyn" -msgstr[4] "%(num)s blwyddyn" -msgstr[5] "%(num)s blwyddyn" +msgstr[1] "%(num)s flwyddyn" +msgstr[2] "%(num)s flynedd" +msgstr[3] "%(num)s blynedd" +msgstr[4] "%(num)s blynedd" +msgstr[5] "%(num)s o flynyddoedd" -#: app/forms/validators.py:345 app/jinja_filters.py:102 +#: app/forms/validators.py:381 app/jinja_filters.py:123 #, python-format msgid "%(num)s month" msgid_plural "%(num)s months" msgstr[0] "%(num)s mis" msgstr[1] "%(num)s mis" -msgstr[2] "%(num)s mis" +msgstr[2] "%(num)s fis" msgstr[3] "%(num)s mis" msgstr[4] "%(num)s mis" msgstr[5] "%(num)s mis" -#: app/jinja_filters.py:145 +#: app/jinja_filters.py:165 #, python-format msgid "%(date)s at %(time)s" msgstr "%(date)s am %(time)s" -#: app/jinja_filters.py:156 +#: app/jinja_filters.py:179 #, python-format msgid "%(from_date)s to %(to_date)s" -msgstr "rhwng %(from_date)s a/ac %(to_date)s" +msgstr "%(from_date)s i %(to_date)s" + +#: app/jinja_filters.py:598 templates/partials/summary/list-summary.html:9 +msgid "Remove" +msgstr "Dileu" + +#: app/jinja_filters.py:599 templates/partials/summary/list-summary.html:10 +msgid "Remove {item_name}" +msgstr "Dileu{item_name}" + +#: app/jinja_filters.py:699 +#: templates/partials/summary/collapsible-summary.html:36 +#: templates/partials/summary/summary.html:20 +msgid "No answer provided" +msgstr "Dim ateb wedi'i roi" #: app/forms/error_messages.py:11 app/forms/error_messages.py:12 #: app/forms/error_messages.py:13 app/forms/error_messages.py:14 @@ -58,29 +72,29 @@ msgstr "Rhowch ateb" #: app/forms/error_messages.py:15 #, python-format msgid "Select an answer to ‘%(question_title)s’" -msgstr "Dewiswch ateb i ‘%(question_title)s’" +msgstr "Dewiswch ateb i %(question_title)s" #: app/forms/error_messages.py:18 -#: app/forms/field_handlers/dropdown_handler.py:11 +#: app/forms/field_handlers/dropdown_handler.py:16 msgid "Select an answer" msgstr "Dewiswch ateb" #: app/forms/error_messages.py:19 #, python-format msgid "Select at least one answer to ‘%(question_title)s’" -msgstr "Dewiswch o leiaf un ateb to ‘%(question_title)s’" +msgstr "Dewiswch o leiaf un atebi %(question_title)s" #: app/forms/error_messages.py:22 msgid "Enter a date" -msgstr "" +msgstr "Rhowch ddyddiad" -#: app/forms/error_messages.py:23 templates/partials/answers/address.html:57 +#: app/forms/error_messages.py:23 templates/partials/answers/address.html:56 msgid "Enter an address" msgstr "Rhowch gyfeiriad" #: app/forms/error_messages.py:24 msgid "Enter a duration" -msgstr "" +msgstr "Rhowch hyd" #: app/forms/error_messages.py:25 msgid "Enter an email address" @@ -93,47 +107,47 @@ msgstr "Rhowch rif ffôn symudol yn y Deyrnas Unedig" #: app/forms/error_messages.py:27 #, python-format msgid "Enter an answer more than or equal to %(min)s" -msgstr "" +msgstr "Rhowch ateb sy'n fwy na neu'n neu'n hafal i %(min)s" #: app/forms/error_messages.py:28 #, python-format msgid "Enter an answer less than or equal to %(max)s" -msgstr "Nodwch ateb sy'n hafal i %(max)s neu’n llai" +msgstr "Rhowch ateb sy'n llai na neu'n hafal i %(max)s" #: app/forms/error_messages.py:29 #, python-format msgid "Enter an answer more than %(min)s" -msgstr "" +msgstr "Rhowch ateb sy'n fwy na %(min)s" #: app/forms/error_messages.py:30 #, python-format msgid "Enter an answer less than %(max)s" -msgstr "" +msgstr "Rhowch ateb sy'n llai na %(max)s" #: app/forms/error_messages.py:31 #, python-format msgid "Enter answers that add up to %(total)s" -msgstr "Nodwch atebion sy'n creu cyfanswm o %(total)s." +msgstr "Rhowch atebion sy'n creu cyfanswm o %(total)s" #: app/forms/error_messages.py:32 #, python-format msgid "Enter answers that add up to or are less than %(total)s" -msgstr "Nodwch atebion sy'n creu cyfanswm o neu sydd o dan %(total)s" +msgstr "Rhowch atebion sy'n creu cyfanswm o neu sy'n llai na %(total)s" #: app/forms/error_messages.py:35 #, python-format msgid "Enter answers that add up to less than %(total)s" -msgstr "Nodwch atebion sy'n creu cyfanswm sydd o dan %(total)s" +msgstr "Rhowch atebion sy'n creu cyfanswm o lai na%(total)s" #: app/forms/error_messages.py:38 #, python-format msgid "Enter answers that add up to greater than %(total)s" -msgstr "Nodwch atebion sy'n creu cyfanswm sydd dros %(total)s" +msgstr "Rhowch atebion sy'n creu cyfanswm o fwy na %(total)s" #: app/forms/error_messages.py:41 #, python-format msgid "Enter answers that add up to or are greater than %(total)s" -msgstr "Nodwch atebion sy'n creu cyfanswm o neu sydd dros %(total)s" +msgstr "Rhowch atebion sy'n creu cyfanswm o neu sy'n fwy na %(total)s" #: app/forms/error_messages.py:44 msgid "Enter an email address in a valid format, for example name@example.com" @@ -141,63 +155,67 @@ msgstr "Rhowch gyfeiriad e-bost mewn fformat dilys, er enghraifft enw@enghraifft #: app/forms/error_messages.py:47 msgid "Enter a number" -msgstr "Nodwch rif" +msgstr "Rhowch rif" #: app/forms/error_messages.py:48 msgid "Enter a whole number" -msgstr "" +msgstr "Rhowch rif cyfan" #: app/forms/error_messages.py:49 #, python-format msgid "Enter a number rounded to %(max)d decimal places" -msgstr "" +msgstr "Rhowch rif wedi'i dalgrynnu i %(max)d le degol" #: app/forms/error_messages.py:50 #, python-format msgid "You have entered too many characters. Enter up to %(max)d characters" -msgstr "Rydych wedi defnyddio gormod o nodau. Rhowch hyd at %(max)d nod" +msgstr "Rydych chi wedi defnyddio gormod o nodau. Rhowch hyd at %(max)d o nodau" #: app/forms/error_messages.py:53 msgid "Enter a valid date" -msgstr "Nodwch ddyddiad dilys" +msgstr "Rhowch ddyddiad dilys" #: app/forms/error_messages.py:54 msgid "Enter a 'period to' date later than the 'period from' date" -msgstr "" +msgstr "Rhowch ddyddiad 'cyfnod hyd at' yn hytrach na dyddiad 'cyfnod o'" #: app/forms/error_messages.py:57 msgid "Enter a valid duration" -msgstr "" +msgstr "Rhowch hyd dilys" #: app/forms/error_messages.py:58 msgid "Enter a UK mobile number in a valid format, for example, 07700 900345 or +44 7700 900345" -msgstr "Rhowch rif ffôn symudol yn y Deyrnas Unedig mewn fformat dilys, er enghraifft, 07700 912345 neu +44 7700 912345" +msgstr "Rhowch rif ffôn symudol yn y Deyrnas Unedig mewn fformat dilys, er enghraifft, 07700 900345 neu +44 7700 900345" #: app/forms/error_messages.py:61 #, python-format msgid "Enter a reporting period greater than or equal to %(min)s" -msgstr "" +msgstr "Rhowch gyfnod adrodd sy'n fwy na neu'n hafal i %(min)s" #: app/forms/error_messages.py:64 #, python-format msgid "Enter a reporting period less than or equal to %(max)s" -msgstr "" +msgstr "Rhowch gyfnod adrodd sy'n llai na neu'n hafal i %(max)s" #: app/forms/error_messages.py:67 #, python-format msgid "Enter a date after %(min)s" -msgstr "Nodwch ddyddiad ar ôl %(min)s" +msgstr "Rhowch ddyddiad ar ôl %(min)s" #: app/forms/error_messages.py:68 #, python-format msgid "Enter a date before %(max)s" -msgstr "Nodwch ddyddiad cyn %(max)s" +msgstr "Rhowch ddyddiad cyn %(max)s" #: app/forms/error_messages.py:69 msgid "Remove an answer" -msgstr "" +msgstr "Dileu ateb" -#: app/forms/validators.py:352 +#: app/forms/error_messages.py:70 +msgid "Enter the year in a valid format. For example, 2023." +msgstr "Rhowch y flwyddyn mewn fformat dilys. Er enghraifft, 2023." + +#: app/forms/validators.py:388 #, python-format msgid "%(num)s day" msgid_plural "%(num)s days" @@ -208,159 +226,197 @@ msgstr[3] "%(num)s diwrnod" msgstr[4] "%(num)s diwrnod" msgstr[5] "%(num)s diwrnod" -#: app/forms/fields/select_field_with_detail_answer.py:35 -msgid "Not a valid choice" -msgstr "Ddim yn ddewis dilys" - -#: app/helpers/template_helpers.py:24 -msgid "Office for National Statistics logo" -msgstr "logo Swyddfa Ystadegau Gwladol" - -#: app/helpers/template_helpers.py:33 app/helpers/template_helpers.py:42 -#: app/helpers/template_helpers.py:170 templates/signed-out.html:3 -msgid "Census 2021" -msgstr "Cyfrifiad 2021" - -#: app/helpers/template_helpers.py:38 -msgid "Northern Ireland Statistics and Research Agency logo" -msgstr "" - -#: app/helpers/template_helpers.py:55 -msgid "Help" -msgstr "Help" - -#: app/helpers/template_helpers.py:60 -msgid "Contact us" -msgstr "Cysylltu Ãĸ ni" +#: app/forms/fields/select_field_with_detail_answer.py:39 +msgid "Not a valid choice." +msgstr "Nid yw'n ddewis dilys." -#: app/helpers/template_helpers.py:69 -msgid "Languages" -msgstr "Ieithoedd" +#: app/helpers/template_helpers.py:48 +msgid "ONS Surveys" +msgstr "Arolygon SYG" -#: app/helpers/template_helpers.py:74 -msgid "BSL and audio videos" -msgstr "Fideos BSL a sain" +#: app/helpers/template_helpers.py:114 +msgid "Menu" +msgstr "Dewislen" -#: app/helpers/template_helpers.py:84 +#: app/helpers/template_helpers.py:145 msgid "The following links open in a new tab" -msgstr "Bydd y dolenni canlynol yn agor mewn tab newydd" +msgstr "Mae'r dolenni canlynol yn agor mewn tab newydd" -#: app/helpers/template_helpers.py:86 -msgid "Crown copyright and database rights 2021 NIMA MOU577.501." -msgstr "" - -#: app/helpers/template_helpers.py:90 -msgid "Crown copyright and database rights 2020 OS 100019153." -msgstr "Hawlfraint y goron a hawliau cronfa ddata 2020 OS 100019153." - -#: app/helpers/template_helpers.py:91 -msgid "Use of address data is subject to the terms and conditions." -msgstr "Mae defnyddio data cyfeiriadau yn ddarostyngedig i’r telerau ac amodau." - -#: app/helpers/template_helpers.py:102 -msgid "Cookies" -msgstr "Cwcis" - -#: app/helpers/template_helpers.py:107 -msgid "Accessibility statement" -msgstr "Datganiad hygyrchedd" - -#: app/helpers/template_helpers.py:112 -msgid "Privacy and data protection" -msgstr "Preifatrwydd a diogelu data" - -#: app/helpers/template_helpers.py:117 -msgid "Terms and conditions" -msgstr "Telerau ac amodau" - -#: app/helpers/template_helpers.py:127 +#: app/helpers/template_helpers.py:170 msgid "Make sure you leave this page or close your browser if using a shared device" -msgstr "Cofiwch adael y dudalen hon neu gau eich porwr os ydych chi'n defnyddio dyfais sy'n cael ei rhannu" +msgstr "Cofiwch adael y dudalen hon neu gau eich porwr os ydych chi'n defnyddio dyfais sy'n cael ei rhannu" -#: app/questionnaire/placeholder_transforms.py:92 +#: app/questionnaire/placeholder_transforms.py:160 msgid "{number_of_years} year" msgid_plural "{number_of_years} years" -msgstr[0] "{number_of_years} oed" -msgstr[1] "{number_of_years} oed" -msgstr[2] "{number_of_years} oed" -msgstr[3] "{number_of_years} oed" -msgstr[4] "{number_of_years} oed" -msgstr[5] "{number_of_years} oed" - -#: app/questionnaire/placeholder_transforms.py:98 +msgstr[0] "{number_of_years} blwyddyn" +msgstr[1] "{number_of_years} flwyddyn" +msgstr[2] "{number_of_years} flynedd" +msgstr[3] "{number_of_years} blynedd" +msgstr[4] "{number_of_years} blynedd" +msgstr[5] "{number_of_years} o flynyddoedd" + +#: app/questionnaire/placeholder_transforms.py:166 msgid "{number_of_months} month" msgid_plural "{number_of_months} months" -msgstr[0] "{number_of_months} mis oed" -msgstr[1] "{number_of_months} mis oed" -msgstr[2] "{number_of_months} fis oed" -msgstr[3] "{number_of_months} mis oed" -msgstr[4] "{number_of_months} mis oed" -msgstr[5] "{number_of_months} mis oed" - -#: app/questionnaire/placeholder_transforms.py:103 +msgstr[0] "{number_of_months} mis" +msgstr[1] "{number_of_months} mis" +msgstr[2] "{number_of_months} fis" +msgstr[3] "{number_of_months} mis" +msgstr[4] "{number_of_months} mis" +msgstr[5] "{number_of_months} mis" + +#: app/questionnaire/placeholder_transforms.py:171 msgid "{number_of_days} day" msgid_plural "{number_of_days} days" -msgstr[0] "{number_of_days} diwrnod oed" -msgstr[1] "{number_of_days} diwrnod oed" -msgstr[2] "{number_of_days} ddiwrnod oed" -msgstr[3] "{number_of_days} diwrnod oed" -msgstr[4] "{number_of_days} diwrnod oed" -msgstr[5] "{number_of_days} diwrnod oed" - -#: app/routes/errors.py:115 +msgstr[0] "{number_of_days} diwrnod" +msgstr[1] "{number_of_days} diwrnod" +msgstr[2] "{number_of_days} ddiwrnod" +msgstr[3] "{number_of_days} diwrnod" +msgstr[4] "{number_of_days} diwrnod" +msgstr[5] "{number_of_days} diwrnod" + +#: app/routes/errors.py:135 msgid "You have reached the maximum number of individual access codes" -msgstr "Rydych wedi cyrraedd y nifer fwyaf o godau mynediad unigol" +msgstr "Rydych wedi cyrraedd y nifer mwyaf o godau mynediad unigol" -#: app/routes/errors.py:118 +#: app/routes/errors.py:138 msgid "If you need more individual access codes, please contact us." msgstr "Os bydd angen i chi gael mwy o godau mynediad unigol, cysylltwch Ãĸ ni." -#: app/routes/errors.py:134 +#: app/routes/errors.py:154 msgid "You have reached the maximum number of times for submitting feedback" msgstr "Ni allwch gyflwyno rhagor o adborth" -#: app/routes/errors.py:137 +#: app/routes/errors.py:157 msgid "If you need to give more feedback, please contact us." msgstr "Cysylltwch Ãĸ ni os bydd angen i chi roi rhagor o adborth." -#: app/routes/errors.py:169 +#: app/routes/errors.py:189 msgid "Sorry, there was a problem sending the access code" -msgstr "Mae'n ddrwg gennym, roedd problem wrth anfon y cod" +msgstr "Mae'n ddrwg gennym, roedd problem wrth anfon y cod mynediad" -#: app/routes/errors.py:175 +#: app/routes/errors.py:195 msgid "You can try to request a new access code again." msgstr "Gallwch geisio gofyn am god mynediad newydd eto." -#: app/routes/errors.py:178 app/routes/errors.py:201 app/routes/errors.py:223 +#: app/routes/errors.py:198 app/routes/errors.py:221 app/routes/errors.py:243 msgid "If this problem keeps happening, please contact us for help." msgstr "Os bydd y broblem hon yn parhau, cysylltwch Ãĸ ni i gael cymorth." -#: app/routes/errors.py:197 +#: app/routes/errors.py:217 msgid "Sorry, there was a problem sending the confirmation email" msgstr "Mae'n ddrwg gennym, roedd problem wrth anfon yr e-bost cadarnhau" -#: app/routes/errors.py:198 +#: app/routes/errors.py:218 msgid "You can try to send the email again." msgstr "Gallwch chi geisio anfon yr e-bost eto." -#: app/routes/errors.py:219 templates/errors/403.html:3 +#: app/routes/errors.py:239 templates/errors/403.html:3 #: templates/errors/403.html:6 templates/errors/submission-failed.html:5 #: templates/errors/submission-failed.html:8 msgid "Sorry, there is a problem" msgstr "Mae'n ddrwg gennym, mae problem wedi codi" -#: app/routes/errors.py:220 +#: app/routes/errors.py:240 msgid "You can try to submit your feedback again." msgstr "Gallwch chi geisio cyflwyno eich adborth eto." -#: app/routes/individual_response.py:167 +#: app/routes/individual_response.py:184 msgid "An individual access code has been sent by post" msgstr "Mae cod mynediad unigol wedi cael ei anfon drwy'r post" -#: app/routes/individual_response.py:262 +#: app/routes/individual_response.py:278 msgid "An individual access code has been sent by text" msgstr "Mae cod mynediad unigol wedi cael ei anfon drwy neges destun" +#: app/survey_config/business_config.py:59 +#: app/survey_config/census_config.py:33 app/survey_config/census_config.py:89 +#: app/survey_config/census_config.py:154 +msgid "Help" +msgstr "Help" + +#: app/survey_config/business_config.py:73 +msgid "My account" +msgstr "Fy nghyfrif" + +#: app/survey_config/business_config.py:78 +msgid "Sign out" +msgstr "Allgofnodi" + +#: app/survey_config/business_config.py:88 +#: app/survey_config/social_survey_config.py:39 +msgid "What we do" +msgstr "Beth rydym yn ei wneud" + +#: app/survey_config/business_config.py:92 +#: app/survey_config/census_config.py:40 app/survey_config/census_config.py:97 +#: app/survey_config/census_config.py:162 +#: app/survey_config/social_survey_config.py:43 +msgid "Contact us" +msgstr "Cysylltu Ãĸ ni" + +#: app/survey_config/business_config.py:97 +#: app/survey_config/social_survey_config.py:48 +msgid "Accessibility" +msgstr "Hygyrchedd" + +#: app/survey_config/business_config.py:107 +#: app/survey_config/census_config.py:61 app/survey_config/census_config.py:117 +#: app/survey_config/census_config.py:171 +#: app/survey_config/social_survey_config.py:58 +msgid "Cookies" +msgstr "Cwcis" + +#: app/survey_config/business_config.py:109 +#: app/survey_config/census_config.py:67 app/survey_config/census_config.py:123 +#: app/survey_config/census_config.py:177 +#: app/survey_config/social_survey_config.py:60 +msgid "Privacy and data protection" +msgstr "Preifatrwydd a diogelu data" + +#: app/survey_config/census_config.py:23 +msgid "Census 2021" +msgstr "Cyfrifiad 2021" + +#: app/survey_config/census_config.py:24 +msgid "Save and complete later" +msgstr "Cadw a chwblhau yn nes ymlaen" + +#: app/survey_config/census_config.py:46 app/survey_config/census_config.py:103 +msgid "Languages" +msgstr "Ieithoedd" + +#: app/survey_config/census_config.py:50 app/survey_config/census_config.py:107 +msgid "BSL and audio videos" +msgstr "Fideos sain ac Iaith Arwyddion Prydain" + +#: app/survey_config/census_config.py:63 app/survey_config/census_config.py:119 +#: app/survey_config/census_config.py:173 +msgid "Accessibility statement" +msgstr "Datganiad hygyrchedd" + +#: app/survey_config/census_config.py:71 app/survey_config/census_config.py:127 +#: app/survey_config/census_config.py:181 +msgid "Terms and conditions" +msgstr "Telerau ac amodau" + +#: app/survey_config/census_config.py:143 +msgid "Crown copyright and database rights 2021 NIMA MOU577.501." +msgstr "Hawlfraint y goron a hawliau cronfa ddata 2021 NIMA MOU577.501." + +#: app/survey_config/census_config.py:146 app/survey_config/survey_config.py:20 +msgid "Use of address data is subject to the terms and conditions." +msgstr "Mae defnyddio data cyfeiriadau yn ddarostyngedig i’r telerau ac amodau." + +#: app/survey_config/survey_config.py:17 +msgid "Crown copyright and database rights 2020 OS 100019153." +msgstr "Hawlfraint y goron a hawliau cronfa ddata 2020 OS 100019153." + +#: app/survey_config/survey_config.py:36 +msgid "Save and exit survey" +msgstr "Cadw a gadael yr arolwg" + #: app/views/contexts/hub_context.py:15 msgid "Completed" msgstr "Cwblhawyd" @@ -383,7 +439,7 @@ msgstr "Parhau Ãĸ'r adran" #: app/views/contexts/hub_context.py:25 msgid "Continue with {section_name} section" -msgstr "Parhau Ãĸ’r adran {section_name}" +msgstr "Parhau ag adran {section_name}" #: app/views/contexts/hub_context.py:29 msgid "Not started" @@ -399,302 +455,312 @@ msgstr "Dechrau adran {section_name}" #: app/views/contexts/hub_context.py:36 msgid "Separate census requested" -msgstr "Cais am gyfrifiad ar wahÃĸn" +msgstr "Cais am gyfrifiad ar wahÃĸn wedi'i wneud" #: app/views/contexts/hub_context.py:38 app/views/contexts/hub_context.py:39 msgid "Change or resend" msgstr "Newid neu ailanfon" -#: app/views/contexts/hub_context.py:49 app/views/contexts/hub_context.py:50 +#: app/views/contexts/hub_context.py:51 app/views/contexts/hub_context.py:52 msgid "Submit survey" msgstr "Cyflwyno'r arolwg" -#: app/views/contexts/hub_context.py:54 +#: app/views/contexts/hub_context.py:56 msgid "You must submit this survey to complete it" -msgstr "Cyflwynwch y arolwg hwn i'w gwblhau" +msgstr "Mae'n rhaid i chi gyflwyno'r arolwg hwn er mwyn ei gwblhau" -#: app/views/contexts/hub_context.py:61 +#: app/views/contexts/hub_context.py:63 msgid "Choose another section to complete" msgstr "Dewiswch adran arall i'w chwblhau" -#: app/views/contexts/hub_context.py:62 templates/confirm-email.html:26 +#: app/views/contexts/hub_context.py:64 templates/confirm-email.html:25 #: templates/individual_response/confirmation-post.html:21 -#: templates/individual_response/confirmation-text-message.html:31 +#: templates/individual_response/confirmation-text-message.html:29 #: templates/individual_response/question.html:5 templates/interstitial.html:8 -#: templates/sectionsummary.html:11 templates/sectionsummary.html:48 +#: templates/sectionsummary.html:10 templates/sectionsummary.html:50 msgid "Continue" msgstr "Parhau" -#: app/views/contexts/list_context.py:104 +#: app/views/contexts/list_context.py:109 msgid " (You)" msgstr " (Chi)" -#: app/views/contexts/questionnaire_summary_context.py:15 +#: app/views/contexts/preview_context.py:66 +msgid "Preview survey questions" +msgstr "Rhagolwg o gwestiynau'r arolwg" + +#: app/views/contexts/submission_metadata_context.py:14 +msgid "Submitted on:" +msgstr "Cyflwynwyd ar:" + +#: app/views/contexts/submission_metadata_context.py:17 +msgid "{date} at {time}" +msgstr "{date} am {time}" + +#: app/views/contexts/submission_metadata_context.py:26 +msgid "Submission reference:" +msgstr "Cyfeirnod cyflwyno:" + +#: app/views/contexts/submit_questionnaire_context.py:13 msgid "Check your answers and submit" msgstr "Gwiriwch eich atebion a'u cyflwyno" -#: app/views/contexts/questionnaire_summary_context.py:18 -#: templates/confirmation.html:6 templates/confirmation.html:28 +#: app/views/contexts/submit_questionnaire_context.py:16 msgid "Submit answers" msgstr "Cyflwyno atebion" -#: app/views/contexts/questionnaire_summary_context.py:21 +#: app/views/contexts/submit_questionnaire_context.py:19 msgid "Please submit this survey to complete it" msgstr "Cyflwynwch yr arolwg hwn i'w gwblhau" -#: app/views/handlers/confirm_email.py:31 +#: app/views/contexts/thank_you_context.py:29 +msgid "Your answers have been submitted for {company_name} ({trading_name})" +msgstr "Mae eich atebion wedi cael eu cyflwyno ar gyfer {company_name} ({trading_name})" + +#: app/views/contexts/thank_you_context.py:36 +msgid "Your answers have been submitted for {company_name}" +msgstr "Mae eich atebion wedi cael eu cyflwyno ar gyfer {company_name}" + +#: app/views/contexts/thank_you_context.py:40 +msgid "Your answers have been submitted." +msgstr "Mae eich atebion wedi cael eu cyflwyno." + +#: app/views/contexts/view_submitted_response_context.py:33 +msgid "Answers submitted for {ru_name} ({trad_as})" +msgstr "Atebion wedi'u cyflwyno ar gyfer {ru_name} ({trad_as})" + +#: app/views/contexts/view_submitted_response_context.py:37 +msgid "Answers submitted for {ru_name}" +msgstr "Atebion wedi'u cyflwyno ar gyfer {ru_name}" + +#: app/views/contexts/view_submitted_response_context.py:41 +msgid "Answers submitted." +msgstr "Atebion wedi'u cyflwyno." + +#: app/views/contexts/preview/preview_question.py:31 +msgid "You can answer with the following options:" +msgstr "Gallwch ateb gyda'r opsiynau canlynol:" + +#: app/views/contexts/preview/preview_question.py:35 +msgid "You can answer with one of the following options:" +msgstr "Gallwch ateb gydag un o'r opsiynau canlynol:" + +#: app/views/handlers/confirm_email.py:38 msgid "Yes, send the confirmation email" -msgstr "Ydy, gellir anfon yr e-bost cadarnhau" +msgstr "Ydy, anfonwch yr e-bost cadarnhau" -#: app/views/handlers/confirm_email.py:60 +#: app/views/handlers/confirm_email.py:68 msgid "Confirm your email address" msgstr "Cadarnhewch eich cyfeiriad e-bost" -#: app/views/handlers/confirm_email.py:78 +#: app/views/handlers/confirm_email.py:89 msgid "Is this email address correct?" -msgstr "A yw’r cyfeiriad e-bost hwn yn gywir?" +msgstr "Ydy'r cyfeiriad e-bost hwn yn gywir?" -#: app/views/handlers/confirm_email.py:91 -#: app/views/handlers/confirm_email.py:92 -#: app/views/handlers/individual_response.py:867 +#: app/views/handlers/confirm_email.py:102 +#: app/views/handlers/confirm_email.py:103 +#: app/views/handlers/individual_response.py:880 msgid "No, I need to change it" msgstr "Nac ydy, mae angen i mi ei newid" -#: app/views/handlers/confirm_email.py:116 -#: app/views/handlers/confirmation_email.py:66 -#: app/views/handlers/feedback.py:62 app/views/handlers/question.py:150 +#: app/views/handlers/confirm_email.py:129 +#: app/views/handlers/confirmation_email.py:67 +#: app/views/handlers/feedback.py:84 app/views/handlers/question.py:170 msgid "Error: {page_title}" msgstr "Gwall: {page_title}" -#: app/views/handlers/confirmation_email.py:46 +#: app/views/handlers/confirmation_email.py:45 msgid "Confirmation email" msgstr "E-bost cadarnhau" -#: app/views/handlers/feedback.py:28 +#: app/views/handlers/feedback.py:44 msgid "Feedback" msgstr "Adborth" -#: app/views/handlers/feedback.py:94 app/views/handlers/feedback.py:100 -#: app/views/handlers/feedback.py:108 -msgid "General" -msgstr "Cyffredinol" - -#: app/views/handlers/feedback.py:95 -msgid "This establishment" -msgstr "Y sefydliad hwn" - -#: app/views/handlers/feedback.py:96 app/views/handlers/feedback.py:109 -msgid "People who live here" -msgstr "Pobl sy'n byw yma" - -#: app/views/handlers/feedback.py:97 app/views/handlers/feedback.py:110 -msgid "Visitors" -msgstr "Ymwelwyr" - -#: app/views/handlers/feedback.py:101 -msgid "Accommodation" -msgstr "Llety" - -#: app/views/handlers/feedback.py:102 app/views/handlers/feedback.py:112 -msgid "Personal details" -msgstr "Manylion Personol" - -#: app/views/handlers/feedback.py:103 app/views/handlers/feedback.py:113 -msgid "Health" -msgstr "Iechyd" - -#: app/views/handlers/feedback.py:104 app/views/handlers/feedback.py:114 -msgid "Qualifications" -msgstr "Cymwysterau" - -#: app/views/handlers/feedback.py:105 app/views/handlers/feedback.py:115 -msgid "Employment" -msgstr "Cyflogaeth" - -#: app/views/handlers/feedback.py:111 -msgid "Household and accommodation" -msgstr "Cartref a llety" - -#: app/views/handlers/feedback.py:127 +#: app/views/handlers/feedback.py:140 msgid "Give feedback about this service" -msgstr "Rhoi adborth ar y gwasanaeth hwn" +msgstr "Rhowch adborth ar y gwasanaeth hwn" -#: app/views/handlers/feedback.py:128 app/views/handlers/feedback.py:172 +#: app/views/handlers/feedback.py:146 app/views/handlers/feedback.py:170 msgid "Select what your feedback is about" msgstr "Dewiswch am beth mae eich adborth yn sôn" -#: app/views/handlers/feedback.py:136 app/views/handlers/feedback.py:137 -msgid "The census questions" -msgstr "Cwestiynau’r cyfrifiad" +#: app/views/handlers/feedback.py:149 app/views/handlers/feedback.py:150 +msgid "The survey questions" +msgstr "Cwestiynau'r arolwg" -#: app/views/handlers/feedback.py:138 +#: app/views/handlers/feedback.py:151 msgid "For example, questions not clear, answer options not relevant" -msgstr "Er enghraifft, nid yw’r cwestiynau’n glir, opsiynau ddim yn berthnasol" - -#: app/views/handlers/feedback.py:145 -msgid "Question topic" -msgstr "Pwnc cwestiwn" - -#: app/views/handlers/feedback.py:146 app/views/handlers/feedback.py:149 -msgid "Select an option" -msgstr "Dewiswch opsiwn" +msgstr "Er enghraifft, nid yw'r cwestiynau'n glir, nid yw'r atebion posibl yn berthnasol" -#: app/views/handlers/feedback.py:158 app/views/handlers/feedback.py:159 +#: app/views/handlers/feedback.py:156 app/views/handlers/feedback.py:157 msgid "Page design and structure" msgstr "Dyluniad a strwythur tudalen" -#: app/views/handlers/feedback.py:162 app/views/handlers/feedback.py:165 +#: app/views/handlers/feedback.py:160 app/views/handlers/feedback.py:163 msgid "General feedback about this service" msgstr "Adborth cyffredinol ar y gwasanaeth hwn" -#: app/views/handlers/feedback.py:180 app/views/handlers/feedback.py:190 +#: app/views/handlers/feedback.py:178 app/views/handlers/feedback.py:188 msgid "Enter your feedback" msgstr "Rhowch eich adborth" -#: app/views/handlers/feedback.py:181 +#: app/views/handlers/feedback.py:179 msgid "Do not include confidential information, such as your contact details" msgstr "Peidiwch Ãĸ chynnwys gwybodaeth gyfrinachol, fel eich manylion cyswllt" -#: app/views/handlers/individual_response.py:137 +#: app/views/handlers/individual_response.py:140 msgid "Person {list_item_position}" -msgstr "Unigolion {list_item_position}" +msgstr "Person {list_item_position}" -#: app/views/handlers/individual_response.py:245 +#: app/views/handlers/individual_response.py:254 msgid "Cannot answer questions for others in your household" msgstr "Methu ateb cwestiynau ar ran pobl eraill yn eich cartref" -#: app/views/handlers/individual_response.py:311 -msgid "How would you like {person_name} to receive a separate census?" -msgstr "Sut hoffech chi i {person_name} gael cyfrifiad ar wahÃĸn?" +#: app/views/handlers/individual_response.py:324 +msgid "How would you like {person_name} to receive a separate census?" +msgstr "Sut hoffech chi i {person_name} gael cyfrifiad ar wahÃĸn?" -#: app/views/handlers/individual_response.py:334 +#: app/views/handlers/individual_response.py:347 msgid "Text message" msgstr "Neges destun" -#: app/views/handlers/individual_response.py:336 +#: app/views/handlers/individual_response.py:349 msgid "We will need their mobile number for this" msgstr "Bydd angen rhif ffôn symudol yr unigolyn arnom ar gyfer hyn" -#: app/views/handlers/individual_response.py:344 +#: app/views/handlers/individual_response.py:357 msgid "Post" msgstr "Post" -#: app/views/handlers/individual_response.py:346 +#: app/views/handlers/individual_response.py:359 msgid "We can only send this to an unnamed resident at the registered household address" -msgstr "Dim ond i gyfeiriad cofrestredig y cartref y gallwn anfon hwn" +msgstr "Dim ond at breswylydd dienw yng nghyfeiriad cofrestredig y cartref y gallwn anfon hwn" -#: app/views/handlers/individual_response.py:355 +#: app/views/handlers/individual_response.py:368 msgid "It is no longer possible to receive an access code by post." -msgstr "Nid yw’n bosibl cael cod mynediad drwy’r post mwyach." +msgstr "Ni ellir cael cod myneidad drwy'r post mwyach." -#: app/views/handlers/individual_response.py:357 +#: app/views/handlers/individual_response.py:370 msgid "Select how to send access code." msgstr "Dewiswch sut i anfon y cod mynediad." -#: app/views/handlers/individual_response.py:360 +#: app/views/handlers/individual_response.py:373 msgid "For someone to complete a separate census, we need to send them an individual access code." -msgstr "Er mwyn i unigolyn allu cwblhau cyfrifiad ar wahÃĸn, bydd angen i ni anfon cod mynediad unigol atynt." +msgstr "Er mwyn i rywun allu cwblhau cyfrifiad ar wahÃĸn, bydd angen i ni anfon cod mynediad unigol ato." -#: app/views/handlers/individual_response.py:406 +#: app/views/handlers/individual_response.py:419 msgid "Send individual access code" msgstr "Anfon cod mynediad unigol" -#: app/views/handlers/individual_response.py:437 -msgid "How would you like to answer {person_name_possessive} questions?" -msgstr "Sut hoffech chi ateb cwestiynau {person_name_possessive}?" +#: app/views/handlers/individual_response.py:450 +msgid "How would you like to answer {person_name_possessive} questions?" +msgstr "Sut hoffech chi ateb cwestiynau {person_name_possessive}?" -#: app/views/handlers/individual_response.py:452 +#: app/views/handlers/individual_response.py:465 msgid "I would like to request a separate census for them to complete" msgstr "Hoffwn i wneud cais am gyfrifiad ar wahÃĸn i’r unigolyn ei gwblhau" -#: app/views/handlers/individual_response.py:458 +#: app/views/handlers/individual_response.py:471 msgid "I will ask them to answer their own questions" msgstr "Byddaf yn gofyn i’r unigolyn ateb ei gwestiynau ei hun" -#: app/views/handlers/individual_response.py:462 +#: app/views/handlers/individual_response.py:475 msgid "They will need the household access code from the letter we sent you" msgstr "Bydd angen cod mynediad y cartref ar yr unigolyn o’r llythyr y gwnaethom ei anfon atoch" -#: app/views/handlers/individual_response.py:468 +#: app/views/handlers/individual_response.py:481 msgid "I will answer for {person_name}" msgstr "Byddaf yn ateb ar ran {person_name}" -#: app/views/handlers/individual_response.py:510 +#: app/views/handlers/individual_response.py:523 msgid "How to answer questions" msgstr "Sut i ateb cwestiynau" -#: app/views/handlers/individual_response.py:575 +#: app/views/handlers/individual_response.py:588 msgid "Do you want to send an individual access code for {person_name} by post?" -msgstr "Ydych chi am anfon cod mynediad unigolyn at {person_name} drwy’r post?" +msgstr "Ydych chi am anfon cod mynediad unigol at {person_name} drwy’r post?" -#: app/views/handlers/individual_response.py:583 +#: app/views/handlers/individual_response.py:596 msgid "A letter with an individual access code will be sent to your registered household address" -msgstr "Bydd llythyr Ãĸ chod mynediad unigryw yn cael ei anfon i'ch cyfeiriad cartref" +msgstr "Bydd llythyr Ãĸ chod mynediad unigol yn cael ei anfon i gyfeiriad cofrestredig eich cartref" -#: app/views/handlers/individual_response.py:590 +#: app/views/handlers/individual_response.py:603 msgid "The letter will be addressed to Individual Resident instead of the name provided" -msgstr "Bydd y llythyr wedi'i gyfeirio at y Preswylydd Unigol yn hytrach na'r enw a gafodd ei roi" +msgstr "Bydd y llythyr wedi'i gyfeirio at y Preswylydd Unigol yn hytrach na'r enw a gafodd ei roi" -#: app/views/handlers/individual_response.py:603 +#: app/views/handlers/individual_response.py:616 msgid "Yes, send the access code by post" msgstr "Ydw, anfonwch y cod mynediad drwy'r post" -#: app/views/handlers/individual_response.py:609 +#: app/views/handlers/individual_response.py:622 msgid "No, send it another way" msgstr "Nac ydw, anfonwch y cod mynediad mewn ffordd arall" -#: app/views/handlers/individual_response.py:642 +#: app/views/handlers/individual_response.py:655 msgid "Confirm address" msgstr "Cadarnhau'r cyfeiriad" -#: app/views/handlers/individual_response.py:697 -#: app/views/handlers/individual_response.py:735 +#: app/views/handlers/individual_response.py:710 +#: app/views/handlers/individual_response.py:748 msgid "Separate Census" msgstr "Cyfrifiad ar wahÃĸn" -#: app/views/handlers/individual_response.py:701 +#: app/views/handlers/individual_response.py:714 msgid "Who do you need to request a separate census for?" msgstr "Ar gyfer pwy y mae angen i chi wneud cais am gyfrifiad ar wahÃĸn?" -#: app/views/handlers/individual_response.py:759 -msgid "What is {person_name_possessive} mobile number?" -msgstr "Beth yw rhif ffôn symudol {person_name_possessive}?" +#: app/views/handlers/individual_response.py:772 +msgid "What is {person_name_possessive} mobile number?" +msgstr "Beth yw rhif ffôn symudol {person_name_possessive}?" -#: app/views/handlers/individual_response.py:771 +#: app/views/handlers/individual_response.py:784 msgid "UK mobile number" -msgstr "Rhif ffôn symudol" +msgstr "Rhif ffôn symudol yn y Deyrnas Unedig" -#: app/views/handlers/individual_response.py:772 +#: app/views/handlers/individual_response.py:785 msgid "This will not be stored and only used once to send the access code" msgstr "Ni chaiff ei storio a bydd ond yn cael ei ddefnyddio unwaith i anfon y cod mynediad" -#: app/views/handlers/individual_response.py:805 +#: app/views/handlers/individual_response.py:818 msgid "Mobile number" msgstr "Rhif ffôn symudol" -#: app/views/handlers/individual_response.py:854 +#: app/views/handlers/individual_response.py:867 msgid "Is this mobile number correct?" msgstr "Ydy’r rhif ffôn symudol hwn yn gywir?" -#: app/views/handlers/individual_response.py:863 +#: app/views/handlers/individual_response.py:876 msgid "Yes, send the text" -msgstr "Ydy, anfonwch y neges destun nawr" +msgstr "Ydy, anfonwch y neges destun" -#: app/views/handlers/individual_response.py:901 +#: app/views/handlers/individual_response.py:914 msgid "Confirm mobile number" msgstr "Cadarnhau'r rhif ffôn symudol" -#: app/views/handlers/thank_you.py:22 templates/census-thank-you.html:24 +#: app/views/handlers/thank_you.py:26 templates/census-thank-you.html:24 msgid "Thank you for completing the census" msgstr "Diolch am gwblhau'r cyfrifiad" +#: app/views/handlers/view_submitted_response.py:52 +msgid "View Submitted Response" +msgstr "Gweld yr Ymateb a Gyflwynwyd" + +#: templates/calculatedsummary.html:13 +msgid "Please review your answers and confirm these are correct" +msgstr "Darllenwch dros eich atebion a chadarnhewch fod y rhain yn gywir" + +#: templates/calculatedsummary.html:24 +msgid "Yes, I confirm these are correct" +msgstr "Rwy'n cadarnhau bod y rhain yn gywir" + #: templates/census-thank-you.html:9 templates/confirmation-email.html:8 msgid "There is a problem with this page" msgstr "Mae problem gyda'r dudalen hon" #: templates/census-thank-you.html:22 msgid "Thank you for completing your census" -msgstr "Diolch am gwblhau'r cyfrifiad" +msgstr "Diolch am gwblhau eich cyfrifiad" #: templates/census-thank-you.html:26 msgid "Thank you for completing the survey" @@ -714,7 +780,7 @@ msgstr "Mae eich cyfrifiad wedi cael ei gyflwyno ar gyfer y llety yn {di #: templates/census-thank-you.html:37 msgid "Anyone staying at this accommodation for at least 6 months needs to fill in their own individual census, including staff. Your Census Officer will provide you with census forms for your residents." -msgstr "Bydd angen i unrhyw un sy'n aros yn y sefydliad hwn am o leiaf 6 mis lenwi ei gyfrifiad unigol ei hun, gan gynnwys y staff. Bydd Swyddog y Cyfrifiad yn dosbarthu ffurflenni'r cyfrifiad i'ch preswylwyr." +msgstr "Bydd angen i unrhyw un sy'n aros yn y llety hwn am o leiaf 6 mis lenwi ei gyfrifiad unigol ei hun, gan gynnwys y staff. Bydd Swyddog y Cyfrifiad yn dosbarthu ffurflenni'r cyfrifiad i'ch preswylwyr." #: templates/census-thank-you.html:49 msgid "Your personal information is protected by law and will be kept confidential" @@ -726,9 +792,9 @@ msgstr "Cael e-bost cadarnhau" #: templates/census-thank-you.html:56 msgid "If you would like to be sent confirmation that you have completed your census, enter your email address" -msgstr "Os hoffech gael cadarnhad eich bod wedi cwblhau eich cyfrifiad, nodwch eich cyfeiriad e-bost" +msgstr "Os hoffech gael cadarnhad eich bod wedi cwblhau eich cyfrifiad, rhowch eich cyfeiriad e-bost" -#: templates/confirm-email.html:13 templates/partials/answers/address.html:56 +#: templates/confirm-email.html:12 templates/partials/answers/address.html:55 #: templates/question.html:10 #, python-format msgid "There is a problem with your answer" @@ -736,9 +802,9 @@ msgid_plural "There are %(num)s problems with your answer" msgstr[0] "" msgstr[1] "Mae problem gyda'ch ateb" msgstr[2] "Mae %(num)s broblem gyda'ch ateb" -msgstr[3] "Mae %(num)s broblem gyda'ch ateb" -msgstr[4] "Mae %(num)s broblem gyda'ch ateb" -msgstr[5] "Mae %(num)s broblem gyda'ch ateb" +msgstr[3] "Mae %(num)s problem gyda'ch ateb" +msgstr[4] "Mae %(num)s phroblem gyda'ch ateb" +msgstr[5] "Mae %(num)s problem gyda'ch ateb" #: templates/confirmation-email-sent.html:3 msgid "Confirmation email sent" @@ -749,8 +815,8 @@ msgid "A confirmation email has been sent to {email}" msgstr "Mae e-bost cadarnhau wedi cael ei anfon i {email}" #: templates/confirmation-email-sent.html:25 -msgid "The email will be sent from census.2021@notifications.service.gov.uk" -msgstr "Caiff yr e-bost ei anfon gan census.2021@notifications.service.gov.uk" +msgid "The email will be sent from census.2021@notifications.service.gov.uk" +msgstr "Caiff yr e-bost ei anfon o census.2021@notifications.service.gov.uk" #: templates/confirmation-email-sent.html:29 msgid "Didn't receive an email?" @@ -764,31 +830,24 @@ msgstr "Gall gymryd ychydig funudau i’r e-bost gyrraedd. Os na fydd yn cyrraed msgid "Send a confirmation email" msgstr "Anfon e-bost cadarnhau" -#: templates/confirmation.html:12 -msgid "Yes, I confirm these are correct" -msgstr "Ydw, rwy'n cadarnhau bod y rhain yn gywir" - -#: templates/confirmation.html:33 -msgid "Submitting" -msgstr "Yn cyflwyno" - #: templates/feedback-sent.html:6 msgid "Feedback sent" msgstr "Wedi anfon adborth" -#: templates/feedback-sent.html:19 +#: templates/feedback-sent.html:17 msgid "Thank you for your feedback" msgstr "Diolch am eich adborth" -#: templates/feedback-sent.html:20 +#: templates/feedback-sent.html:18 msgid "Your comments will help us make improvements to our surveys. We are not able to reply to comments, but we appreciate your feedback" msgstr "Bydd eich sylwadau yn ein helpu ni i wella ein harolygon. Ni allwn ymateb i sylwadau, ond rydym yn gwerthfawrogi eich adborth" -#: templates/feedback-sent.html:28 +#: templates/feedback-sent.html:24 msgid "Done" msgstr "Cwblhawyd" -#: templates/feedback.html:15 +#: templates/feedback.html:14 templates/preview.html:15 +#: templates/view-submitted-response.html:16 msgid "Back" msgstr "Yn ôl" @@ -796,48 +855,38 @@ msgstr "Yn ôl" #, python-format msgid "There is a problem with your feedback" msgid_plural "There are %(num)s problems with your feedback" +msgstr[0] "" msgstr[1] "Mae problem gyda'ch adborth" -msgstr[2] "Mae %(num)s o broblemau gyda'ch adborth" -msgstr[3] "Mae %(num)s o broblemau gyda'ch adborth" -msgstr[4] "Mae %(num)s o broblemau gyda'ch adborth" -msgstr[5] "Mae %(num)s o broblemau gyda'ch adborth" +msgstr[2] "Mae %(num)s broblem gyda'ch adborth" +msgstr[3] "Mae %(num)s problem gyda'ch adborth" +msgstr[4] "Mae %(num)s phroblem gyda'ch adborth" +msgstr[5] "Mae %(num)s problem gyda'ch adborth" -#: templates/feedback.html:41 +#: templates/feedback.html:39 msgid "Send feedback" msgstr "Anfon adborth" -#: templates/hub.html:40 +#: templates/hub.html:7 msgid "If you can’t answer someone else’s questions" msgstr "Os na allwch chi ateb cwestiynau rhywun arall" -#: templates/interstitial.html:23 templates/partials/question.html:27 +#: templates/interstitial.html:23 templates/partials/question.html:24 msgid "If you can’t answer questions for this person" -msgstr "Os na allwch ateb cwestiynau ar ran yr unigolyn hwn" +msgstr "Os na allwch chi ateb cwestiynau ar ran yr unigolyn hwn" -#: templates/interstitial.html:32 templates/layouts/_questionnaire.html:27 +#: templates/interstitial.html:31 templates/layouts/_questionnaire.html:26 msgid "Save and continue" msgstr "Cadw a pharhau" #: templates/introduction.html:10 msgid "Introduction" -msgstr "" - -#: templates/introduction.html:17 -#, python-format -msgid "You are completing this for %(ru_name)s (%(trading_as_name)s)" -msgstr "Rydych chi'n cwblhau hwn ar gyfer %(ru_name)s (%(trading_as_name)s)" - -#: templates/introduction.html:21 -#, python-format -msgid "You are completing this for %(ru_name)s" -msgstr "Rydych chi'n cwblhau hwn ar gyfer %(ru_name)s" +msgstr "Cyflwyniad" -#: templates/introduction.html:25 -#, python-format -msgid "If the company details or structure have changed contact us on %(telephone_number)s or email %(email_address)s" -msgstr "Os yw manylion neu strwythur y cwmni wedi newid, cysylltwch Ãĸ ni ar %(telephone_number)s neu e-bost % (email_address)s" +#: templates/introduction.html:24 +msgid "View the questions you will be asked in this survey" +msgstr "Gweld y cwestiynau a ofynnir i chi yn yr arolwg hwn" -#: templates/introduction.html:40 +#: templates/introduction.html:29 msgid "Your response is legally required" msgstr "Mae eich ymateb yn ofynnol yn gyfreithiol" @@ -857,53 +906,186 @@ msgstr "Yn anffodus, dim ond un arolwg y gallwch chi ei gwblhau ar y tro" msgid "Close this window to continue with your current survey" msgstr "Caewch y ffenestr hon i barhau Ãĸ'ch arolwg presennol" -#: templates/signed-out.html:3 +#: templates/preview.html:36 +msgid "Preview of the questions in this survey" +msgstr "Rhagolwg o'r cwestiynau yn yr arolwg hwn" + +#: templates/preview.html:41 +msgid "To answer these questions you need to start survey" +msgstr "I ateb y cwestiynau hyn mae angen i chi dechrau arolwg" + +#: templates/preview.html:43 +msgid "You may not have to answer all of these questions. The questions you see will depend on the answers you provide." +msgstr "Efallai na fydd yn rhaid i chi ateb pob un o'r cwestiynau hyn. Bydd y cwestiynau a welwch yn dibynnu ar yr atebion a roddwch." + +#: templates/preview.html:48 +msgid "Print questions" +msgstr "Argraffu cwestiynau" + +#: templates/preview.html:60 +msgid "Save questions as PDF" +msgstr "Cadw cwestiynau fel PDF" + +#: templates/partials/introduction/preview.html:29 +#: templates/partials/summary/collapsible-summary.html:14 +#: templates/preview.html:81 +msgid "Show" +msgstr "Dangos" + +#: templates/layouts/_base.html:74 +#: templates/partials/introduction/preview.html:30 +#: templates/partials/summary/collapsible-summary.html:15 +#: templates/preview.html:82 +msgid "Hide" +msgstr "Cuddio" + +#: templates/partials/introduction/preview.html:47 +#: templates/partials/summary/collapsible-summary.html:59 +#: templates/preview.html:102 +msgid "Show all" +msgstr "Dangos popeth" + +#: templates/partials/introduction/preview.html:48 +#: templates/partials/summary/collapsible-summary.html:60 +#: templates/preview.html:103 +msgid "Hide all" +msgstr "Cuddio popeth" + +#: templates/signed-out.html:5 msgid "Signed out" msgstr "Wedi allgofnodi" -#: templates/signed-out.html:6 -msgid "Your survey answers have been saved. You are now signed out" -msgstr "Mae eich atebion i'r arolwg wedi'u cadw. Rydych chi nawr wedi'ch allgofnodi" +#: templates/signed-out.html:16 +msgid "

Your progress has been saved

" +msgstr "

Mae eich cynnydd wedi'i gadw

" -#: templates/signed-out.html:9 -msgid "Return to your account" -msgstr "Dychwelyd i'ch cyfrif" +#: templates/signed-out.html:21 +msgid "To find further information or resume the survey, return to My Account." +msgstr "I ddod o hyd i ragor o wybodaeth neu i ailddechrau'r arolwg, dychwelwch i Fy Nghyfrif." -#: templates/summary.html:28 -msgid "Please review your answers and confirm these are correct" -msgstr "Adolygwch eich atebion a chadarnhewch fod y rhain yn gywir" +#: templates/signed-out.html:23 +msgid "To resume the survey, re-enter your access code." +msgstr "I ailddechrau'r arolwg, ail-nodwch eich cod mynediad." -#: templates/thank-you.html:4 +#: templates/thank-you.html:6 msgid "We’ve received your answers" -msgstr "" +msgstr "Rydym ni wedi derbyn eich atebion" -#: templates/thank-you.html:9 -msgid "Submission successful" -msgstr "" +#: templates/thank-you.html:15 +msgid "Back to surveys" +msgstr "Yn ôl i'r arolygon" -#: templates/thank-you.html:21 -msgid "Your answers were submitted for {ru_name} ({trading_as_name}) on {submitted_date_time}" -msgstr "" - -#: templates/thank-you.html:26 -msgid "Your answers were submitted for {ru_name} on {submitted_date_time}" -msgstr "" - -#: templates/thank-you.html:32 -msgid "Transaction ID: {transaction_id}" -msgstr "" +#: templates/thank-you.html:33 +msgid "Thank you for completing the {survey_title}" +msgstr "Diolch am gwblhau'r {survey_title}" -#: templates/thank-you.html:35 +#: templates/thank-you.html:45 msgid "Your answers will be processed in the next few weeks." -msgstr "" +msgstr "Caiff eich atebion eu prosesu yn ystod yr ychydig wythnosau nesaf." -#: templates/thank-you.html:36 +#: templates/thank-you.html:46 msgid "We may contact you to query your answers via phone or secure message." -msgstr "" +msgstr "Efallai y byddwn ni'n cysylltu Ãĸ chi i drafod eich atebion dros y ffôn neu drwy neges ddiogel." -#: templates/thank-you.html:37 +#: templates/thank-you.html:47 msgid "For more information on how we use this data." -msgstr "" +msgstr "I gael rhagor o wybodaeth am sut rydym ni'n defnyddio'r data hyn." + +#: templates/thank-you.html:51 templates/view-submitted-response.html:69 +msgid "For security, you can no longer view or get a copy of your answers" +msgstr "Er diogelwch, ni allwch weld eich atebion na chael copi ohonynt mwyach" + +#: templates/thank-you.html:63 +msgid "For security, your answers will only be available to view for another " +msgstr "Er diogelwch, byddwch ond yn gallu gweld eich atebion am " + +#: templates/thank-you.html:64 +msgid "Get a copy of your answers" +msgstr "Cael copi o'ch atebion" + +#: templates/thank-you.html:66 +msgid "You can save or print your answers for your records." +msgstr "Gallwch gadw neu argraffu eich atebion ar gyfer eich cofnodion." + +#: templates/layouts/_base.html:157 templates/thank-you.html:71 +msgid "minute" +msgstr "munud" + +#: templates/layouts/_base.html:158 templates/thank-you.html:72 +msgid "minutes" +msgstr "munudau" + +#: templates/layouts/_base.html:159 templates/thank-you.html:73 +msgid "second" +msgstr "eiliad" + +#: templates/layouts/_base.html:160 templates/thank-you.html:74 +msgid "seconds" +msgstr "eiliadau" + +#: templates/thank-you.html:76 +msgid "For security, your answers will only be available to view for 45 minutes" +msgstr "Er diogelwch, byddwch ond yn gallu gweld eich atebion am 45 munud" + +#: templates/view-submitted-response.html:35 +msgid "Print answers" +msgstr "Argraffu atebion" + +#: templates/view-submitted-response.html:47 +msgid "Save answers as PDF" +msgstr "Cadw atebion fel PDF" + +#: templates/errors/401.html:3 +msgid "Page is not available" +msgstr "Nid yw'r dudalen ar gael" + +#: templates/errors/401.html:6 +msgid "Sorry, you need to sign in again" +msgstr "Mae'n ddrwg gennym, mae angen i chi fewngofnodi eto" + +#: templates/errors/401.html:7 +msgid "This is because you have either:" +msgstr "Mae hyn oherwydd eich bod chi naill ai:" + +#: templates/errors/401.html:9 +msgid "been inactive for 45 minutes and your session has timed out to protect your information" +msgstr "wedi bod yn anweithgar am 45 munud a bod eich sesiwn wedi cyrraedd y terfyn amser er mwyn diogelu eich gwybodaeth" + +#: templates/errors/401.html:10 +msgid "followed a link to a page you are not signed in to" +msgstr "wedi dilyn dolen i dudalen nad ydych wedi mewngofnodi iddi" + +#: templates/errors/401.html:11 +msgid "followed a link to a survey that has already been submitted" +msgstr "wedi dilyn dolen i arolwg sydd eisoes wedi'i gyflwyno" + +#: templates/errors/401.html:14 +msgid "You will need to sign back in to access your account" +msgstr "Bydd angen i chi fewngofnodi eto i gael mynediad i'ch cyfrif" + +#: templates/errors/401.html:17 +msgid "To access this page you need to re-enter your access code." +msgstr "I fynd i'r dudalen hon, bydd angen i chi roi eich cod mynediad eto." + +#: templates/errors/401.html:20 templates/errors/403.html:12 +#: templates/errors/404.html:13 templates/errors/500.html:12 +#: templates/errors/submission-failed.html:14 +msgid "Business surveys" +msgstr "Arolygon busnes" + +#: templates/errors/401.html:21 +msgid "If you are completing a business survey, you need to sign back in to your account." +msgstr "Os ydych chi'n cwblhau arolwg busnes, bydd angen i chi fewngofnodi eto i'ch cyfrif." + +#: templates/errors/401.html:22 templates/errors/403.html:14 +#: templates/errors/404.html:15 templates/errors/500.html:14 +#: templates/errors/submission-failed.html:16 +msgid "All other surveys" +msgstr "Pob arolwg arall" + +#: templates/errors/401.html:23 +msgid "If you started your survey using an access code, you need to re-enter your code." +msgstr "Os gwnaethoch chi ddechrau eich arolwg gan ddefnyddio cod mynediad, bydd angen i chi roi eich cod eto." #: templates/errors/403.html:7 msgid "You may need to update your browser to a newer version." @@ -913,10 +1095,18 @@ msgstr "Efallai y bydd angen i chi ddiweddaru eich porwr i fersiwn fwy newydd." msgid "If the problem still occurs, try using a different browser or device." msgstr "Os bydd y broblem yn parhau, rhowch gynnig ar ddefnyddio porwr neu ddyfais wahanol." -#: templates/errors/403.html:9 +#: templates/errors/403.html:10 msgid "For further help, please contact us." msgstr "I gael rhagor o gymorth, cysylltwch Ãĸ ni." +#: templates/errors/403.html:13 +msgid "If you are completing a business survey and you need further help, please contact us." +msgstr "Os ydych chi'n cwblhau arolwg busnes a bod angen rhagor o gymorth arnoch, cysylltwch Ãĸ ni." + +#: templates/errors/403.html:15 +msgid "If you started your survey using an access code and you need further help, please contact us." +msgstr "Os gwnaethoch chi ddechrau eich arolwg gan ddefnyddio cod mynediad a bod angen rhagor o gymorth arnoch, cysylltwch Ãĸ ni." + #: templates/errors/404.html:3 templates/errors/404.html:6 msgid "Page not found" msgstr "Heb ddod o hyd i'r dudalen" @@ -929,10 +1119,22 @@ msgstr "Os gwnaethoch chi roi cyfeiriad gwe, gwnewch yn siÅĩr ei fod yn gywir." msgid "If you pasted the web address, check you copied the whole address." msgstr "Os gwnaethoch chi ludo'r cyfeiriad gwe, gwnewch yn siÅĩr eich bod wedi copïo'r cyfeiriad cyfan." -#: templates/errors/404.html:9 +#: templates/errors/404.html:10 msgid "If the web address is correct or you selected a link or button, contact us for more help." msgstr "Os yw'r cyfeiriad gwe yn gywir, neu os gwnaethoch chi ddewis dolen neu fotwm, cysylltwch Ãĸ ni am fwy o gymorth." +#: templates/errors/404.html:12 +msgid "If the web address is correct or you selected a link or button, please see the following help links." +msgstr "Os yw'r cyfeiriad gwe yn gywir, neu os gwnaethoch chi ddewis dolen neu fotwm, edrychwch ar y dolenni cymorth canlynol." + +#: templates/errors/404.html:14 templates/errors/submission-failed.html:15 +msgid "If you are completing a business survey, please contact us." +msgstr "Os ydych chi'n cwblhau arolwg busnes, cysylltwch Ãĸ ni." + +#: templates/errors/404.html:16 templates/errors/submission-failed.html:17 +msgid "If you started your survey using an access code, please contact us." +msgstr "Os gwnaethoch chi ddechrau eich arolwg gan ddefnyddio cod mynediad, cysylltwch Ãĸ ni." + #: templates/errors/500.html:3 msgid "An error has occurred" msgstr "Mae gwall wedi digwydd" @@ -949,65 +1151,53 @@ msgstr "Rhowch gynnig arall arni yn nes ymlaen." msgid "If you have started a survey, your answers have been saved." msgstr "Os ydych chi wedi dechrau arolwg, mae eich atebion wedi cael eu cadw." -#: templates/errors/500.html:9 +#: templates/errors/500.html:10 msgid "Contact us if you need to speak to someone about your survey." msgstr "Cysylltwch Ãĸ ni os oes angen i chi siarad Ãĸ rhywun am eich arolwg." -#: templates/errors/no-cookie.html:3 -msgid "Page is not available" -msgstr "Nid yw'r dudalen ar gael" - -#: templates/errors/no-cookie.html:6 -msgid "Sorry there is a problem" -msgstr "Mae'n ddrwg gennym, mae problem wedi codi" - -#: templates/errors/no-cookie.html:7 -msgid "To access this page you need to enter your 16-character access code." -msgstr "I fynd i'r dudalen hon, bydd angen i chi roi eich cod mynediad 16 nod." - -#: templates/errors/session-expired.html:3 -msgid "Session timed out" -msgstr "" - -#: templates/errors/session-expired.html:6 -msgid "Your session has timed out due to inactivity" -msgstr "Mae eich sesiwn wedi dod i ben gan nad oedd gweithgarwch" - -#: templates/errors/session-expired.html:7 -msgid "To help protect your information we have timed you out" -msgstr "Er mwyn helpu i ddiogelu eich gwybodaeth, rydyn ni wedi dod Ãĸ’ch sesiwn i ben" +#: templates/errors/500.html:13 +msgid "If you are completing a business survey and you need to speak to someone about your survey, please contact us." +msgstr "Os ydych chi'n cwblhau arolwg busnes a bod angen i chi siarad Ãĸ rhywun am eich arolwg, cysylltwch Ãĸ ni." -#: templates/errors/session-expired.html:8 -msgid "You will need to enter your 16-character access code again to continue your census." -msgstr "Bydd angen i chi roi eich cod mynediad 16 nod i barhau Ãĸ'ch cyfrifiad." +#: templates/errors/500.html:15 +msgid "If you started your survey using an access code and you need to speak to someone about your survey, please contact us." +msgstr "Os gwnaethoch chi ddechrau eich arolwg gan ddefnyddio cod mynediad a bod angen i chi siarad Ãĸ rhywun am eich arolwg, cysylltwch Ãĸ ni." -#: templates/errors/submission-complete.html:3 +#: templates/errors/previously-submitted.html:3 msgid "Submission Complete" -msgstr "" +msgstr "Cyflwyniad wedi'i Gwblhau" -#: templates/errors/submission-complete.html:6 +#: templates/errors/previously-submitted.html:6 msgid "This page is no longer available" msgstr "Nid yw'r dudalen hon ar gael mwyach" -#: templates/errors/submission-complete.html:7 -msgid "Your census has been submitted" -msgstr "Mae eich cyfrifiad wedi cael ei gyflwyno" +#: templates/errors/previously-submitted.html:7 +msgid "Your survey has been submitted" +msgstr "Mae eich arolwg wedi cael ei gyflwyno" + +#: templates/errors/previously-submitted.html:8 +msgid "Return to previous page" +msgstr "Dychwelyd i'r dudalen flaenorol" #: templates/errors/submission-failed.html:9 -msgid "You can try to submit your census again" -msgstr "Gallwch geisio cyflwyno eich cyfrifiad eto" +msgid "You can try to submit your survey again" +msgstr "Gallwch geisio cyflwyno eich arolwg eto" -#: templates/errors/submission-failed.html:10 +#: templates/errors/submission-failed.html:11 msgid "If this problem keeps happening, please contact us for help." msgstr "Os bydd y broblem hon yn parhau, cysylltwch Ãĸ ni i gael cymorth." +#: templates/errors/submission-failed.html:13 +msgid "If this problem keeps happening, please see the following help links." +msgstr "Os bydd y broblem hon yn parhau, edrychwch ar y dolenni cymorth canlynol." + #: templates/individual_response/confirmation-post.html:15 msgid "A letter has been sent to Individual Resident at {display_address}" msgstr "Mae llythyr wedi cael ei anfon at Breswylydd Unigol yn {display_address}" #: templates/individual_response/confirmation-post.html:17 msgid "The letter with an individual access code should arrive soon for them to complete their own census" -msgstr "Dylai’r llythyr Ãĸ chod mynediad unigol gyrraedd yn fuan er mwyn i’r unigolyn allu llenwi ei gyfrifiad ei hun" +msgstr "Dylai’r llythyr Ãĸ chod mynediad unigol gyrraedd yn fuan er mwyn i’r unigolyn allu cwblhau ei gyfrifiad ei hun" #: templates/individual_response/confirmation-text-message.html:15 msgid "We have sent a text to {mobile_number}" @@ -1015,11 +1205,11 @@ msgstr "Rydym wedi anfon neges destun i {mobile_number}" #: templates/individual_response/confirmation-text-message.html:17 msgid "The text message with an individual access code should arrive soon for them to complete their own census" -msgstr "Dylai’r neges destun Ãĸ chod mynediad unigol gyrraedd yn fuan er mwyn i’r unigolyn allu llenwi ei gyfrifiad ei hun" +msgstr "Dylai’r neges destun Ãĸ chod mynediad unigol gyrraedd yn fuan er mwyn i’r unigolyn allu cwblhau ei gyfrifiad ei hun" -#: templates/individual_response/confirmation-text-message.html:26 -msgid "The text will be sent from Census2021" -msgstr "Caiff y testun ei anfon gan Census2021" +#: templates/individual_response/confirmation-text-message.html:25 +msgid "The text will be sent from Census2021" +msgstr "Caiff y neges destun ei hanfon o Census2021" #: templates/individual_response/interstitial.html:8 msgid "If you can't answer questions for others in your household" @@ -1041,63 +1231,69 @@ msgstr "Gwneud cais am gyfrifiad ar wahÃĸn" msgid "To request a census in a different format or for further help, please contact us" msgstr "I wneud cais am gyfrifiad mewn fformat gwahanol neu am fwy o gymorth, cysylltwch Ãĸ ni" -#: templates/layouts/_base.html:19 +#: templates/layouts/_base.html:20 msgid "Previous" msgstr "Blaenorol" -#: templates/layouts/_base.html:62 +#: templates/layouts/_base.html:69 msgid "Tell us whether you accept cookies" msgstr "Dywedwch wrthym a ydych chi'n derbyn cwcis" -#: templates/layouts/_base.html:63 -msgid "We use cookies to collect information about how you use census.gov.uk. We use this information to make the website work as well as possible and improve our services." -msgstr "Rydym ni'n defnyddio cwcis i gasglu gwybodaeth am y ffordd rydych chi'n defnyddio cyfrifiad.gov.uk. Rydym ni'n defnyddio'r wybodaeth hon i sicrhau bod y wefan yn gweithio cystal Ãĸ phosibl ac i wella ein gwasanaethau." +#: templates/layouts/_base.html:70 +msgid "We use cookies to collect information about how you use {cookie_domain}. We use this information to make the website work as well as possible and improve our services." +msgstr "Rydym ni'n defnyddio cwcis i gasglu gwybodaeth am y ffordd rydych chi'n defnyddio {cookie_domain}. Rydym ni'n defnyddio'r wybodaeth hon i sicrhau bod y wefan yn gweithio cystal Ãĸ phosibl ac i wella ein gwasanaethau." -#: templates/layouts/_base.html:64 +#: templates/layouts/_base.html:71 msgid "You’ve accepted all cookies. You can change your cookie preferences at any time." -msgstr "Rydych chi wedi derbyn yr holl gwcis Gallwch chi newid eich dewisiadau o ran cwcis ar unrhyw adeg." +msgstr "Rydych chi wedi derbyn yr holl gwcis. Gallwch chi newid eich dewisiadau o ran cwcis ar unrhyw adeg." -#: templates/layouts/_base.html:65 +#: templates/layouts/_base.html:72 msgid "Accept all cookies" msgstr "Derbyn yr holl gwcis" -#: templates/layouts/_base.html:66 +#: templates/layouts/_base.html:73 msgid "Set cookie preferences" msgstr "Gosod dewisiadau o ran cwcis" -#: templates/layouts/_base.html:67 -#: templates/partials/introduction/preview.html:30 -#: templates/partials/summary/collapsible-summary.html:14 -msgid "Hide" -msgstr "Cuddio" - -#: templates/layouts/_base.html:115 +#: templates/layouts/_base.html:130 msgid "Skip to main content" -msgstr "Neidio i’r prif gynnwy" +msgstr "Neidio i’r prif gynnwys" -#: templates/layouts/_questionnaire.html:38 -msgid "Choose another section and return to this later" -msgstr "Dewiswch adran arall a dod yn ôl yn nes ymlaen" +#: templates/layouts/_base.html:152 +msgid "You will be signed out soon" +msgstr "Byddwch yn cael eich allgofnodi yn fuan" -#: templates/layouts/configs/_save-sign-out-button.html:5 -msgid "Save and sign out" -msgstr "Cadw ac allgofnodi" +#: templates/layouts/_base.html:153 +msgid "It appears you have been inactive for a while." +msgstr "Mae'n ymddangos eich bod wedi bod yn segur ers tro." -#: templates/layouts/configs/_save-sign-out-button.html:7 -msgid "Save and complete later" -msgstr "Cadw a chwblhau yn nes ymlaen" +#: templates/layouts/_base.html:154 +msgid "To protect your information, your progress will be saved and you will be signed out in" +msgstr "I ddiogelu eich gwybodaeth, bydd eich cynnydd yn cael ei gadw a byddwch yn cael eich allgofnodi" + +#: templates/layouts/_base.html:155 +msgid "You are being signed out" +msgstr "Rydych chi'n cael eich allgofnodi" -#: templates/layouts/configs/_save-sign-out-button.html:25 +#: templates/layouts/_base.html:156 +msgid "Continue survey" +msgstr "Parhau Ãĸ'r arolwg" + +#: templates/layouts/_questionnaire.html:39 +msgid "Choose another section and return to this later" +msgstr "Dewiswch adran arall a dod yn ôl yn nes ymlaen" + +#: templates/layouts/configs/_header.html:11 msgid "Exit" -msgstr "Cau" +msgstr "Ymadael" -#: templates/macros/helpers.html:29 +#: templates/macros/helpers.html:13 msgid "Interviewer note:" msgstr "Nodyn i’r cyfwelydd:" -#: templates/partials/answer-guidance.html:19 -#: templates/partials/definition.html:19 -#: templates/partials/individual-response-guidance.html:14 +#: templates/partials/answer-guidance.html:18 +#: templates/partials/definition.html:18 +#: templates/partials/individual-response-guidance.html:13 msgid "Hide this" msgstr "Cuddio hwn" @@ -1109,7 +1305,7 @@ msgstr "Cyfeiriad e-bost" msgid "This will not be stored and only used once to send your confirmation" msgstr "Ni fydd hwn yn cael ei storio a dim ond unwaith y bydd yn cael ei ddefnyddio er mwyn anfon cadarnhad atoch" -#: templates/partials/email-form.html:39 +#: templates/partials/email-form.html:38 msgid "Send confirmation" msgstr "Anfon cadarnhad" @@ -1119,17 +1315,17 @@ msgstr "Beth yw eich barn chi am y gwasanaeth hwn?" #: templates/partials/feedback-call-to-action.html:7 msgid "Your comments will help us make improvements" -msgstr "Bydd eich sylwadau yn ein helpu ni i wella’r gwasanaeth" +msgstr "Bydd eich sylwadau yn ein helpu ni i wneud gwelliannau" #: templates/partials/feedback-call-to-action.html:9 msgid "Give feedback" msgstr "Rhoi adborth" -#: templates/partials/individual-response-guidance.html:25 -msgid "You can share your household access code with the people you live with so they can complete their own sections." -msgstr "Gallwch rannu cod mynediad eich cartref Ãĸ’r bobl rydych chi’n byw gyda nhw fel y gallant gwblhau eu hadrannau eu hunain." +#: templates/partials/individual-response-guidance.html:24 +msgid "You can share your household access code with the people you live with so they can complete their own sections." +msgstr "Gallwch rannu cod mynediad eich cartref Ãĸ’r bobl rydych chi’n byw gyda nhw fel y gallant gwblhau eu hadrannau eu hunain." -#: templates/partials/individual-response-guidance.html:26 +#: templates/partials/individual-response-guidance.html:25 msgid "If this is not possible, there are other ways each person can complete their own census." msgstr "Os nad yw hyn yn bosibl, mae ffyrdd eraill y gall pob person lenwi ei gyfrifiad ei hun." @@ -1141,115 +1337,120 @@ msgstr "Dyma'r cwestiwn a gafodd ei weld ddiwethaf yn yr adran hon" msgid "You can also go back to the start of the section" msgstr "Gallwch chi hefyd fynd yn ôl i ddechrau'r adran" -#: templates/partials/question.html:62 +#: templates/partials/preview-question.html:35 +#: templates/partials/question.html:68 +msgid "Or" +msgstr "Neu" + +#: templates/partials/preview-question.html:56 +msgid "{max_characters} characters can be added." +msgstr "Mae modd ychwanegu {max_characters} nod." + +#: templates/partials/question.html:59 msgid "Selecting this will clear your answer" msgstr "Bydd dewis hwn yn clirio eich ateb" -#: templates/partials/question.html:63 +#: templates/partials/question.html:60 msgid "cleared" msgstr "wedi'i glirio" -#: templates/partials/question.html:66 +#: templates/partials/question.html:63 msgid "Selecting this will deselect any selected options" msgstr "Bydd dewis hwn yn dad-ddewis unrhyw opsiynau sydd wedi'u dewis" -#: templates/partials/question.html:67 templates/partials/question.html:75 +#: templates/partials/question.html:64 templates/partials/question.html:72 msgid "deselected" msgstr "dad-ddewiswyd" -#: templates/partials/question.html:71 -msgid "Or" -msgstr "Neu" - -#: templates/partials/answers/address.html:10 +#: templates/partials/answers/address.html:9 msgid "Address line 1" msgstr "Llinell cyfeiriad 1" -#: templates/partials/answers/address.html:15 +#: templates/partials/answers/address.html:14 msgid "Address line 2" msgstr "Llinell cyfeiriad 2" -#: templates/partials/answers/address.html:19 +#: templates/partials/answers/address.html:18 msgid "Town or city" msgstr "Tref neu ddinas" -#: templates/partials/answers/address.html:23 +#: templates/partials/answers/address.html:22 msgid "Postcode" msgstr "Cod post" -#: templates/partials/answers/address.html:29 +#: templates/partials/answers/address.html:33 +msgid "Enter address or postcode and select from results" +msgstr "Rhowch y cyfeiriad neu'r cod post a dewiswch o'r canlyniadau" + +#: templates/partials/answers/address.html:35 msgid "Search for an address" msgstr "Chwilio am gyfeiriad" -#: templates/partials/answers/address.html:30 +#: templates/partials/answers/address.html:36 msgid "Manually enter address" msgstr "Nodwch y cyfeiriad Ãĸ llaw" -#: templates/partials/answers/address.html:36 -msgid "Enter address or postcode and select from results" -msgstr "Nodwch y cyfeiriad neu'r cod post a dewiswch o'r canlyniadau" - -#: templates/partials/answers/address.html:42 +#: templates/partials/answers/address.html:41 msgid "Use up and down keys to navigate suggestions once you’ve typed more than two characters. Use the enter key to select a suggestion. Touch device users, explore by touch or with swipe gestures." msgstr "Defnyddiwch y bysellau i fyny ac i lawr i edrych drwy'r awgrymiadau unwaith y byddwch chi wedi teipio mwy na dau nod. Defnyddiwch y fysell ‘enter’ i ddewis awgrym. Gall defnyddwyr dyfeisiau cyffwrdd symud o gwmpas drwy gyffwrdd neu sweipio." -#: templates/partials/answers/address.html:43 +#: templates/partials/answers/address.html:42 msgid "You have selected" msgstr "Rydych chi wedi dewis" -#: templates/partials/answers/address.html:44 +#: templates/partials/answers/address.html:43 msgid "Enter 3 or more characters for suggestions." msgstr "Rhowch 3 nod neu fwy i gael awgrymiadau." -#: templates/partials/answers/address.html:45 +#: templates/partials/answers/address.html:44 msgid "There is one suggestion available." msgstr "Mae un awgrym ar gael." -#: templates/partials/answers/address.html:46 +#: templates/partials/answers/address.html:45 msgid "There are {n} suggestions available." -msgstr "Mae {n} o awgrymiadau ar gael." +msgstr "Mae {n} awgrym ar gael." -#: templates/partials/answers/address.html:47 +#: templates/partials/answers/address.html:46 msgid "Results have been limited to 10 suggestions. Type more characters to improve your search" msgstr "Mae'r canlyniadau wedi cael eu cyfyngu i 10 awgrym. Teipiwch fwy o nodau i wella eich chwiliad" -#: templates/partials/answers/address.html:48 +#: templates/partials/answers/address.html:47 msgid "There are {n} for {x}" msgstr "Mae {n} ar gyfer {x}" -#: templates/partials/answers/address.html:49 +#: templates/partials/answers/address.html:48 msgid "{n} addresses" msgstr "{n} o gyfeiriadau" -#: templates/partials/answers/address.html:50 +#: templates/partials/answers/address.html:49 msgid "Enter more of the address to improve results" msgstr "Rhowch fwy o'r cyfeiriad i wella canlyniadau" -#: templates/partials/answers/address.html:51 +#: templates/partials/answers/address.html:50 msgid "Select an address" msgstr "Dewiswch gyfeiriad" -#: templates/partials/answers/address.html:52 +#: templates/partials/answers/address.html:51 msgid "No results found. Try entering a different part of the address" msgstr "Heb ddod o hyd i unrhyw ganlyniadau. Ceisiwch ddefnyddio rhan wahanol o'r cyfeiriad" -#: templates/partials/answers/address.html:53 +#: templates/partials/answers/address.html:52 msgid "{n} results found. Enter more of the address to improve results" -msgstr "Wedi dod o hyd i {n} o ganlyniadau. Rhowch fwy o'r cyfeiriad i wella'r canlyniadau" +msgstr "Wedi dod o hyd i {n} ganlyniad. Rhowch fwy o'r cyfeiriad i wella'r canlyniadau" -#: templates/partials/answers/address.html:54 +#: templates/partials/answers/address.html:53 msgid "Enter more of the address to get results" msgstr "Rhowch fwy o'r cyfeiriad i gael canlyniadau" -#: templates/partials/answers/address.html:58 +#: templates/partials/answers/address.html:57 msgid "Select or manually enter an address" msgstr "Dewiswch gyfeiriad neu nodwch Ãĸ llaw" -#: templates/partials/answers/address.html:59 +#: templates/partials/answers/address.html:58 msgid "Sorry, there was a problem loading addresses" msgstr "Mae'n ddrwg gennym, roedd problem wrth lwytho cyfeiriadau" -#: templates/partials/answers/address.html:60 +#: templates/partials/answers/address.html:59 msgid "Enter address manually" msgstr "Teipiwch y cyfeiriad eich hun" @@ -1269,92 +1470,64 @@ msgstr "Mis" msgid "Year" msgstr "Blwyddyn" -#: templates/partials/answers/radio.html:14 +#: templates/partials/answers/radio.html:15 msgid "Clear selection" msgstr "Clirio’r dewis" -#: templates/partials/answers/textarea.html:16 -#: templates/partials/answers/textfield.html:28 +#: templates/partials/answers/textarea.html:20 +#: templates/partials/answers/textfield.html:45 msgid "You have {x} character remaining" msgstr "Mae gennych chi {x} nod ar ôl" -#: templates/partials/answers/textarea.html:17 -#: templates/partials/answers/textfield.html:29 +#: templates/partials/answers/textarea.html:21 +#: templates/partials/answers/textfield.html:46 msgid "You have {x} characters remaining" -msgstr "Mae gennych chi {x} nod ar ôl" - -#: templates/partials/answers/textfield.html:26 -msgid "{x} character too many" -msgstr "{x} o nodau yn ormod" - -#: templates/partials/answers/textfield.html:27 -msgid "{x} characters too many" -msgstr "{x} nod yn ormod" +msgstr "Mae gennych chi {x} o nodau ar ôl" -#: templates/partials/answers/textfield.html:35 +#: templates/partials/answers/textfield.html:25 msgid "Use up and down keys to navigate suggestions once you've typed more than two characters. Use the enter key to select a suggestion. Touch device users, explore by touch or with swipe gestures." msgstr "Defnyddiwch y bysellau i fyny ac i lawr i edrych drwy'r awgrymiadau unwaith y byddwch chi wedi teipio mwy na dau nod. Defnyddiwch y fysell ‘enter’ i ddewis awgrym. Gall defnyddwyr dyfeisiau cyffwrdd symud o gwmpas drwy gyffwrdd neu sweipio." -#: templates/partials/answers/textfield.html:36 +#: templates/partials/answers/textfield.html:26 msgid "Continue entering to improve suggestions" msgstr "Parhewch i roi nodau er mwyn gwella'r awgrymiadau" -#: templates/partials/answers/textfield.html:37 +#: templates/partials/answers/textfield.html:27 msgid "Suggestions" msgstr "Awgrymiadau" -#: templates/partials/answers/textfield.html:38 +#: templates/partials/answers/textfield.html:28 msgid "No results found" msgstr "Heb ganfod unrhyw ganlyniadau" -#: templates/partials/answers/textfield.html:39 +#: templates/partials/answers/textfield.html:29 msgid "Continue entering to get suggestions" msgstr "Parhewch i roi nodau er mwyn cael awgrymiadau" -#: templates/partials/introduction/preview.html:29 -#: templates/partials/summary/collapsible-summary.html:13 -msgid "Show" -msgstr "Dangos" - -#: templates/partials/introduction/preview.html:48 -#: templates/partials/summary/collapsible-summary.html:57 -msgid "Show all" -msgstr "Dangos popeth" +#: templates/partials/answers/textfield.html:43 +msgid "{x} character too many" +msgstr "{x} nod yn ormod" -#: templates/partials/introduction/preview.html:49 -#: templates/partials/summary/collapsible-summary.html:58 -msgid "Hide all" -msgstr "Cuddio popeth" +#: templates/partials/answers/textfield.html:44 +msgid "{x} characters too many" +msgstr "{x} o nodau yn ormod" #: templates/partials/introduction/start-survey.html:5 msgid "Start survey" -msgstr "Cychwyn yr arolwg" +msgstr "Dechrau'r arolwg" -#: templates/partials/summary/collapsible-summary.html:35 -#: templates/partials/summary/summary.html:18 -msgid "No answer provided" -msgstr "Ni roddwyd ateb" - -#: templates/partials/summary/collapsible-summary.html:36 -#: templates/partials/summary/list-summary.html:8 -#: templates/partials/summary/summary.html:19 +#: templates/partials/summary/collapsible-summary.html:37 +#: templates/partials/summary/list-summary.html:7 +#: templates/partials/summary/summary.html:21 msgid "Change" msgstr "Newid" -#: templates/partials/summary/collapsible-summary.html:37 -#: templates/partials/summary/summary.html:20 +#: templates/partials/summary/collapsible-summary.html:38 +#: templates/partials/summary/summary.html:22 msgid "Change your answer for:" msgstr "Newidiwch eich ateb ar gyfer:" -#: templates/partials/summary/list-summary.html:9 +#: templates/partials/summary/list-summary.html:8 msgid "Change details for {item_name}" msgstr "Newid manylion ar gyfer {item_name}" -#: templates/partials/summary/list-summary.html:10 -msgid "Remove" -msgstr "Dileu" - -#: templates/partials/summary/list-summary.html:11 -msgid "Remove {item_name}" -msgstr "Tynnu {item_name}" - diff --git a/app/translations/eo/LC_MESSAGES/messages.po b/app/translations/eo/LC_MESSAGES/messages.po index 6efb67342d..9ceb4f0fc4 100644 --- a/app/translations/eo/LC_MESSAGES/messages.po +++ b/app/translations/eo/LC_MESSAGES/messages.po @@ -540,8 +540,8 @@ msgid "Cannot answer questions for others in your household" msgstr "Cannae mak repone fur speirins anent ithers in yer hoosehaud" #: app/views/handlers/individual_response.py:311 -msgid "How would you like {person_name} to receive a separate census?" -msgstr "Hoo wud ye lake {person_name} tae get anither heid-coont?" +msgid "How would you like {person_name} to receive a separate census?" +msgstr "Hoo wud ye lake {person_name} tae get anither heid-coont?" #: app/views/handlers/individual_response.py:334 msgid "Text message" @@ -576,8 +576,8 @@ msgid "Send individual access code" msgstr "Sen sing'l ingang code" #: app/views/handlers/individual_response.py:437 -msgid "How would you like to answer {person_name_possessive} questions?" -msgstr "Hoo wud ye lake tae mak repone fur {person_name_possessive} speirins?" +msgid "How would you like to answer {person_name_possessive} questions?" +msgstr "Hoo wud ye lake tae mak repone fur {person_name_possessive} speirins?" #: app/views/handlers/individual_response.py:452 msgid "I would like to request a separate census for them to complete" @@ -633,8 +633,8 @@ msgid "Who do you need to request a separate census for?" msgstr "Wha's tha bodie ye need tae ax fur anither heid-coont fur?" #: app/views/handlers/individual_response.py:759 -msgid "What is {person_name_possessive} mobile number?" -msgstr "Whut bes {person_name_possessive} mobil phone nummer?" +msgid "What is {person_name_possessive} mobile number?" +msgstr "Whut bes {person_name_possessive} mobil phone nummer?" #: app/views/handlers/individual_response.py:771 msgid "UK mobile number" @@ -721,8 +721,8 @@ msgid "A confirmation email has been sent to {email}" msgstr "An homologation email haes bin sent til {email}" #: templates/confirmation-email-sent.html:25 -msgid "The email will be sent from census.2021@notifications.service.gov.uk" -msgstr "Tha email wull be sent frae census.2021@notifications.service.gov.uk" +msgid "The email will be sent from census.2021@notifications.service.gov.uk" +msgstr "Tha email wull be sent frae census.2021@notifications.service.gov.uk" #: templates/confirmation-email-sent.html:29 msgid "Didn't receive an email?" @@ -987,8 +987,8 @@ msgid "The text message with an individual access code should arrive soon for th msgstr "Tha text message wi a sing'l ingang code shud land shane eneuch fur thaim tae fill oot thair ain heid-coont" #: templates/individual_response/confirmation-text-message.html:26 -msgid "The text will be sent from Census2021" -msgstr "Tha text wull be sent frae Heid-coont 20an21" +msgid "The text will be sent from Census2021" +msgstr "Tha text wull be sent frae Heid-coont 20an21" #: templates/individual_response/interstitial.html:8 msgid "If you can't answer questions for others in your household" @@ -1095,8 +1095,8 @@ msgid "Give feedback" msgstr "Gie feedbak" #: templates/partials/individual-response-guidance.html:25 -msgid "You can share your household access code with the people you live with so they can complete their own sections." -msgstr "Ye can gie oot yer hoosehaud ingang code wi tha fowk wha leeve wi ye sae they can fill oot thair ain lay- oots." +msgid "You can share your household access code with the people you live with so they can complete their own sections." +msgstr "Ye can gie oot yer hoosehaud ingang code wi tha fowk wha leeve wi ye sae they can fill oot thair ain lay- oots." #: templates/partials/individual-response-guidance.html:26 msgid "If this is not possible, there are other ways each person can complete their own census." diff --git a/app/translations/ga/LC_MESSAGES/messages.po b/app/translations/ga/LC_MESSAGES/messages.po index 165acc553f..4e72dbfa5b 100644 --- a/app/translations/ga/LC_MESSAGES/messages.po +++ b/app/translations/ga/LC_MESSAGES/messages.po @@ -558,8 +558,8 @@ msgid "Cannot answer questions for others in your household" msgstr "Ní thig liom ceisteanna a fhreagairt do dhaoine eile i do theaghlach" #: app/views/handlers/individual_response.py:311 -msgid "How would you like {person_name} to receive a separate census?" -msgstr "CÊn dÃŗigh ar ar mhaith leat go bhfaigheadh {person_name} daonÃĄireamh ar leith?" +msgid "How would you like {person_name} to receive a separate census?" +msgstr "CÊn dÃŗigh ar ar mhaith leat go bhfaigheadh {person_name} daonÃĄireamh ar leith?" #: app/views/handlers/individual_response.py:334 msgid "Text message" @@ -594,8 +594,8 @@ msgid "Send individual access code" msgstr "Cuir cÃŗd rochtana ar leith" #: app/views/handlers/individual_response.py:437 -msgid "How would you like to answer {person_name_possessive} questions?" -msgstr "CÊn dÃŗigh ar mhaith leat ceisteanna {person_name_possessive} a fhreagairt?" +msgid "How would you like to answer {person_name_possessive} questions?" +msgstr "CÊn dÃŗigh ar mhaith leat ceisteanna {person_name_possessive} a fhreagairt?" #: app/views/handlers/individual_response.py:452 msgid "I would like to request a separate census for them to complete" @@ -651,8 +651,8 @@ msgid "Who do you need to request a separate census for?" msgstr "CÊ dÃŗ a bhfuil tÃē ag iarraidh daonÃĄireamh ar leith>?" #: app/views/handlers/individual_response.py:759 -msgid "What is {person_name_possessive} mobile number?" -msgstr "CÊn uimhir ghuthÃĄn pÃŗca atÃĄ ag {person_name_possessive}?" +msgid "What is {person_name_possessive} mobile number?" +msgstr "CÊn uimhir ghuthÃĄn pÃŗca atÃĄ ag {person_name_possessive}?" #: app/views/handlers/individual_response.py:771 msgid "UK mobile number" @@ -742,8 +742,8 @@ msgid "A confirmation email has been sent to {email}" msgstr "Seoladh r-phost dearbhaithe chuig {email}" #: templates/confirmation-email-sent.html:25 -msgid "The email will be sent from census.2021@notifications.service.gov.uk" -msgstr "Seolfar an r-phost Ãŗ census.2021@notifications.service.gov.uk" +msgid "The email will be sent from census.2021@notifications.service.gov.uk" +msgstr "Seolfar an r-phost Ãŗ census.2021@notifications.service.gov.uk" #: templates/confirmation-email-sent.html:29 msgid "Didn't receive an email?" @@ -1011,8 +1011,8 @@ msgid "The text message with an individual access code should arrive soon for th msgstr "Ba chÃŗir go dtiocfadh an tÊacstheachtaireacht ina bhfuil cÃŗd rochtana duine aonair dÃŗibh gan mhoill le go dtig leo a ndaonÃĄireamh fÊin a chomhlÃĄnÃē" #: templates/individual_response/confirmation-text-message.html:26 -msgid "The text will be sent from Census2021" -msgstr "Seolfar an tÊacs Ãŗ Census2021" +msgid "The text will be sent from Census2021" +msgstr "Seolfar an tÊacs Ãŗ Census2021" #: templates/individual_response/interstitial.html:8 msgid "If you can't answer questions for others in your household" @@ -1119,8 +1119,8 @@ msgid "Give feedback" msgstr "Tabhair aiseolas" #: templates/partials/individual-response-guidance.html:25 -msgid "You can share your household access code with the people you live with so they can complete their own sections." -msgstr "Tig leat do chÃŗd rochtana teaghlaigh a roinnt leis na daoine a gcÃŗnaíonn tÃē leo sa dÃŗigh go dtig leo a míreanna fÊin a chomhlÃĄnÃē." +msgid "You can share your household access code with the people you live with so they can complete their own sections." +msgstr "Tig leat do chÃŗd rochtana teaghlaigh a roinnt leis na daoine a gcÃŗnaíonn tÃē leo sa dÃŗigh go dtig leo a míreanna fÊin a chomhlÃĄnÃē." #: templates/partials/individual-response-guidance.html:26 msgid "If this is not possible, there are other ways each person can complete their own census." diff --git a/app/translations/messages.pot b/app/translations/messages.pot index 944d57dd47..1f8a3639cd 100644 --- a/app/translations/messages.pot +++ b/app/translations/messages.pot @@ -1,46 +1,60 @@ # Translations template for PROJECT. -# Copyright (C) 2022 ORGANIZATION +# Copyright (C) 2025 ORGANIZATION # This file is distributed under the same license as the PROJECT project. -# FIRST AUTHOR , 2022. +# FIRST AUTHOR , 2025. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2022-03-29 09:47+0100\n" +"POT-Creation-Date: 2025-01-10 12:58+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.9.1\n" +"Generated-By: Babel 2.14.0\n" -#: app/forms/validators.py:372 app/jinja_filters.py:94 +#: app/forms/validators.py:389 app/jinja_filters.py:111 #, python-format msgid "%(num)s year" msgid_plural "%(num)s years" msgstr[0] "" msgstr[1] "" -#: app/forms/validators.py:376 app/jinja_filters.py:102 +#: app/forms/validators.py:393 app/jinja_filters.py:119 #, python-format msgid "%(num)s month" msgid_plural "%(num)s months" msgstr[0] "" msgstr[1] "" -#: app/jinja_filters.py:142 +#: app/jinja_filters.py:161 #, python-format msgid "%(date)s at %(time)s" msgstr "" -#: app/jinja_filters.py:155 +#: app/jinja_filters.py:175 #, python-format msgid "%(from_date)s to %(to_date)s" msgstr "" +#: app/jinja_filters.py:441 +msgid "Change answer for {item_name}: {question_title_or_answer_label}" +msgstr "" + +#: app/jinja_filters.py:445 +msgid "Change your answer for: {question_title_or_answer_label}" +msgstr "" + +#: app/jinja_filters.py:765 +#: templates/partials/summary/collapsible-summary.html:27 +#: templates/partials/summary/summary.html:24 +msgid "No answer provided" +msgstr "" + #: app/forms/error_messages.py:11 app/forms/error_messages.py:12 #: app/forms/error_messages.py:13 app/forms/error_messages.py:14 msgid "Enter an answer" @@ -52,7 +66,7 @@ msgid "Select an answer to ‘%(question_title)s’leave this page or close your " "browser if using a shared device" msgstr "" -#: app/questionnaire/placeholder_transforms.py:144 +#: app/questionnaire/placeholder_transforms.py:203 msgid "{number_of_years} year" msgid_plural "{number_of_years} years" msgstr[0] "" msgstr[1] "" -#: app/questionnaire/placeholder_transforms.py:150 +#: app/questionnaire/placeholder_transforms.py:209 msgid "{number_of_months} month" msgid_plural "{number_of_months} months" msgstr[0] "" msgstr[1] "" -#: app/questionnaire/placeholder_transforms.py:155 +#: app/questionnaire/placeholder_transforms.py:214 msgid "{number_of_days} day" msgid_plural "{number_of_days} days" msgstr[0] "" msgstr[1] "" -#: app/routes/errors.py:118 +#: app/routes/errors.py:159 msgid "You have reached the maximum number of individual access codes" msgstr "" -#: app/routes/errors.py:121 +#: app/routes/errors.py:162 msgid "" "If you need more individual access codes, please contact us." msgstr "" -#: app/routes/errors.py:137 +#: app/routes/errors.py:180 msgid "You have reached the maximum number of times for submitting feedback" msgstr "" -#: app/routes/errors.py:140 +#: app/routes/errors.py:183 msgid "" "If you need to give more feedback, please contact us." msgstr "" -#: app/routes/errors.py:172 +#: app/routes/errors.py:233 msgid "Sorry, there was a problem sending the access code" msgstr "" -#: app/routes/errors.py:178 +#: app/routes/errors.py:240 msgid "You can try to request a new access code again." msgstr "" -#: app/routes/errors.py:181 app/routes/errors.py:204 app/routes/errors.py:226 +#: app/routes/errors.py:243 app/routes/errors.py:268 app/routes/errors.py:290 msgid "" "If this problem keeps happening, please contact us for help." msgstr "" -#: app/routes/errors.py:200 +#: app/routes/errors.py:264 msgid "Sorry, there was a problem sending the confirmation email" msgstr "" -#: app/routes/errors.py:201 +#: app/routes/errors.py:265 msgid "You can try to send the email again." msgstr "" -#: app/routes/errors.py:222 templates/errors/403.html:3 -#: templates/errors/403.html:6 templates/errors/submission-failed.html:5 -#: templates/errors/submission-failed.html:8 +#: app/routes/errors.py:286 templates/errors/403.html:3 +#: templates/errors/403.html:9 templates/errors/submission-failed.html:4 +#: templates/errors/submission-failed.html:11 msgid "Sorry, there is a problem" msgstr "" -#: app/routes/errors.py:223 +#: app/routes/errors.py:287 msgid "You can try to submit your feedback again." msgstr "" -#: app/routes/individual_response.py:177 +#: app/routes/individual_response.py:202 msgid "An individual access code has been sent by post" msgstr "" -#: app/routes/individual_response.py:272 +#: app/routes/individual_response.py:308 msgid "An individual access code has been sent by text" msgstr "" -#: app/survey_config/business_config.py:33 -msgid "What we do" -msgstr "" - -#: app/survey_config/business_config.py:34 -#: app/survey_config/census_config.py:30 app/survey_config/census_config.py:80 -#: app/survey_config/census_config.py:141 -msgid "Contact us" -msgstr "" - -#: app/survey_config/business_config.py:36 -msgid "Accessibility" -msgstr "" - -#: app/survey_config/business_config.py:41 -#: app/survey_config/census_config.py:44 app/survey_config/census_config.py:95 -#: app/survey_config/census_config.py:148 -msgid "Cookies" -msgstr "" - -#: app/survey_config/business_config.py:43 -#: app/survey_config/census_config.py:50 app/survey_config/census_config.py:101 -#: app/survey_config/census_config.py:154 -msgid "Privacy and data protection" +#: app/survey_config/business_config.py:60 +msgid "Help" msgstr "" -#: app/survey_config/business_config.py:54 +#: app/survey_config/business_config.py:74 msgid "My account" msgstr "" -#: app/survey_config/business_config.py:59 +#: app/survey_config/business_config.py:79 msgid "Sign out" msgstr "" -#: app/survey_config/business_config.py:78 -msgid "Northern Ireland Department of Finance logo" -msgstr "" - -#: app/survey_config/census_config.py:20 app/survey_config/census_config.py:63 -msgid "Census 2021" -msgstr "" - -#: app/survey_config/census_config.py:27 app/survey_config/census_config.py:77 -#: app/survey_config/census_config.py:138 -msgid "Help" -msgstr "" - -#: app/survey_config/census_config.py:32 app/survey_config/census_config.py:82 -msgid "Languages" -msgstr "" - -#: app/survey_config/census_config.py:36 app/survey_config/census_config.py:86 -msgid "BSL and audio videos" +#: app/survey_config/business_config.py:89 +#: app/survey_config/social_survey_config.py:39 +msgid "What we do" msgstr "" -#: app/survey_config/census_config.py:46 app/survey_config/census_config.py:97 -#: app/survey_config/census_config.py:150 -msgid "Accessibility statement" +#: app/survey_config/business_config.py:93 +#: app/survey_config/social_survey_config.py:43 +msgid "Contact us" msgstr "" -#: app/survey_config/census_config.py:54 app/survey_config/census_config.py:105 -#: app/survey_config/census_config.py:158 -msgid "Terms and conditions" +#: app/survey_config/business_config.py:98 +#: app/survey_config/social_survey_config.py:48 +msgid "Accessibility" msgstr "" -#: app/survey_config/census_config.py:64 -msgid "Save and complete later" +#: app/survey_config/business_config.py:108 +#: app/survey_config/social_survey_config.py:58 +msgid "Cookies" msgstr "" -#: app/survey_config/census_config.py:124 -msgid "Northern Ireland Statistics and Research Agency logo" +#: app/survey_config/business_config.py:110 +#: app/survey_config/social_survey_config.py:60 +msgid "Privacy and data protection" msgstr "" -#: app/survey_config/census_config.py:129 -msgid "Crown copyright and database rights 2021 NIMA MOU577.501." +#: app/survey_config/survey_config.py:17 +msgid "Crown copyright and database rights 2020 OS 100019153." msgstr "" -#: app/survey_config/census_config.py:132 app/survey_config/survey_config.py:21 +#: app/survey_config/survey_config.py:20 msgid "Use of address data is subject to the terms and conditions." msgstr "" -#: app/survey_config/survey_config.py:15 -msgid "Office for National Statistics logo" -msgstr "" - -#: app/survey_config/survey_config.py:18 -msgid "Crown copyright and database rights 2020 OS 100019153." -msgstr "" - -#: app/survey_config/survey_config.py:43 +#: app/survey_config/survey_config.py:36 msgid "Save and exit survey" msgstr "" -#: app/views/contexts/hub_context.py:14 +#: app/views/contexts/hub_context.py:18 msgid "Completed" msgstr "" -#: app/views/contexts/hub_context.py:16 +#: app/views/contexts/hub_context.py:20 msgid "View answers" msgstr "" -#: app/views/contexts/hub_context.py:17 -msgid "View answers for {section_name}" +#: app/views/contexts/hub_context.py:21 +msgid "View answers: {section_name}" msgstr "" -#: app/views/contexts/hub_context.py:21 +#: app/views/contexts/hub_context.py:25 msgid "Partially completed" msgstr "" -#: app/views/contexts/hub_context.py:23 +#: app/views/contexts/hub_context.py:27 msgid "Continue with section" msgstr "" -#: app/views/contexts/hub_context.py:24 -msgid "Continue with {section_name} section" +#: app/views/contexts/hub_context.py:28 +msgid "Continue with section: {section_name}" msgstr "" -#: app/views/contexts/hub_context.py:28 +#: app/views/contexts/hub_context.py:32 msgid "Not started" msgstr "" -#: app/views/contexts/hub_context.py:30 +#: app/views/contexts/hub_context.py:34 msgid "Start section" msgstr "" -#: app/views/contexts/hub_context.py:31 -msgid "Start {section_name} section" +#: app/views/contexts/hub_context.py:35 +msgid "Start section: {section_name}" msgstr "" -#: app/views/contexts/hub_context.py:35 +#: app/views/contexts/hub_context.py:39 msgid "Separate census requested" msgstr "" -#: app/views/contexts/hub_context.py:37 app/views/contexts/hub_context.py:38 +#: app/views/contexts/hub_context.py:41 app/views/contexts/hub_context.py:42 msgid "Change or resend" msgstr "" -#: app/views/contexts/hub_context.py:48 app/views/contexts/hub_context.py:49 +#: app/views/contexts/hub_context.py:54 app/views/contexts/hub_context.py:55 msgid "Submit survey" msgstr "" -#: app/views/contexts/hub_context.py:53 +#: app/views/contexts/hub_context.py:59 msgid "You must submit this survey to complete it" msgstr "" -#: app/views/contexts/hub_context.py:60 +#: app/views/contexts/hub_context.py:66 msgid "Choose another section to complete" msgstr "" -#: app/views/contexts/hub_context.py:61 templates/confirm-email.html:25 -#: templates/individual_response/confirmation-post.html:21 -#: templates/individual_response/confirmation-text-message.html:31 -#: templates/individual_response/question.html:5 templates/interstitial.html:8 -#: templates/sectionsummary.html:10 templates/sectionsummary.html:46 +#: app/views/contexts/hub_context.py:67 templates/confirm-email.html:28 +#: templates/individual_response/confirmation-post.html:26 +#: templates/individual_response/confirmation-text-message.html:39 +#: templates/individual_response/question.html:5 templates/interstitial.html:7 +#: templates/listcollectorcontent.html:8 templates/sectionsummary.html:11 +#: templates/sectionsummary.html:51 msgid "Continue" msgstr "" -#: app/views/contexts/list_context.py:104 +#: app/views/contexts/list_context.py:125 msgid " (You)" msgstr "" -#: app/views/contexts/submission_metadata_context.py:12 +#: app/views/contexts/preview_context.py:51 +msgid "Preview survey questions" +msgstr "" + +#: app/views/contexts/submission_metadata_context.py:14 msgid "Submitted on:" msgstr "" -#: app/views/contexts/submission_metadata_context.py:15 +#: app/views/contexts/submission_metadata_context.py:17 msgid "{date} at {time}" msgstr "" -#: app/views/contexts/submission_metadata_context.py:23 +#: app/views/contexts/submission_metadata_context.py:26 msgid "Submission reference:" msgstr "" @@ -476,303 +463,254 @@ msgstr "" msgid "Please submit this survey to complete it" msgstr "" -#: app/views/contexts/thank_you_context.py:27 -msgid "Your answers have been submitted." -msgstr "" - -#: app/views/contexts/thank_you_context.py:29 +#: app/views/contexts/thank_you_context.py:30 msgid "" "Your answers have been submitted for {company_name} " "({trading_name})" msgstr "" -#: app/views/contexts/thank_you_context.py:33 +#: app/views/contexts/thank_you_context.py:37 msgid "Your answers have been submitted for {company_name}" msgstr "" -#: app/views/contexts/view_submitted_response_context.py:28 -msgid "Answers submitted." +#: app/views/contexts/thank_you_context.py:41 +msgid "Your answers have been submitted." msgstr "" -#: app/views/contexts/view_submitted_response_context.py:30 +#: app/views/contexts/view_submitted_response_context.py:33 msgid "Answers submitted for {ru_name} ({trad_as})" msgstr "" -#: app/views/contexts/view_submitted_response_context.py:34 +#: app/views/contexts/view_submitted_response_context.py:37 msgid "Answers submitted for {ru_name}" msgstr "" -#: app/views/handlers/confirm_email.py:37 +#: app/views/contexts/view_submitted_response_context.py:41 +msgid "Answers submitted." +msgstr "" + +#: app/views/contexts/preview/preview_question.py:31 +msgid "You can answer with the following options:" +msgstr "" + +#: app/views/contexts/preview/preview_question.py:35 +msgid "You can answer with one of the following options:" +msgstr "" + +#: app/views/handlers/confirm_email.py:44 msgid "Yes, send the confirmation email" msgstr "" -#: app/views/handlers/confirm_email.py:68 +#: app/views/handlers/confirm_email.py:74 msgid "Confirm your email address" msgstr "" -#: app/views/handlers/confirm_email.py:88 +#: app/views/handlers/confirm_email.py:90 msgid "Is this email address correct?" msgstr "" -#: app/views/handlers/confirm_email.py:101 -#: app/views/handlers/confirm_email.py:102 -#: app/views/handlers/individual_response.py:877 +#: app/views/handlers/confirm_email.py:103 +#: app/views/handlers/confirm_email.py:104 +#: app/views/handlers/individual_response.py:910 msgid "No, I need to change it" msgstr "" #: app/views/handlers/confirm_email.py:128 -#: app/views/handlers/confirmation_email.py:68 -#: app/views/handlers/feedback.py:79 app/views/handlers/question.py:160 +#: app/views/handlers/confirmation_email.py:65 +#: app/views/handlers/feedback.py:73 app/views/handlers/question.py:173 msgid "Error: {page_title}" msgstr "" -#: app/views/handlers/confirmation_email.py:46 +#: app/views/handlers/confirmation_email.py:45 msgid "Confirmation email" msgstr "" -#: app/views/handlers/feedback.py:40 +#: app/views/handlers/feedback.py:39 msgid "Feedback" msgstr "" -#: app/views/handlers/feedback.py:123 +#: app/views/handlers/feedback.py:122 msgid "Give feedback about this service" msgstr "" -#: app/views/handlers/feedback.py:129 app/views/handlers/feedback.py:153 +#: app/views/handlers/feedback.py:128 app/views/handlers/feedback.py:152 msgid "Select what your feedback is about" msgstr "" -#: app/views/handlers/feedback.py:132 app/views/handlers/feedback.py:133 +#: app/views/handlers/feedback.py:131 app/views/handlers/feedback.py:132 msgid "The survey questions" msgstr "" -#: app/views/handlers/feedback.py:134 +#: app/views/handlers/feedback.py:133 msgid "For example, questions not clear, answer options not relevant" msgstr "" -#: app/views/handlers/feedback.py:139 app/views/handlers/feedback.py:140 +#: app/views/handlers/feedback.py:138 app/views/handlers/feedback.py:139 msgid "Page design and structure" msgstr "" -#: app/views/handlers/feedback.py:143 app/views/handlers/feedback.py:146 +#: app/views/handlers/feedback.py:142 app/views/handlers/feedback.py:145 msgid "General feedback about this service" msgstr "" -#: app/views/handlers/feedback.py:161 app/views/handlers/feedback.py:171 +#: app/views/handlers/feedback.py:160 app/views/handlers/feedback.py:170 msgid "Enter your feedback" msgstr "" -#: app/views/handlers/feedback.py:162 +#: app/views/handlers/feedback.py:161 msgid "Do not include confidential information, such as your contact details" msgstr "" -#: app/views/handlers/individual_response.py:139 +#: app/views/handlers/individual_response.py:151 msgid "Person {list_item_position}" msgstr "" -#: app/views/handlers/individual_response.py:251 +#: app/views/handlers/individual_response.py:258 msgid "Cannot answer questions for others in your household" msgstr "" -#: app/views/handlers/individual_response.py:321 -msgid "How would you like {person_name} to receive a separate census?" +#: app/views/handlers/individual_response.py:329 +msgid "" +"How would you like {person_name} to receive a separate " +"census?" msgstr "" -#: app/views/handlers/individual_response.py:344 +#: app/views/handlers/individual_response.py:352 msgid "Text message" msgstr "" -#: app/views/handlers/individual_response.py:346 +#: app/views/handlers/individual_response.py:354 msgid "We will need their mobile number for this" msgstr "" -#: app/views/handlers/individual_response.py:354 +#: app/views/handlers/individual_response.py:362 msgid "Post" msgstr "" -#: app/views/handlers/individual_response.py:356 +#: app/views/handlers/individual_response.py:364 msgid "" "We can only send this to an unnamed resident at the registered household " "address" msgstr "" -#: app/views/handlers/individual_response.py:365 +#: app/views/handlers/individual_response.py:373 msgid "It is no longer possible to receive an access code by post." msgstr "" -#: app/views/handlers/individual_response.py:367 +#: app/views/handlers/individual_response.py:375 msgid "Select how to send access code." msgstr "" -#: app/views/handlers/individual_response.py:370 +#: app/views/handlers/individual_response.py:378 msgid "" "For someone to complete a separate census, we need to send them an " "individual access code." msgstr "" -#: app/views/handlers/individual_response.py:416 +#: app/views/handlers/individual_response.py:424 msgid "Send individual access code" msgstr "" -#: app/views/handlers/individual_response.py:447 -msgid "How would you like to answer {person_name_possessive} questions?" +#: app/views/handlers/individual_response.py:455 +msgid "" +"How would you like to answer {person_name_possessive} " +"questions?" msgstr "" -#: app/views/handlers/individual_response.py:462 +#: app/views/handlers/individual_response.py:470 msgid "I would like to request a separate census for them to complete" msgstr "" -#: app/views/handlers/individual_response.py:468 +#: app/views/handlers/individual_response.py:476 msgid "I will ask them to answer their own questions" msgstr "" -#: app/views/handlers/individual_response.py:472 +#: app/views/handlers/individual_response.py:480 msgid "They will need the household access code from the letter we sent you" msgstr "" -#: app/views/handlers/individual_response.py:478 +#: app/views/handlers/individual_response.py:486 msgid "I will answer for {person_name}" msgstr "" -#: app/views/handlers/individual_response.py:520 +#: app/views/handlers/individual_response.py:537 msgid "How to answer questions" msgstr "" -#: app/views/handlers/individual_response.py:585 +#: app/views/handlers/individual_response.py:602 msgid "Do you want to send an individual access code for {person_name} by post?" msgstr "" -#: app/views/handlers/individual_response.py:593 +#: app/views/handlers/individual_response.py:610 msgid "" "A letter with an individual access code will be sent to your registered " "household address" msgstr "" -#: app/views/handlers/individual_response.py:600 +#: app/views/handlers/individual_response.py:617 msgid "" "The letter will be addressed to Individual Resident " "instead of the name provided" msgstr "" -#: app/views/handlers/individual_response.py:613 +#: app/views/handlers/individual_response.py:630 msgid "Yes, send the access code by post" msgstr "" -#: app/views/handlers/individual_response.py:619 +#: app/views/handlers/individual_response.py:636 msgid "No, send it another way" msgstr "" -#: app/views/handlers/individual_response.py:652 +#: app/views/handlers/individual_response.py:673 msgid "Confirm address" msgstr "" -#: app/views/handlers/individual_response.py:707 -#: app/views/handlers/individual_response.py:745 +#: app/views/handlers/individual_response.py:738 +#: app/views/handlers/individual_response.py:776 msgid "Separate Census" msgstr "" -#: app/views/handlers/individual_response.py:711 +#: app/views/handlers/individual_response.py:742 msgid "Who do you need to request a separate census for?" msgstr "" -#: app/views/handlers/individual_response.py:769 -msgid "What is {person_name_possessive} mobile number?" +#: app/views/handlers/individual_response.py:800 +msgid "What is {person_name_possessive} mobile number?" msgstr "" -#: app/views/handlers/individual_response.py:781 +#: app/views/handlers/individual_response.py:812 msgid "UK mobile number" msgstr "" -#: app/views/handlers/individual_response.py:782 +#: app/views/handlers/individual_response.py:813 msgid "This will not be stored and only used once to send the access code" msgstr "" -#: app/views/handlers/individual_response.py:815 +#: app/views/handlers/individual_response.py:848 msgid "Mobile number" msgstr "" -#: app/views/handlers/individual_response.py:864 +#: app/views/handlers/individual_response.py:897 msgid "Is this mobile number correct?" msgstr "" -#: app/views/handlers/individual_response.py:873 +#: app/views/handlers/individual_response.py:906 msgid "Yes, send the text" msgstr "" -#: app/views/handlers/individual_response.py:911 +#: app/views/handlers/individual_response.py:948 msgid "Confirm mobile number" msgstr "" -#: app/views/handlers/thank_you.py:24 templates/census-thank-you.html:24 -msgid "Thank you for completing the census" +#: app/views/handlers/thank_you.py:22 +msgid "Thank you for completing the survey" msgstr "" #: app/views/handlers/view_submitted_response.py:52 msgid "View Submitted Response" msgstr "" -#: templates/calculatedsummary.html:14 -msgid "Please review your answers and confirm these are correct" -msgstr "" - -#: templates/calculatedsummary.html:25 -msgid "Yes, I confirm these are correct" -msgstr "" - -#: templates/census-thank-you.html:9 templates/confirmation-email.html:8 -msgid "There is a problem with this page" -msgstr "" - -#: templates/census-thank-you.html:22 -msgid "Thank you for completing your census" -msgstr "" - -#: templates/census-thank-you.html:26 -msgid "Thank you for completing the survey" -msgstr "" - -#: templates/census-thank-you.html:32 -msgid "" -"Your individual census has been submitted for " -"{display_address}" -msgstr "" - -#: templates/census-thank-you.html:34 -msgid "" -"Your census has been submitted for the household at " -"{display_address}" -msgstr "" - -#: templates/census-thank-you.html:36 -msgid "" -"Your census has been submitted for the accommodation at " -"{display_address}" -msgstr "" - -#: templates/census-thank-you.html:37 -msgid "" -"Anyone staying at this accommodation for at least 6 months needs to fill " -"in their own individual census, including staff. Your Census Officer will" -" provide you with census forms for your residents." -msgstr "" - -#: templates/census-thank-you.html:49 -msgid "" -"Your personal information is protected by law and will be kept " -"confidential" -msgstr "" - -#: templates/census-thank-you.html:55 -msgid "Get confirmation email" -msgstr "" - -#: templates/census-thank-you.html:56 -msgid "" -"If you would like to be sent confirmation that you have completed your " -"census, enter your email address" -msgstr "" - -#: templates/confirm-email.html:12 templates/partials/answers/address.html:55 +#: templates/confirm-email.html:14 templates/partials/answers/address.html:54 #: templates/question.html:10 #, python-format msgid "There is a problem with your answer" @@ -780,65 +718,71 @@ msgid_plural "There are %(num)s problems with your answer" msgstr[0] "" msgstr[1] "" -#: templates/confirmation-email-sent.html:3 +#: templates/confirmation-email-sent.html:5 msgid "Confirmation email sent" msgstr "" -#: templates/confirmation-email-sent.html:16 +#: templates/confirmation-email-sent.html:21 msgid "A confirmation email has been sent to {email}" msgstr "" -#: templates/confirmation-email-sent.html:25 +#: templates/confirmation-email-sent.html:32 msgid "" "The email will be sent from " -"census.2021@notifications.service.gov.uk" +"census.2021@notifications.service.gov.uk" msgstr "" -#: templates/confirmation-email-sent.html:29 -msgid "Didn't receive an email?" +#: templates/confirmation-email-sent.html:38 +msgid "Didn’t receive an email?" msgstr "" -#: templates/confirmation-email-sent.html:30 +#: templates/confirmation-email-sent.html:41 msgid "" -"It can take a few minutes for the email to arrive. If it doesn't arrive, " -"check your junk mail, or you can send another confirmation email." +"It can take a few minutes for the email to arrive. If it doesn’t arrive, " +"check your junk mail, or you can send " +"another confirmation email." +msgstr "" + +#: templates/confirmation-email.html:9 +msgid "There is a problem with this page" msgstr "" #: templates/confirmation-email.html:12 msgid "Send a confirmation email" msgstr "" -#: templates/feedback-sent.html:6 +#: templates/feedback-sent.html:7 msgid "Feedback sent" msgstr "" -#: templates/feedback-sent.html:19 +#: templates/feedback-sent.html:21 msgid "Thank you for your feedback" msgstr "" -#: templates/feedback-sent.html:20 +#: templates/feedback-sent.html:23 msgid "" "Your comments will help us make improvements to our surveys. We are not " "able to reply to comments, but we appreciate your feedback" msgstr "" -#: templates/feedback-sent.html:28 +#: templates/feedback-sent.html:29 msgid "Done" msgstr "" -#: templates/feedback.html:14 templates/view-submitted-response.html:16 +#: templates/feedback.html:16 templates/preview.html:14 +#: templates/view-submitted-response.html:15 msgid "Back" msgstr "" -#: templates/feedback.html:27 +#: templates/feedback.html:31 #, python-format msgid "There is a problem with your feedback" msgid_plural "There are %(num)s problems with your feedback" msgstr[0] "" msgstr[1] "" -#: templates/feedback.html:38 +#: templates/feedback.html:41 msgid "Send feedback" msgstr "" @@ -846,117 +790,173 @@ msgstr "" msgid "If you can’t answer someone else’s questions" msgstr "" -#: templates/interstitial.html:23 templates/partials/question.html:25 +#: templates/interstitial.html:22 templates/partials/question.html:47 msgid "If you can’t answer questions for this person" msgstr "" -#: templates/interstitial.html:32 templates/layouts/_questionnaire.html:27 +#: templates/interstitial.html:31 templates/layouts/_questionnaire.html:29 msgid "Save and continue" msgstr "" -#: templates/introduction.html:10 +#: templates/introduction.html:8 msgid "Introduction" msgstr "" -#: templates/introduction.html:22 +#: templates/introduction.html:20 +msgid "View the questions you will be asked in this survey" +msgstr "" + +#: templates/introduction.html:24 msgid "Your response is legally required" msgstr "" -#: templates/list-action.html:9 +#: templates/list-action.html:10 msgid "Cancel and return to the previous page" msgstr "" -#: templates/multiple_survey.html:4 templates/multiple_survey.html:7 +#: templates/multiple_survey.html:5 templates/multiple_survey.html:8 msgid "Information" msgstr "" -#: templates/multiple_survey.html:16 +#: templates/multiple_survey.html:20 msgid "Unfortunately you can only complete one survey at a time" msgstr "" -#: templates/multiple_survey.html:17 +#: templates/multiple_survey.html:22 msgid "Close this window to continue with your current survey" msgstr "" -#: templates/signed-out.html:3 +#: templates/preview.html:35 +msgid "Preview of the questions in this survey" +msgstr "" + +#: templates/preview.html:43 +msgid "To answer these questions you need to start survey" +msgstr "" + +#: templates/preview.html:48 +msgid "" +"You may not have to answer all of these questions. The questions you see " +"will depend on the answers you provide." +msgstr "" + +#: templates/preview.html:54 +msgid "Print questions" +msgstr "" + +#: templates/preview.html:68 +msgid "Save questions as PDF" +msgstr "" + +#: templates/partials/introduction/preview.html:34 +#: templates/partials/summary/collapsible-summary.html:51 +#: templates/preview.html:98 +msgid "Show all" +msgstr "" + +#: templates/partials/introduction/preview.html:35 +#: templates/partials/summary/collapsible-summary.html:52 +#: templates/preview.html:99 +msgid "Hide all" +msgstr "" + +#: templates/signed-out.html:5 msgid "Signed out" msgstr "" -#: templates/signed-out.html:6 -msgid "Your survey answers have been saved. You are now signed out" +#: templates/signed-out.html:16 +msgid "

Your progress has been saved

" msgstr "" -#: templates/signed-out.html:9 -msgid "Return to your account" +#: templates/signed-out.html:22 +msgid "" +"To find further information or resume the survey, return " +"to My Account." msgstr "" -#: templates/thank-you.html:6 +#: templates/signed-out.html:26 +msgid "To resume the survey, re-enter your access code." +msgstr "" + +#: templates/thank-you.html:7 msgid "We’ve received your answers" msgstr "" -#: templates/thank-you.html:14 +#: templates/thank-you.html:17 msgid "Back to surveys" msgstr "" -#: templates/thank-you.html:31 +#: templates/thank-you.html:42 msgid "Thank you for completing the {survey_title}" msgstr "" -#: templates/thank-you.html:43 -msgid "Your answers will be processed in the next few weeks." -msgstr "" - -#: templates/thank-you.html:44 -msgid "We may contact you to query your answers via phone or secure message." +#: templates/thank-you.html:54 +msgid "" +"Your response will help inform decision-makers how best to support the UK" +" population and economy." msgstr "" -#: templates/thank-you.html:45 -msgid "For more information on how we use this data." +#: templates/thank-you.html:57 +msgid "Learn more about how we use this data" msgstr "" -#: templates/thank-you.html:49 templates/view-submitted-response.html:66 +#: templates/thank-you.html:60 templates/view-submitted-response.html:76 msgid "For security, you can no longer view or get a copy of your answers" msgstr "" -#: templates/thank-you.html:61 +#: templates/thank-you.html:74 msgid "For security, your answers will only be available to view for another " msgstr "" -#: templates/thank-you.html:62 +#: templates/thank-you.html:75 msgid "Get a copy of your answers" msgstr "" -#: templates/thank-you.html:64 +#: templates/thank-you.html:76 +msgid "We may contact you to query your answers." +msgstr "" + +#: templates/thank-you.html:78 msgid "" -"You can save or print " -"your answers for your records." +"If you need a copy for your records, save or print your answers." msgstr "" -#: templates/layouts/_base.html:158 templates/thank-you.html:67 +#: templates/layouts/_base.html:147 templates/thank-you.html:84 msgid "minute" msgstr "" -#: templates/layouts/_base.html:159 templates/thank-you.html:68 +#: templates/layouts/_base.html:148 templates/thank-you.html:85 msgid "minutes" msgstr "" -#: templates/layouts/_base.html:160 templates/thank-you.html:69 +#: templates/layouts/_base.html:149 templates/thank-you.html:86 msgid "second" msgstr "" -#: templates/layouts/_base.html:161 templates/thank-you.html:70 +#: templates/layouts/_base.html:150 templates/thank-you.html:87 msgid "seconds" msgstr "" -#: templates/thank-you.html:72 +#: templates/thank-you.html:89 msgid "For security, your answers will only be available to view for 45 minutes" msgstr "" +#: templates/thank-you.html:99 +msgid "Get confirmation email" +msgstr "" + +#: templates/thank-you.html:101 +msgid "" +"If you would like to be sent confirmation that you have completed your " +"survey, enter your email address" +msgstr "" + #: templates/view-submitted-response.html:35 msgid "Print answers" msgstr "" -#: templates/view-submitted-response.html:46 +#: templates/view-submitted-response.html:49 msgid "Save answers as PDF" msgstr "" @@ -964,504 +964,570 @@ msgstr "" msgid "Page is not available" msgstr "" +#: templates/errors/401.html:4 +msgid "You will need to sign back in to access your account" +msgstr "" + +#: templates/errors/401.html:5 +msgid "" +"To access this page you need to re-enter your access " +"code." +msgstr "" + #: templates/errors/401.html:6 -msgid "Sorry, you need to sign in again" +msgid "" +"If you are completing a business survey, you need to sign back in to your account." msgstr "" #: templates/errors/401.html:7 +msgid "" +"If you started your survey using an access code, you need to re-enter your code." +msgstr "" + +#: templates/errors/401.html:10 +msgid "Sorry, you need to sign in again" +msgstr "" + +#: templates/errors/401.html:11 msgid "This is because you have either:" msgstr "" -#: templates/errors/401.html:9 +#: templates/errors/401.html:13 msgid "" "been inactive for 45 minutes and your session has timed out to protect " "your information" msgstr "" -#: templates/errors/401.html:10 +#: templates/errors/401.html:14 msgid "followed a link to a page you are not signed in to" msgstr "" -#: templates/errors/401.html:11 +#: templates/errors/401.html:15 msgid "followed a link to a survey that has already been submitted" msgstr "" -#: templates/errors/401.html:13 -msgid "You will need to sign back in to access your account" +#: templates/errors/401.html:22 templates/errors/403.html:15 +#: templates/errors/404.html:16 templates/errors/500.html:21 +#: templates/errors/submission-failed.html:17 +msgid "Business surveys" +msgstr "" + +#: templates/errors/401.html:24 templates/errors/403.html:17 +#: templates/errors/404.html:18 templates/errors/500.html:24 +#: templates/errors/submission-failed.html:19 +msgid "All other surveys" msgstr "" -#: templates/errors/403.html:7 +#: templates/errors/403.html:4 +msgid "For further help, please contact us." +msgstr "" + +#: templates/errors/403.html:5 +msgid "" +"If you are completing a business survey and you need further help, please" +" contact us." +msgstr "" + +#: templates/errors/403.html:6 +msgid "" +"If you started your survey using an access code and you need further " +"help, please contact us." +msgstr "" + +#: templates/errors/403.html:10 msgid "You may need to update your browser to a newer version." msgstr "" -#: templates/errors/403.html:8 +#: templates/errors/403.html:11 msgid "If the problem still occurs, try using a different browser or device." msgstr "" -#: templates/errors/403.html:9 -msgid "For further help, please contact us." +#: templates/errors/404.html:3 templates/errors/404.html:9 +msgid "Page not found" msgstr "" -#: templates/errors/404.html:3 templates/errors/404.html:6 -msgid "Page not found" +#: templates/errors/404.html:4 +msgid "" +"If the web address is correct or you selected a link or button, contact us for more help." +msgstr "" + +#: templates/errors/404.html:5 templates/errors/submission-failed.html:7 +msgid "" +"If you are completing a business survey, please contact " +"us." +msgstr "" + +#: templates/errors/404.html:6 templates/errors/submission-failed.html:8 +msgid "" +"If you started your survey using an access code, please contact us." msgstr "" -#: templates/errors/404.html:7 +#: templates/errors/404.html:10 msgid "If you entered a web address, check it is correct." msgstr "" -#: templates/errors/404.html:8 +#: templates/errors/404.html:11 msgid "If you pasted the web address, check you copied the whole address." msgstr "" -#: templates/errors/404.html:9 +#: templates/errors/404.html:15 msgid "" -"If the web address is correct or you selected a link or button, contact us for more help." +"If the web address is correct or you selected a link or button, please " +"see the following help links." msgstr "" #: templates/errors/500.html:3 msgid "An error has occurred" msgstr "" +#: templates/errors/500.html:4 +msgid "" +"If you have attempted to submit your survey, you should check that this " +"was successful. To do this, sign in to your business " +"survey account." +msgstr "" + +#: templates/errors/500.html:5 +msgid "If you need more help, contact us." +msgstr "" + #: templates/errors/500.html:6 -msgid "Sorry, there is a problem with this service" +msgid "" +"If you have attempted to submit your survey, you should check that this " +"was successful. To do this, re-enter your code." msgstr "" #: templates/errors/500.html:7 -msgid "Try again later." +msgid "" +"If you need more help, contact us about business " +"surveys." msgstr "" #: templates/errors/500.html:8 -msgid "If you have started a survey, your answers have been saved." +msgid "" +"If you need more help, contact us about all other " +"surveys." msgstr "" -#: templates/errors/500.html:9 -msgid "" -"Contact us if you need to speak to someone about your" -" survey." +#: templates/errors/500.html:11 +msgid "Sorry, there is a problem with this service" +msgstr "" + +#: templates/errors/500.html:12 +msgid "Try again later." +msgstr "" + +#: templates/errors/500.html:13 +msgid "If you have started a survey, your answers have been saved." msgstr "" #: templates/errors/previously-submitted.html:3 msgid "Submission Complete" msgstr "" -#: templates/errors/previously-submitted.html:6 -msgid "This page is no longer available" +#: templates/errors/previously-submitted.html:4 +msgid "Return to previous page" msgstr "" #: templates/errors/previously-submitted.html:7 -msgid "Your survey has been submitted" +msgid "This page is no longer available" msgstr "" #: templates/errors/previously-submitted.html:8 -msgid "Return to previous page" +msgid "Your survey has been submitted" msgstr "" -#: templates/errors/submission-failed.html:9 -msgid "You can try to submit your census again" +#: templates/errors/submission-failed.html:5 +msgid "You can try to submit your survey again" msgstr "" -#: templates/errors/submission-failed.html:10 +#: templates/errors/submission-failed.html:6 msgid "" "If this problem keeps happening, please contact us " "for help." msgstr "" -#: templates/individual_response/confirmation-post.html:15 +#: templates/errors/submission-failed.html:16 +msgid "If this problem keeps happening, please see the following help links." +msgstr "" + +#: templates/individual_response/confirmation-post.html:20 msgid "A letter has been sent to Individual Resident at {display_address}" msgstr "" -#: templates/individual_response/confirmation-post.html:17 +#: templates/individual_response/confirmation-post.html:22 msgid "" "The letter with an individual access code should arrive soon for them to " "complete their own census" msgstr "" -#: templates/individual_response/confirmation-text-message.html:15 +#: templates/individual_response/confirmation-text-message.html:20 msgid "We have sent a text to {mobile_number}" msgstr "" -#: templates/individual_response/confirmation-text-message.html:17 +#: templates/individual_response/confirmation-text-message.html:23 msgid "" "The text message with an individual access code should arrive soon for " "them to complete their own census" msgstr "" -#: templates/individual_response/confirmation-text-message.html:26 -msgid "The text will be sent from Census2021" +#: templates/individual_response/confirmation-text-message.html:34 +msgid "The text will be sent from Census2021" msgstr "" -#: templates/individual_response/interstitial.html:8 +#: templates/individual_response/interstitial.html:9 msgid "If you can't answer questions for others in your household" msgstr "" -#: templates/individual_response/interstitial.html:9 +#: templates/individual_response/interstitial.html:11 msgid "" "You can ask the people you live with to answer their own questions by " "sharing the household access code with them." msgstr "" -#: templates/individual_response/interstitial.html:10 +#: templates/individual_response/interstitial.html:13 msgid "" "If this is not possible, you can request a separate census for them to " "complete." msgstr "" -#: templates/individual_response/interstitial.html:13 +#: templates/individual_response/interstitial.html:17 msgid "Request separate census" msgstr "" -#: templates/individual_response/question.html:9 +#: templates/individual_response/question.html:10 msgid "" "To request a census in a different format or for further help, please contact us" msgstr "" -#: templates/layouts/_base.html:20 +#: templates/layouts/_base.html:15 msgid "Previous" msgstr "" -#: templates/layouts/_base.html:71 +#: templates/layouts/_base.html:60 msgid "Tell us whether you accept cookies" msgstr "" -#: templates/layouts/_base.html:72 +#: templates/layouts/_base.html:61 msgid "" "We use cookies to collect information" -" about how you use census.gov.uk. We use this information to make the " +" about how you use {cookie_domain}. We use this information to make the " "website work as well as possible and improve our services." msgstr "" -#: templates/layouts/_base.html:73 +#: templates/layouts/_base.html:62 msgid "" "You’ve accepted all cookies. You can change your cookie preferences at any " "time." msgstr "" -#: templates/layouts/_base.html:74 +#: templates/layouts/_base.html:63 msgid "Accept all cookies" msgstr "" -#: templates/layouts/_base.html:75 +#: templates/layouts/_base.html:64 msgid "Set cookie preferences" msgstr "" -#: templates/layouts/_base.html:76 -#: templates/partials/introduction/preview.html:30 -#: templates/partials/summary/collapsible-summary.html:15 +#: templates/layouts/_base.html:65 msgid "Hide" msgstr "" -#: templates/layouts/_base.html:131 +#: templates/layouts/_base.html:114 msgid "Skip to main content" msgstr "" -#: templates/layouts/_base.html:153 +#: templates/layouts/_base.html:142 msgid "You will be signed out soon" msgstr "" -#: templates/layouts/_base.html:154 +#: templates/layouts/_base.html:143 msgid "It appears you have been inactive for a while." msgstr "" -#: templates/layouts/_base.html:155 +#: templates/layouts/_base.html:144 msgid "" "To protect your information, your progress will be saved and you will be " "signed out in" msgstr "" -#: templates/layouts/_base.html:156 +#: templates/layouts/_base.html:145 msgid "You are being signed out" msgstr "" -#: templates/layouts/_base.html:157 +#: templates/layouts/_base.html:146 msgid "Continue survey" msgstr "" -#: templates/layouts/_questionnaire.html:38 +#: templates/layouts/_calculatedsummary.html:17 +msgid "Yes, I confirm this is correct" +msgstr "" + +#: templates/layouts/_questionnaire.html:47 msgid "Choose another section and return to this later" msgstr "" -#: templates/layouts/configs/_save-sign-out-button.html:21 +#: templates/layouts/configs/_header.html:10 msgid "Exit" msgstr "" -#: templates/macros/helpers.html:13 +#: templates/macros/helpers.html:15 msgid "Interviewer note:" msgstr "" -#: templates/partials/answer-guidance.html:18 -#: templates/partials/definition.html:18 -#: templates/partials/individual-response-guidance.html:13 -msgid "Hide this" -msgstr "" - -#: templates/partials/email-form.html:25 +#: templates/partials/confirmation-email-form.html:27 msgid "Email address" msgstr "" -#: templates/partials/email-form.html:26 +#: templates/partials/confirmation-email-form.html:28 msgid "This will not be stored and only used once to send your confirmation" msgstr "" -#: templates/partials/email-form.html:39 +#: templates/partials/confirmation-email-form.html:39 msgid "Send confirmation" msgstr "" -#: templates/partials/feedback-call-to-action.html:6 +#: templates/partials/feedback-call-to-action.html:7 msgid "What do you think about this service?" msgstr "" -#: templates/partials/feedback-call-to-action.html:7 +#: templates/partials/feedback-call-to-action.html:8 msgid "Your comments will help us make improvements" msgstr "" -#: templates/partials/feedback-call-to-action.html:9 +#: templates/partials/feedback-call-to-action.html:10 msgid "Give feedback" msgstr "" -#: templates/partials/individual-response-guidance.html:24 +#: templates/partials/individual-response-guidance.html:19 msgid "" -"You can share your household access code with the people you " -"live with so they can complete their own sections." +"You can share your household access code with the people" +" you live with so they can complete their own sections." msgstr "" -#: templates/partials/individual-response-guidance.html:25 +#: templates/partials/individual-response-guidance.html:22 msgid "" "If this is not possible, there are other ways each person " "can complete their own census." msgstr "" -#: templates/partials/last_viewed_question_guidance.html:7 +#: templates/partials/last_viewed_question_guidance.html:10 msgid "This is the last viewed question in this section" msgstr "" -#: templates/partials/last_viewed_question_guidance.html:10 +#: templates/partials/last_viewed_question_guidance.html:12 msgid "" "You can also go back to the start" " of the section" msgstr "" -#: templates/partials/question.html:60 +#: templates/partials/preview-question.html:31 +#: templates/partials/question.html:86 +msgid "Or" +msgstr "" + +#: templates/partials/preview-question.html:51 +msgid "{max_characters} characters can be added." +msgstr "" + +#: templates/partials/question.html:78 msgid "Selecting this will clear your answer" msgstr "" -#: templates/partials/question.html:61 +#: templates/partials/question.html:79 msgid "cleared" msgstr "" -#: templates/partials/question.html:64 +#: templates/partials/question.html:81 msgid "Selecting this will deselect any selected options" msgstr "" -#: templates/partials/question.html:65 templates/partials/question.html:73 +#: templates/partials/question.html:82 templates/partials/question.html:90 msgid "deselected" msgstr "" -#: templates/partials/question.html:69 -msgid "Or" -msgstr "" - -#: templates/partials/answers/address.html:9 +#: templates/partials/answers/address.html:8 msgid "Address line 1" msgstr "" -#: templates/partials/answers/address.html:14 +#: templates/partials/answers/address.html:13 msgid "Address line 2" msgstr "" -#: templates/partials/answers/address.html:18 +#: templates/partials/answers/address.html:17 msgid "Town or city" msgstr "" -#: templates/partials/answers/address.html:22 +#: templates/partials/answers/address.html:21 msgid "Postcode" msgstr "" -#: templates/partials/answers/address.html:33 +#: templates/partials/answers/address.html:32 msgid "Enter address or postcode and select from results" msgstr "" -#: templates/partials/answers/address.html:35 +#: templates/partials/answers/address.html:34 msgid "Search for an address" msgstr "" -#: templates/partials/answers/address.html:36 +#: templates/partials/answers/address.html:35 msgid "Manually enter address" msgstr "" -#: templates/partials/answers/address.html:41 +#: templates/partials/answers/address.html:40 msgid "" "Use up and down keys to navigate suggestions once you’ve typed more than " "two characters. Use the enter key to select a suggestion. Touch device " "users, explore by touch or with swipe gestures." msgstr "" -#: templates/partials/answers/address.html:42 +#: templates/partials/answers/address.html:41 msgid "You have selected" msgstr "" -#: templates/partials/answers/address.html:43 +#: templates/partials/answers/address.html:42 msgid "Enter 3 or more characters for suggestions." msgstr "" -#: templates/partials/answers/address.html:44 +#: templates/partials/answers/address.html:43 msgid "There is one suggestion available." msgstr "" -#: templates/partials/answers/address.html:45 +#: templates/partials/answers/address.html:44 msgid "There are {n} suggestions available." msgstr "" -#: templates/partials/answers/address.html:46 +#: templates/partials/answers/address.html:45 msgid "" "Results have been limited to 10 suggestions. Type more characters to " "improve your search" msgstr "" -#: templates/partials/answers/address.html:47 +#: templates/partials/answers/address.html:46 msgid "There are {n} for {x}" msgstr "" -#: templates/partials/answers/address.html:48 +#: templates/partials/answers/address.html:47 msgid "{n} addresses" msgstr "" -#: templates/partials/answers/address.html:49 +#: templates/partials/answers/address.html:48 msgid "Enter more of the address to improve results" msgstr "" -#: templates/partials/answers/address.html:50 +#: templates/partials/answers/address.html:49 msgid "Select an address" msgstr "" -#: templates/partials/answers/address.html:51 +#: templates/partials/answers/address.html:50 msgid "No results found. Try entering a different part of the address" msgstr "" -#: templates/partials/answers/address.html:52 +#: templates/partials/answers/address.html:51 msgid "{n} results found. Enter more of the address to improve results" msgstr "" -#: templates/partials/answers/address.html:53 +#: templates/partials/answers/address.html:52 msgid "Enter more of the address to get results" msgstr "" -#: templates/partials/answers/address.html:57 +#: templates/partials/answers/address.html:56 msgid "Select or manually enter an address" msgstr "" -#: templates/partials/answers/address.html:58 +#: templates/partials/answers/address.html:57 msgid "Sorry, there was a problem loading addresses" msgstr "" -#: templates/partials/answers/address.html:59 +#: templates/partials/answers/address.html:58 msgid "Enter address manually" msgstr "" -#: templates/partials/answers/checkbox.html:13 +#: templates/partials/answers/checkbox.html:12 msgid "Select all that apply" msgstr "" -#: templates/partials/answers/date.html:23 +#: templates/partials/answers/date.html:21 msgid "Day" msgstr "" -#: templates/partials/answers/date.html:38 +#: templates/partials/answers/date.html:35 msgid "Month" msgstr "" -#: templates/partials/answers/date.html:53 +#: templates/partials/answers/date.html:49 msgid "Year" msgstr "" -#: templates/partials/answers/radio.html:15 +#: templates/partials/answers/radio.html:14 msgid "Clear selection" msgstr "" #: templates/partials/answers/textarea.html:20 -#: templates/partials/answers/textfield.html:45 +#: templates/partials/answers/textfield.html:44 msgid "You have {x} character remaining" msgstr "" #: templates/partials/answers/textarea.html:21 -#: templates/partials/answers/textfield.html:46 +#: templates/partials/answers/textfield.html:45 msgid "You have {x} characters remaining" msgstr "" -#: templates/partials/answers/textfield.html:25 +#: templates/partials/answers/textfield.html:24 msgid "" "Use up and down keys to navigate suggestions once you've typed more than " "two characters. Use the enter key to select a suggestion. Touch device " "users, explore by touch or with swipe gestures." msgstr "" -#: templates/partials/answers/textfield.html:26 +#: templates/partials/answers/textfield.html:25 msgid "Continue entering to improve suggestions" msgstr "" -#: templates/partials/answers/textfield.html:27 +#: templates/partials/answers/textfield.html:26 msgid "Suggestions" msgstr "" -#: templates/partials/answers/textfield.html:28 +#: templates/partials/answers/textfield.html:27 msgid "No results found" msgstr "" -#: templates/partials/answers/textfield.html:29 +#: templates/partials/answers/textfield.html:28 msgid "Continue entering to get suggestions" msgstr "" -#: templates/partials/answers/textfield.html:43 +#: templates/partials/answers/textfield.html:42 msgid "{x} character too many" msgstr "" -#: templates/partials/answers/textfield.html:44 +#: templates/partials/answers/textfield.html:43 msgid "{x} characters too many" msgstr "" -#: templates/partials/introduction/preview.html:29 -#: templates/partials/summary/collapsible-summary.html:14 -msgid "Show" -msgstr "" - -#: templates/partials/introduction/preview.html:48 -#: templates/partials/summary/collapsible-summary.html:59 -msgid "Show all" -msgstr "" - -#: templates/partials/introduction/preview.html:49 -#: templates/partials/summary/collapsible-summary.html:60 -msgid "Hide all" -msgstr "" - -#: templates/partials/introduction/start-survey.html:5 +#: templates/partials/introduction/start-survey.html:6 msgid "Start survey" msgstr "" -#: templates/partials/summary/collapsible-summary.html:37 -#: templates/partials/summary/summary.html:19 -msgid "No answer provided" -msgstr "" - -#: templates/partials/summary/collapsible-summary.html:38 +#: templates/partials/summary/collapsible-summary.html:28 #: templates/partials/summary/list-summary.html:8 -#: templates/partials/summary/summary.html:20 +#: templates/partials/summary/summary.html:27 msgid "Change" msgstr "" -#: templates/partials/summary/collapsible-summary.html:39 -#: templates/partials/summary/summary.html:21 -msgid "Change your answer for:" -msgstr "" - +#: templates/partials/summary/collapsible-summary.html:29 #: templates/partials/summary/list-summary.html:9 +#: templates/partials/summary/summary.html:28 msgid "Change details for {item_name}" msgstr "" #: templates/partials/summary/list-summary.html:10 +#: templates/partials/summary/summary.html:25 msgid "Remove" msgstr "" @@ -1469,3 +1535,7 @@ msgstr "" msgid "Remove {item_name}" msgstr "" +#: templates/partials/summary/summary.html:26 +msgid "Remove details for {item_name}" +msgstr "" + diff --git a/app/utilities/__init__.py b/app/utilities/__init__.py index 388b2b1a34..23599a169c 100644 --- a/app/utilities/__init__.py +++ b/app/utilities/__init__.py @@ -1,3 +1,3 @@ -from .strings import safe_content +from app.utilities.strings import safe_content __all__ = ["safe_content"] diff --git a/app/utilities/bind_context.py b/app/utilities/bind_context.py new file mode 100644 index 0000000000..199fb09447 --- /dev/null +++ b/app/utilities/bind_context.py @@ -0,0 +1,17 @@ +from structlog import contextvars + +from app.data_models.metadata_proxy import MetadataProxy + + +def bind_contextvars_schema_from_metadata(metadata: MetadataProxy) -> None: + """ + Metadata always contains exactly one way of identifying a schema, bind the reference to contextvars + """ + if schema_name := metadata.schema_name: + contextvars.bind_contextvars(schema_name=schema_name) + + if schema_url := metadata.schema_url: + contextvars.bind_contextvars(schema_url=schema_url) + + if cir_instrument_id := metadata.cir_instrument_id: + contextvars.bind_contextvars(cir_instrument_id=cir_instrument_id) diff --git a/app/utilities/credentials.py b/app/utilities/credentials.py new file mode 100644 index 0000000000..f24b803217 --- /dev/null +++ b/app/utilities/credentials.py @@ -0,0 +1,12 @@ +import requests +from flask import current_app + +from app.oidc.oidc import OIDCCredentialsService + + +def fetch_and_apply_oidc_credentials(session: requests.Session, client_id: str) -> None: + # Type ignore: oidc_credentials_service is a singleton of this application + oidc_credentials_service: OIDCCredentialsService = current_app.eq["oidc_credentials_service"] # type: ignore + + credentials = oidc_credentials_service.get_credentials(iap_client_id=client_id) + credentials.apply(headers=session.headers) diff --git a/app/utilities/decimal_places.py b/app/utilities/decimal_places.py new file mode 100644 index 0000000000..e9fee55907 --- /dev/null +++ b/app/utilities/decimal_places.py @@ -0,0 +1,127 @@ +from decimal import Decimal +from typing import Literal, TypeAlias + +import flask_babel +from babel import Locale, numbers, units +from babel.numbers import NumberPattern, get_currency_precision + +UnitLengthType: TypeAlias = Literal["short", "long", "narrow"] + + +def custom_format_decimal(value: int | Decimal | float, locale: Locale | str) -> str: + """ + This function provides a wrapper for the numbers `format_decimal` method, generating the + number format (including the desired number of decimals), based on the value entered by the user and + the locale. + """ + number_format = get_number_format(value, locale) + + return numbers.format_decimal( + value, + format=number_format, + locale=locale, + ) + + +def get_formatted_currency( + *, + value: float | Decimal, + currency: str = "GBP", + locale: str | Locale | None = None, + decimal_limit: int | None = None, +) -> str: + """ + This function provides a wrapper for the numbers `format_currency` method, generating the + number format (including the desired number of decimals). + + The number of decimals displayed is based on the value entered by the user, the decimal limit set in the schema + and the locale. + """ + locale = locale or flask_babel.get_locale() + decimal_places = _get_decimal_places(value) + + # get locale pattern + parsed_locale = Locale.parse(locale) + + # Use the default babel currency format "standard" + number_format = parsed_locale.currency_formats["standard"] + + currency_precision = get_currency_precision(currency) + + # If there is no decimal limit then use the currency precision value if the number of decimals entered + # is less than the value returned by babel's currency precision method. + if ( + decimal_limit is not None + and currency_precision > decimal_limit >= decimal_places + ) or (decimal_limit is None and not decimal_places): + currency_digits = False + else: + currency_digits = decimal_places < currency_precision + + # The decimal limit is set to either the number of decimal places entered by the user, or the currency precision + # value for the given currency, whichever is larger. + decimal_limit = max(decimal_places, currency_precision) + + # Formats the number based on the number of decimal places and the decimal limit that have been calcualted + # above. + number_format.frac_prec = (min(decimal_places, decimal_limit), decimal_limit) + + return numbers.format_currency( + number=value, + currency=currency, + format=number_format, + locale=parsed_locale, + currency_digits=currency_digits, + ) + + +def custom_format_unit( + value: int | float | Decimal, + measurement_unit: str, + locale: Locale | str, + length: UnitLengthType = "short", +) -> str: + """ + This function provides a wrapper for the numbers `format_unit` method, generating the + number format (including the desired number of decimals), based on the value entered by the user and + the locale. + """ + number_format = get_number_format(value, locale) + + formatted_unit: str = units.format_unit( + value=value, + measurement_unit=measurement_unit, + length=length, + # Type ignore: babel function has incorrect type hinting, NumberPattern is valid here + format=number_format, # type: ignore + locale=locale, + ) + + return formatted_unit + + +def get_number_format( + value: int | float | Decimal, locale: Locale | str +) -> NumberPattern: + """ + Generates the number format based on the value entered by the user and the locale + + Format follows the number formats as specified in the babel docs e.g: '#,##0.###' + + Uses the decimal places set by the user with frac_prec to ensure that trailing zeroes + are not dropped and that the correct number of decimal places as entered by the user are displayed + after formatting. + """ + decimal_places = _get_decimal_places(value) + locale = Locale.parse(locale) + locale_decimal_format: NumberPattern = locale.decimal_formats[None] + locale_decimal_format.frac_prec = (decimal_places, decimal_places) + return locale_decimal_format + + +def _get_decimal_places(value: int | float | Decimal | None) -> int: + """ + We use '.' rather than the decimal separator based on the locale as the separator will always be + formatted so that it is '.' by the time it reaches this method. + """ + return len(str(value).split(".")[1]) if "." in str(value) else 0 diff --git a/app/utilities/json.py b/app/utilities/json.py index 045067b476..6ca7c4a347 100644 --- a/app/utilities/json.py +++ b/app/utilities/json.py @@ -1,13 +1,15 @@ +from typing import IO, Any + import simplejson as json -def json_load(file, **kwargs): +def json_load(file: IO[str], **kwargs: Any) -> Any: return json.load(file, use_decimal=True, **kwargs) -def json_loads(data, **kwargs): +def json_loads(data: str, **kwargs: Any) -> Any: return json.loads(data, use_decimal=True, **kwargs) -def json_dumps(data, **kwargs) -> str: +def json_dumps(data: Any, **kwargs: Any) -> str: return json.dumps(data, for_json=True, use_decimal=True, **kwargs) diff --git a/app/utilities/make_immutable.py b/app/utilities/make_immutable.py new file mode 100644 index 0000000000..522f6ec324 --- /dev/null +++ b/app/utilities/make_immutable.py @@ -0,0 +1,14 @@ +from collections import abc +from typing import Any + +from werkzeug.datastructures import ImmutableDict + + +def make_immutable(data: Any) -> Any: + if isinstance(data, abc.Hashable): + return data + if isinstance(data, list): + return tuple(make_immutable(item) for item in data) + if isinstance(data, dict): + key_value_tuples = {k: make_immutable(v) for k, v in data.items()} + return ImmutableDict(key_value_tuples) diff --git a/app/utilities/mappings.py b/app/utilities/mappings.py new file mode 100644 index 0000000000..4a7caa58ad --- /dev/null +++ b/app/utilities/mappings.py @@ -0,0 +1,46 @@ +from typing import Generator, Iterable, Mapping, Sequence + +from ordered_set import OrderedSet + +from app.utilities.types import SectionKey + + +def get_flattened_mapping_values( + map_to_flatten: Mapping[SectionKey, Iterable[str]] | Mapping[str, Iterable[str]], +) -> OrderedSet[str]: + return OrderedSet([x for v in map_to_flatten.values() for x in v]) + + +def get_mappings_with_key( # noqa: C901 pylint: disable=too-complex + key: str, *, data: Mapping | Sequence, ignore_keys: list[str] | None = None +) -> Generator[Mapping, None, None]: + ignore_keys = ignore_keys or [] + + def _handle_sequence(value: Sequence) -> Generator[Mapping, None, None]: + for element in value: + if isinstance(element, Mapping): + yield from get_mappings_with_key( + key, data=element, ignore_keys=ignore_keys + ) + + if isinstance(data, Sequence): + yield from _handle_sequence(data) + + if isinstance(data, Mapping): + if key not in ignore_keys and key in data: + yield data + + for k, v in data.items(): + if k in ignore_keys: + continue + if isinstance(v, Mapping): + yield from get_mappings_with_key(key, data=v, ignore_keys=ignore_keys) + if isinstance(v, Sequence): + yield from _handle_sequence(v) + + +def get_values_for_key( + key: str, *, data: Mapping | Sequence, ignore_keys: list[str] | None = None +) -> Generator: + for mapping in get_mappings_with_key(key, data=data, ignore_keys=ignore_keys): + yield mapping[key] diff --git a/app/utilities/metadata_parser.py b/app/utilities/metadata_parser.py deleted file mode 100644 index a6a4f2e426..0000000000 --- a/app/utilities/metadata_parser.py +++ /dev/null @@ -1,217 +0,0 @@ -import functools -from datetime import datetime, timezone -from typing import Dict - -from marshmallow import ( - EXCLUDE, - Schema, - ValidationError, - fields, - post_load, - pre_load, - validate, - validates_schema, -) -from structlog import get_logger - -from app.questionnaire.rules.utils import parse_iso_8601_datetime -from app.utilities.schema import get_schema_name_from_params - -logger = get_logger() - - -class RegionCode(validate.Regexp): - """A region code defined as per ISO 3166-2:GB - Currently, this does not validate the subdivision, but only checks length - """ - - def __init__(self, *args, **kwargs): - super().__init__("^GB-[A-Z]{3}$", *args, **kwargs) - - -class UUIDString(fields.UUID): - """Currently, runner cannot handle UUID objects in metadata - Since all metadata is serialized and deserialized to JSON. - This custom field deserializes UUIDs to strings. - """ - - def _deserialize(self, *args, **kwargs): # pylint: disable=arguments-differ - return str(super()._deserialize(*args, **kwargs)) - - -class DateString(fields.DateTime): - """Currently, runner cannot handle Date objects in metadata - Since all metadata is serialized and deserialized to JSON. - This custom field deserializes Dates to strings. - """ - - def _deserialize(self, *args, **kwargs): # pylint: disable=arguments-differ - date = super()._deserialize(*args, **kwargs) - - if self.format == "iso8601": - return date.isoformat() - - return date.strftime(self.format) - - -VALIDATORS = { - "date": functools.partial(DateString, format="%Y-%m-%d", required=True), - "uuid": functools.partial(UUIDString, required=True), - "boolean": functools.partial(fields.Boolean, required=True), - "string": functools.partial(fields.String, required=True), - "url": functools.partial(fields.Url, required=True), - "iso_8601_date_string": functools.partial( - DateString, format="iso8601", required=True - ), -} - - -class StripWhitespaceMixin: - @pre_load() - def strip_whitespace( - self, items, **kwargs - ): # pylint: disable=no-self-use, unused-argument - for key, value in items.items(): - if isinstance(value, str): - items[key] = value.strip() - return items - - -class RunnerMetadataSchema(Schema, StripWhitespaceMixin): - """Metadata which is required for the operation of runner itself""" - - jti = VALIDATORS["uuid"]() # type:ignore - ru_ref = VALIDATORS["string"](validate=validate.Length(min=1)) # type:ignore - collection_exercise_sid = VALIDATORS["string"]( - validate=validate.Length(min=1) - ) # type:ignore - tx_id = VALIDATORS["uuid"]() # type:ignore - response_id = VALIDATORS["string"](required=False) # type:ignore - - account_service_url = VALIDATORS["url"](required=True) # type:ignore - case_id = VALIDATORS["uuid"]() # type:ignore - account_service_log_out_url = VALIDATORS["url"](required=False) # type:ignore - roles = fields.List(fields.String(), required=False) - survey_url = VALIDATORS["url"](required=False) # type:ignore - language_code = VALIDATORS["string"](required=False) # type:ignore - channel = VALIDATORS["string"]( - required=False, validate=validate.Length(min=1) - ) # type:ignore - case_type = VALIDATORS["string"](required=False) # type:ignore - response_expires_at = VALIDATORS["iso_8601_date_string"]( - required=False, - validate=lambda x: parse_iso_8601_datetime(x) > datetime.now(tz=timezone.utc), - ) # type:ignore - - # Either schema_name OR the three census parameters are required. Should be required after census. - schema_name = VALIDATORS["string"](required=False) # type:ignore - - # The following three parameters can be removed after Census - survey = VALIDATORS["string"]( - required=False, validate=validate.OneOf(("CENSUS", "CCS")), missing="CENSUS" - ) # type:ignore - region_code = VALIDATORS["string"]( - required=False, validate=RegionCode() - ) # type:ignore - - # The following two parameters are for business schemas - form_type = VALIDATORS["string"](required=False) # type:ignore - eq_id = VALIDATORS["string"](required=False) # type:ignore - - @validates_schema - def validate_schema_name(self, data, **kwargs): - # pylint: disable=no-self-use, unused-argument - """Function to validate the business schema parameters""" - if not data.get("schema_name"): - business_schema_claims = ( - data.get("eq_id"), - data.get("form_type"), - ) - if not all(business_schema_claims): - raise ValidationError( - "Either 'schema_name' or 'eq_id' and 'form_type' must be defined" - ) - - @post_load - def update_schema_name(self, data, **kwargs): - # pylint: disable=no-self-use, unused-argument - """Function to transform parameters into a business schema""" - if data.get("schema_name"): - logger.info( - "Using schema_name claim to specify schema, overriding eq_id and form_type" - ) - else: - data["schema_name"] = get_schema_name_from_params( - data.get("eq_id"), data.get("form_type") - ) - return data - - @post_load - def update_response_id( - self, data, **kwargs - ): # pylint: disable=no-self-use, unused-argument - """ - If response_id is present : return as it is - If response_id is not present : Build response_id from ru_ref,collection_exercise_sid,eq_id and form_type - and updates metadata with response_id - - - """ - if data.get("response_id"): - logger.info( - "'response_id' exists in claims, skipping 'response_id' generation" - ) - return data - eq_id = data.get("eq_id") - form_type = data.get("form_type") - if eq_id and form_type: - ru_ref = data["ru_ref"] - collection_exercise_sid = data["collection_exercise_sid"] - response_id = f"{ru_ref}{collection_exercise_sid}{eq_id}{form_type}" - data["response_id"] = response_id - return data - - raise ValidationError( - "Both 'eq_id' and 'form_type' must be defined when 'response_id' is not defined" - ) - - -def validate_questionnaire_claims(claims, questionnaire_specific_metadata): - """Validate any survey specific claims required for a questionnaire""" - dynamic_fields = {} - - for metadata_field in questionnaire_specific_metadata: - field_arguments = {} - validators = [] - - if metadata_field.get("optional"): - field_arguments["required"] = False - - if any( - length_limit in metadata_field - for length_limit in ("min_length", "max_length", "length") - ): - validators.append( - validate.Length( - min=metadata_field.get("min_length"), - max=metadata_field.get("max_length"), - equal=metadata_field.get("length"), - ) - ) - - dynamic_fields[metadata_field["name"]] = VALIDATORS[metadata_field["type"]]( - validate=validators, **field_arguments - ) - - questionnaire_metadata_schema = type( - "QuestionnaireMetadataSchema", (Schema, StripWhitespaceMixin), dynamic_fields - )(unknown=EXCLUDE) - - # The load method performs validation. - return questionnaire_metadata_schema.load(claims) - - -def validate_runner_claims(claims: Dict): - """Validate claims required for runner to function""" - runner_metadata_schema = RunnerMetadataSchema(unknown=EXCLUDE) - return runner_metadata_schema.load(claims) diff --git a/app/utilities/metadata_parser_v2.py b/app/utilities/metadata_parser_v2.py new file mode 100644 index 0000000000..431c72bc19 --- /dev/null +++ b/app/utilities/metadata_parser_v2.py @@ -0,0 +1,165 @@ +import functools +from datetime import datetime, timezone +from typing import Any, Callable, Iterable, Mapping, MutableMapping + +from marshmallow import ( + EXCLUDE, + INCLUDE, + Schema, + ValidationError, + fields, + pre_load, + validate, + validates_schema, +) +from structlog import get_logger + +from app.authentication.auth_payload_versions import AuthPayloadVersion +from app.questionnaire.rules.utils import parse_iso_8601_datetime +from app.utilities.metadata_validators import DateString, RegionCode, UUIDString + +logger = get_logger() + +VALIDATORS: Mapping[str, Callable] = { + "date": functools.partial(DateString, format="%Y-%m-%d", required=True), + "uuid": functools.partial(UUIDString, required=True), + "boolean": functools.partial(fields.Boolean, required=True), + "string": functools.partial(fields.String, required=True), + "url": functools.partial(fields.Url, required=True), + "iso_8601_date_string": functools.partial( + DateString, format="iso8601", required=True + ), +} + + +class StripWhitespaceMixin: + @pre_load() + def strip_whitespace( # pylint: disable=no-self-use, unused-argument + self, items: MutableMapping, **kwargs: Any + ) -> MutableMapping: + for key, value in items.items(): + if isinstance(value, str): + items[key] = value.strip() + return items + + +class Data(Schema, StripWhitespaceMixin): + pass + + +class SurveyMetadata(Schema, StripWhitespaceMixin): + data = fields.Nested(Data, unknown=INCLUDE, validate=validate.Length(min=1)) + receipting_keys = fields.List(fields.String) + + @validates_schema + def validate_receipting_keys( # pylint: disable=no-self-use, unused-argument + self, data: Mapping, **kwargs: Any + ) -> None: + if data and (receipting_keys := data.get("receipting_keys", {})): + missing_receipting_keys = [ + receipting_key + for receipting_key in receipting_keys + if receipting_key not in data.get("data", {}) + ] + + if missing_receipting_keys: + receipting_keys_error_message = f"Receipting keys: {missing_receipting_keys} not set in Survey Metadata" + raise ValidationError(receipting_keys_error_message) + + +class RunnerMetadataSchema(Schema, StripWhitespaceMixin): + """Metadata which is required for the operation of runner itself""" + + METADATA_OPTION_ERROR_MESSAGE = ( + "Neither schema_name, schema_url or cir_instrument_id has been set in metadata" + ) + + jti = VALIDATORS["uuid"]() + tx_id = VALIDATORS["uuid"]() + case_id = VALIDATORS["uuid"]() + collection_exercise_sid = VALIDATORS["string"](validate=validate.Length(min=1)) + version = VALIDATORS["string"]( + required=True, validate=validate.OneOf([AuthPayloadVersion.V2.value]) + ) + schema_name = VALIDATORS["string"](required=False) + schema_url = VALIDATORS["url"](required=False) + cir_instrument_id = VALIDATORS["uuid"](required=False) + response_id = VALIDATORS["string"](required=True) + account_service_url = VALIDATORS["url"](required=True) + + language_code = VALIDATORS["string"](required=False) + channel = VALIDATORS["string"](required=False, validate=validate.Length(min=1)) + response_expires_at = VALIDATORS["iso_8601_date_string"]( + required=True, + validate=lambda x: parse_iso_8601_datetime(x) > datetime.now(tz=timezone.utc), + ) + region_code = VALIDATORS["string"](required=False, validate=RegionCode()) + + roles = fields.List(fields.String(), required=False) + survey_metadata = fields.Nested(SurveyMetadata, required=False) + + @validates_schema + def validate_schema_options( # pylint: disable=unused-argument + self, data: Mapping, **kwargs: Any + ) -> None: + if data: + options = [ + option + for option in ["schema_name", "schema_url", "cir_instrument_id"] + if data.get(option) + ] + if len(options) == 0: + raise ValidationError(self.METADATA_OPTION_ERROR_MESSAGE) + if len(options) > 1: + metadata_combination_error_message = ( + "Only one of schema_name, schema_url or cir_instrument_id should be specified " + f"in metadata, but {', '.join(options)} were provided" + ) + raise ValidationError(metadata_combination_error_message) + + +def validate_questionnaire_claims( + claims: Mapping, + questionnaire_specific_metadata: Iterable[Mapping], + unknown: str = EXCLUDE, +) -> dict: + """Validate any survey specific claims required for a questionnaire""" + dynamic_fields: dict[str, fields.String | DateString] = {} + + for metadata_field in questionnaire_specific_metadata: + field_arguments: dict[str, bool] = {} + validators: list[validate.Validator] = [] + + if metadata_field.get("optional"): + field_arguments["required"] = False + + if any( + length_limit in metadata_field + for length_limit in ("min_length", "max_length", "length") + ): + validators.append( + validate.Length( + min=metadata_field.get("min_length"), + max=metadata_field.get("max_length"), + equal=metadata_field.get("length"), + ) + ) + + dynamic_fields[metadata_field["name"]] = VALIDATORS[metadata_field["type"]]( + validate=validators, **field_arguments + ) + + questionnaire_metadata_schema = type( + "QuestionnaireMetadataSchema", (Schema, StripWhitespaceMixin), dynamic_fields + )(unknown=unknown) + + # The load method performs validation. + # Type ignore: the load method in the Marshmallow parent schema class doesn't have type hints for return + return questionnaire_metadata_schema.load(claims) # type: ignore + + +def validate_runner_claims_v2(claims: Mapping) -> dict: + """Validate claims required for runner to function""" + runner_metadata_schema = RunnerMetadataSchema(unknown=EXCLUDE) + # Type ignore: the load method in the Marshmallow parent schema class doesn't have type hints for return + return runner_metadata_schema.load(claims) # type: ignore diff --git a/app/utilities/metadata_validators.py b/app/utilities/metadata_validators.py new file mode 100644 index 0000000000..0704fd1ce9 --- /dev/null +++ b/app/utilities/metadata_validators.py @@ -0,0 +1,39 @@ +from typing import Any + +from marshmallow import fields, validate + + +class RegionCode(validate.Regexp): + """A region code defined as per ISO 3166-2:GB + Currently, this does not validate the subdivision, but only checks length + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__("^GB-[A-Z]{3}$", *args, **kwargs) + + +class UUIDString(fields.UUID): + """Currently, runner cannot handle UUID objects in metadata + Since all metadata is serialized and deserialized to JSON. + This custom field deserializes UUIDs to strings. + """ + + def _deserialize(self, *args: Any, **kwargs: Any) -> str: # type: ignore + return str(super()._deserialize(*args, **kwargs)) + + +class DateString(fields.DateTime): + """Currently, runner cannot handle Date objects in metadata + Since all metadata is serialized and deserialized to JSON. + This custom field deserializes Dates to strings. + """ + + DEFAULT_FORMAT = "iso8601" + + def _deserialize(self, *args: Any, **kwargs: Any) -> str: # type: ignore + date = super()._deserialize(*args, **kwargs) + date_format = self.format or self.DEFAULT_FORMAT + if date_format == "iso8601": + return date.isoformat() + + return date.strftime(date_format) diff --git a/app/utilities/request_session.py b/app/utilities/request_session.py new file mode 100644 index 0000000000..48d8cb5457 --- /dev/null +++ b/app/utilities/request_session.py @@ -0,0 +1,23 @@ +from typing import Sequence + +import requests +from requests.adapters import HTTPAdapter +from urllib3 import Retry + + +def get_retryable_session( + max_retries: int, retry_status_codes: Sequence[int], backoff_factor: float +) -> requests.Session: + session = requests.Session() + + retries = Retry( + total=max_retries, + status_forcelist=retry_status_codes, + ) # Codes to retry according to Google Docs https://cloud.google.com/storage/docs/retry-strategy#client-libraries + + retries.backoff_factor = backoff_factor + + session.mount("http://", HTTPAdapter(max_retries=retries)) + session.mount("https://", HTTPAdapter(max_retries=retries)) + + return session diff --git a/app/utilities/schema.py b/app/utilities/schema.py index 4598e084e4..a6bfb30638 100644 --- a/app/utilities/schema.py +++ b/app/utilities/schema.py @@ -1,26 +1,52 @@ import os -import time from functools import lru_cache from glob import glob from pathlib import Path -from typing import Mapping, Optional +from typing import Any +from urllib.parse import urlencode -import requests +from flask import current_app +from requests import RequestException from structlog import get_logger -from werkzeug.exceptions import NotFound +from app.data_models.metadata_proxy import MetadataProxy from app.questionnaire.questionnaire_schema import ( DEFAULT_LANGUAGE_CODE, QuestionnaireSchema, ) +from app.settings import CIR_OAUTH2_CLIENT_ID +from app.utilities.credentials import fetch_and_apply_oidc_credentials from app.utilities.json import json_load, json_loads +from app.utilities.request_session import get_retryable_session logger = get_logger() SCHEMA_DIR = "schemas" LANGUAGE_CODES = ("en", "cy") +CIR_RETRIEVE_COLLECTION_INSTRUMENT_URL = "/v2/retrieve_collection_instrument" -LANGUAGES_MAP = {"test_language": [["en", "cy"]]} +LANGUAGES_MAP = { + "test_language": [["en", "cy"]], + "cris_0001": [["en", "cy"]], + "phm_0001": [["en", "cy"]], +} + +SCHEMA_REQUEST_BACKOFF_FACTOR = 0.2 +SCHEMA_REQUEST_MAX_RETRIES = 2 # Totals no. of request should be 3. The initial request + SCHEMA_REQUEST_MAX_RETRIES +SCHEMA_REQUEST_TIMEOUT = 3 +SCHEMA_REQUEST_RETRY_STATUS_CODES = [ + 408, + 429, + 500, + 502, + 503, + 504, +] + + +class SchemaRequestFailed(Exception): + def __str__(self) -> str: + return str("schema request failed") @lru_cache(maxsize=None) @@ -36,7 +62,7 @@ def get_schema_list(language_code: str = DEFAULT_LANGUAGE_CODE) -> dict[str, lis @lru_cache(maxsize=None) -def get_schema_path(language_code, schema_name): +def get_schema_path(language_code: str, schema_name: str) -> str | None: for schemas_by_language in get_schema_path_map(include_test_schemas=True).values(): schema_path = schemas_by_language.get(language_code, {}).get(schema_name) if schema_path: @@ -44,8 +70,10 @@ def get_schema_path(language_code, schema_name): @lru_cache(maxsize=None) -def get_schema_path_map(include_test_schemas: Optional[bool] = False) -> Mapping: - schemas = {} +def get_schema_path_map( + include_test_schemas: bool = False, +) -> dict[str, dict[str, dict[str, str]]]: + schemas: dict[str, dict[str, dict[str, str]]] = {} for survey_type in os.listdir(SCHEMA_DIR): if not include_test_schemas and survey_type == "test": continue @@ -63,7 +91,7 @@ def get_schema_path_map(include_test_schemas: Optional[bool] = False) -> Mapping return schemas -def _schema_exists(language_code, schema_name): +def _schema_exists(language_code: str, schema_name: str) -> bool: schema_path_map = get_schema_path_map(include_test_schemas=True) return any( True @@ -73,64 +101,66 @@ def _schema_exists(language_code, schema_name): ) -def get_allowed_languages(schema_name, launch_language): - for language_combination in LANGUAGES_MAP.get(schema_name, []): - if launch_language in language_combination: - return language_combination +def get_allowed_languages(schema_name: str | None, launch_language: str) -> list[str]: + if schema_name: + for language_combination in LANGUAGES_MAP.get(schema_name, []): + if launch_language in language_combination: + return language_combination return [DEFAULT_LANGUAGE_CODE] -def load_schema_from_metadata(metadata): - if survey_url := metadata.get("survey_url"): - # :TODO: Remove before production uses survey_url - # This is temporary and is only for development/integration purposes. - # This should not be used in production. - - start = time.time() - schema = load_schema_from_url(survey_url, metadata.get("language_code")) - duration_in_milliseconds = (time.time() - start) * 1_000 - - cache_info = ( - load_schema_from_url.cache_info() # pylint: disable=no-value-for-parameter +def load_schema_from_metadata( + metadata: MetadataProxy, *, language_code: str | None +) -> QuestionnaireSchema: + if schema_url := metadata.schema_url: + return load_schema_from_url( + url=schema_url, + language_code=language_code, ) - logger.info( - f"load_schema_from_url took {duration_in_milliseconds:.6f} milliseconds", - survey_url=survey_url, - currsize=cache_info.currsize, - hits=cache_info.hits, - misses=cache_info.misses, - pid=os.getpid(), + + if cir_instrument_id := metadata.cir_instrument_id: + return load_schema_from_instrument_id( + cir_instrument_id=cir_instrument_id, language_code=language_code ) - return schema return load_schema_from_name( - metadata.get("schema_name"), language_code=metadata.get("language_code") + # Type ignore: Metadata is validated to have either schema_name or schema_url populated. + # This code runs only if schema_url was not present, thus schema_name is present (not None). + metadata.schema_name, # type: ignore + language_code=language_code, ) -def load_schema_from_session_data(session_data): - return load_schema_from_metadata(vars(session_data)) +def load_schema_from_name( + schema_name: str, language_code: str | None = DEFAULT_LANGUAGE_CODE +) -> QuestionnaireSchema: + language_code = language_code or DEFAULT_LANGUAGE_CODE + return _load_schema_from_name(schema_name, language_code) -def load_schema_from_name(schema_name, language_code=DEFAULT_LANGUAGE_CODE): - return _load_schema_from_name(schema_name, language_code) +def load_schema_from_instrument_id( + *, cir_instrument_id: str, language_code: str | None +) -> QuestionnaireSchema: + parameters = {"guid": cir_instrument_id} + cir_url = f"{current_app.config['CIR_API_BASE_URL']}{CIR_RETRIEVE_COLLECTION_INSTRUMENT_URL}?{urlencode(parameters)}" + return load_schema_from_url(url=cir_url, language_code=language_code, is_cir=True) @lru_cache(maxsize=None) -def _load_schema_from_name(schema_name, language_code): +def _load_schema_from_name(schema_name: str, language_code: str) -> QuestionnaireSchema: schema_json = _load_schema_file(schema_name, language_code) return QuestionnaireSchema(schema_json, language_code) -def get_schema_name_from_params(eq_id, form_type): +def get_schema_name_from_params(eq_id: str | None, form_type: str | None) -> str: return f"{eq_id}_{form_type}" -def _load_schema_file(schema_name, language_code): +def _load_schema_file(schema_name: str, language_code: str) -> Any: """ Load a schema, optionally for a specified language. - :param schema_name: The name of the schema e.g. census_household + :param schema_name: The name of the schema e.g. test_address :param language_code: ISO 2-character code for language e.g. 'en', 'cy' """ if language_code != DEFAULT_LANGUAGE_CODE and not _schema_exists( @@ -160,40 +190,68 @@ def _load_schema_file(schema_name, language_code): schema_path=schema_path, ) - with open(schema_path, encoding="utf8") as json_file: + # Type ignore: Existence of the file is checked prior to call for the path + with open(schema_path, encoding="utf8") as json_file: # type: ignore return json_load(json_file) @lru_cache(maxsize=None) -def load_schema_from_url(survey_url, language_code): +def load_schema_from_url( + url: str, *, language_code: str | None, is_cir: bool = False +) -> QuestionnaireSchema: + """ + Fetches a schema from the provided url. + The caller is responsible for including any required query parameters in the url + """ language_code = language_code or DEFAULT_LANGUAGE_CODE pid = os.getpid() logger.info( "loading schema from URL", - survey_url=survey_url, + schema_url=url, language_code=language_code, pid=pid, ) - constructed_survey_url = f"{survey_url}?language={language_code}" + session = get_retryable_session( + max_retries=SCHEMA_REQUEST_MAX_RETRIES, + retry_status_codes=SCHEMA_REQUEST_RETRY_STATUS_CODES, + backoff_factor=SCHEMA_REQUEST_BACKOFF_FACTOR, + ) - req = requests.get(constructed_survey_url) - schema_response = req.content.decode() - response_duration_in_milliseconds = req.elapsed.total_seconds() * 1000 + if is_cir: + # Type ignore: CIR_OAUTH2_CLIENT_ID is an env var which must exist as it is verified in setup.py + fetch_and_apply_oidc_credentials(session=session, client_id=CIR_OAUTH2_CLIENT_ID) # type: ignore - logger.info( - f"schema request took {response_duration_in_milliseconds:.2f} milliseconds", - pid=pid, - ) + try: + req = session.get(url, timeout=SCHEMA_REQUEST_TIMEOUT) + except RequestException as exc: + logger.exception( + "schema request errored", + schema_url=url, + ) + raise SchemaRequestFailed from exc - if req.status_code == 404: - logger.error("no schema exists", survey_url=constructed_survey_url) - raise NotFound + if req.status_code == 200: + schema_response = req.content.decode() + response_duration_in_milliseconds = req.elapsed.total_seconds() * 1000 + + logger.info( + f"schema request took {response_duration_in_milliseconds:.2f} milliseconds", + pid=pid, + ) + + return QuestionnaireSchema(json_loads(schema_response), language_code) + + logger.error( + "got a non-200 response for schema url request", + status_code=req.status_code, + schema_url=url, + ) - return QuestionnaireSchema(json_loads(schema_response), language_code) + raise SchemaRequestFailed -def cache_questionnaire_schemas(): +def cache_questionnaire_schemas() -> None: for schemas_by_language in get_schema_path_map().values(): for language_code, schemas in schemas_by_language.items(): for schema in schemas: diff --git a/app/utilities/strings.py b/app/utilities/strings.py index 7145023f9b..f58ce64e10 100644 --- a/app/utilities/strings.py +++ b/app/utilities/strings.py @@ -1,8 +1,7 @@ import re -from typing import Union -def to_bytes(bytes_or_str: Union[bytes, str]) -> bytes: +def to_bytes(bytes_or_str: bytes | str) -> bytes: """ Converts supplied data into bytes if the data is of type str. :param bytes_or_str: Data to be converted. @@ -13,7 +12,7 @@ def to_bytes(bytes_or_str: Union[bytes, str]) -> bytes: return bytes_or_str -def to_str(bytes_or_str: Union[bytes, str]) -> str: +def to_str(bytes_or_str: bytes | str) -> str: """ Converts supplied data into a UTF-8 encoded string if the data is of type bytes. :param bytes_or_str: Data to be converted. @@ -38,3 +37,10 @@ def safe_content(content: str) -> str: # Strip HTML Tags content = re.sub(r"]+>", "", content) return content + + +def pascal_case_to_hyphenated_lowercase(string: str) -> str: + """ + Changes text from PascalCase to hyphenated-lowercase + """ + return re.sub(r"(? None: + if not (isinstance(identifier, str) and identifier.strip()) and not ( + isinstance(identifier, int) and identifier >= 0 + ): + raise ValidationError(self.ITEM_IDENTIFIER_ERROR_MESSAGE) + + +class ItemsData(Schema, StripWhitespaceMixin): + pass + + +class SupplementaryData(Schema, StripWhitespaceMixin): + SDS_IDENTIFIER_ERROR_MESSAGE = ( + "Supplementary data did not return the specified Identifier" + ) + + identifier = VALIDATORS["string"](validate=validate.Length(min=1)) + items = fields.Nested(ItemsData, required=False, unknown=INCLUDE) + + @validates_schema() + def validate_identifier( # pylint: disable=unused-argument + self, data: Mapping, **kwargs: Any + ) -> None: + if data and data["identifier"] != self.context["identifier"]: + raise ValidationError(self.SDS_IDENTIFIER_ERROR_MESSAGE) + + +class SupplementaryDataMetadataSchema(Schema, StripWhitespaceMixin): + + DATASET_ID_ERROR_MESSAGE = ( + "Supplementary data did not return the specified Dataset ID" + ) + SURVEY_ID_ERROR_MESSAGE = ( + "Supplementary data did not return the specified Survey ID" + ) + SDS_VERSION_ERROR_MESSAGE = "The Supplementary Dataset Schema Version does not match the version set in the Questionnaire Schema" + + dataset_id = VALIDATORS["uuid"]() + survey_id = VALIDATORS["string"](validate=validate.Length(min=1)) + data = fields.Nested( + SupplementaryData, + required=True, + unknown=INCLUDE, + validate=validate.Length(min=1), + ) + + @validates_schema() + def validate_payload( # pylint: disable=unused-argument + self, payload: Mapping, **kwargs: Any + ) -> None: + if payload: + if payload["dataset_id"] != self.context["dataset_id"]: + raise ValidationError(self.DATASET_ID_ERROR_MESSAGE) + + if payload["survey_id"] != self.context["survey_id"]: + raise ValidationError(self.SURVEY_ID_ERROR_MESSAGE) + + if self.context["sds_schema_version"] and ( + payload["data"]["schema_version"] != self.context["sds_schema_version"] + ): + raise ValidationError(self.SDS_VERSION_ERROR_MESSAGE) + + +def validate_supplementary_data_v1( + supplementary_data: Mapping, + dataset_id: str, + identifier: str, + survey_id: str, + sds_schema_version: str | None = None, +) -> dict[str, str | dict | int | list]: + """Validate claims required for supplementary data""" + supplementary_data_metadata_schema = SupplementaryDataMetadataSchema( + unknown=INCLUDE + ) + supplementary_data_metadata_schema.context = { + "dataset_id": dataset_id, + "identifier": identifier, + "survey_id": survey_id, + "sds_schema_version": sds_schema_version, + } + validated_supplementary_data = supplementary_data_metadata_schema.load( + supplementary_data + ) + + if supplementary_data_items := supplementary_data.get("data", {}).get("items"): + for key, values in supplementary_data_items.items(): + items = [ItemsSchema(unknown=INCLUDE).load(value) for value in values] + validated_supplementary_data["data"]["items"][key] = items + + # Type ignore: the load method in the Marshmallow parent schema class doesn't have type hints for return + return validated_supplementary_data # type: ignore diff --git a/app/utilities/types.py b/app/utilities/types.py new file mode 100644 index 0000000000..2d159b736b --- /dev/null +++ b/app/utilities/types.py @@ -0,0 +1,70 @@ +from typing import TYPE_CHECKING, NamedTuple, TypeAlias, TypedDict, Union + +if TYPE_CHECKING: + from app.forms.validators import ( # pragma: no cover + DateCheck, + DateRequired, + OptionalForm, + SingleDatePeriodCheck, + ) + from app.questionnaire.location import Location # pragma: no cover + from app.questionnaire.relationship_location import ( + RelationshipLocation, # pragma: no cover + ) + +LocationType: TypeAlias = Union["Location", "RelationshipLocation"] # noqa: UP007 +SupplementaryDataKeyType: TypeAlias = tuple[str, str | None] +SupplementaryDataValueType: TypeAlias = dict | str | list | None + +DateValidatorType: TypeAlias = Union[ + "OptionalForm", "DateRequired", "DateCheck", "SingleDatePeriodCheck" +] # noqa: UP007 + +ChoiceType: TypeAlias = Union["Choice", "ChoiceWithDetailAnswer"] # noqa: UP007 +ChoiceWidgetRenderType: TypeAlias = tuple[str, str, bool, str | None] + + +class SectionKeyDict(TypedDict): + section_id: str + list_item_id: str | None + + +class SectionKey(NamedTuple): + section_id: str + list_item_id: str | None = None + + def to_dict(self) -> SectionKeyDict: + return SectionKeyDict( + section_id=self.section_id, list_item_id=self.list_item_id + ) + + +class DependentSection(NamedTuple): + """ + The 'is_complete' property is used when updating the progress of the section. If the value is 'None' + then the routing path for this section will be re-evaluated to determine if it is complete. + """ + + section_id: str + list_item_id: str | None = None + is_complete: bool | None = None + + @property + def section_key(self) -> SectionKey: + return SectionKey(self.section_id, self.list_item_id) + + +class SupplementaryDataListMapping(TypedDict): + identifier: str | int + list_item_id: str + + +class Choice(NamedTuple): + value: str + label: str + + +class ChoiceWithDetailAnswer(NamedTuple): + value: str + label: str + detail_answer_id: str | None diff --git a/app/views/contexts/__init__.py b/app/views/contexts/__init__.py index 6477a330bc..3233bec3e7 100644 --- a/app/views/contexts/__init__.py +++ b/app/views/contexts/__init__.py @@ -1,15 +1,19 @@ -from .calculated_summary_context import CalculatedSummaryContext -from .context import Context -from .hub_context import HubContext -from .list_context import ListContext -from .section_summary_context import SectionSummaryContext -from .submit_questionnaire_context import SubmitQuestionnaireContext +from app.views.contexts.calculated_summary_context import CalculatedSummaryContext +from app.views.contexts.context import Context +from app.views.contexts.grand_calculated_summary_context import ( + GrandCalculatedSummaryContext, +) +from app.views.contexts.hub_context import HubContext +from app.views.contexts.list_context import ListContext +from app.views.contexts.section_summary_context import SectionSummaryContext +from app.views.contexts.submit_questionnaire_context import SubmitQuestionnaireContext __all__ = [ "CalculatedSummaryContext", "Context", - "SubmitQuestionnaireContext", + "GrandCalculatedSummaryContext", "HubContext", "ListContext", "SectionSummaryContext", + "SubmitQuestionnaireContext", ] diff --git a/app/views/contexts/calculated_summary_context.py b/app/views/contexts/calculated_summary_context.py index e57f0b1119..b5f13f543a 100644 --- a/app/views/contexts/calculated_summary_context.py +++ b/app/views/contexts/calculated_summary_context.py @@ -1,85 +1,160 @@ -from copy import deepcopy +from typing import Callable, Iterable, Mapping -from app.jinja_filters import ( - format_number, - format_percentage, - format_unit, - get_formatted_currency, +from werkzeug.datastructures import ImmutableDict + +from app.data_models.data_stores import DataStores +from app.jinja_filters import format_number, format_percentage, format_unit +from app.questionnaire.questionnaire_schema import ( + QuestionnaireSchema, + get_calculated_summary_answer_ids, ) -from app.questionnaire.location import Location -from app.questionnaire.questionnaire_schema import QuestionnaireSchema +from app.questionnaire.return_location import ReturnLocation +from app.questionnaire.routing_path import RoutingPath +from app.questionnaire.rules.rule_evaluator import RuleEvaluator from app.questionnaire.schema_utils import get_answer_ids_in_block from app.questionnaire.value_source_resolver import ValueSourceResolver from app.questionnaire.variants import choose_question_to_display, transform_variants +from app.utilities.decimal_places import get_formatted_currency +from app.utilities.strings import pascal_case_to_hyphenated_lowercase +from app.utilities.types import LocationType from app.views.contexts.context import Context +from app.views.contexts.summary.calculated_summary_block import NumericType from app.views.contexts.summary.group import Group class CalculatedSummaryContext(Context): - def build_groups_for_section(self, section): - routing_path = self._router.routing_path(section["id"]) - - location = Location(section["id"]) + def __init__( + self, + language: str, + schema: QuestionnaireSchema, + data_stores: DataStores, + routing_path: RoutingPath, + current_location: LocationType, + return_location: ReturnLocation, + rendered_block: dict, + ) -> None: + super().__init__( + language, + schema, + data_stores, + ) + self._data_stores = data_stores + self.routing_path_block_ids = routing_path.block_ids + self.current_location = current_location + self.return_location = return_location + self.rendered_block = rendered_block + def build_groups_for_section( + self, + *, + section: Mapping, + routing_path_block_ids: Iterable[str], + ) -> list[Mapping]: + """ + If the calculated summary is being edited from a grand calculated summary + the details of the grand calculated summary to return to needs to be passed down to the calculated summary answer links + """ + return_to = "calculated-summary" + # Type ignore: safe to assume block_id is not None + return_to_block_id: str = self.current_location.block_id # type: ignore + if self.return_location.return_to == "grand-calculated-summary": + return_to_block_id += f",{self.return_location.return_to_block_id}" + return_to += ",grand-calculated-summary" return [ Group( - group, - routing_path, - self._answer_store, - self._list_store, - self._metadata, - self._response_metadata, - self._schema, - location, - self._language, - return_to="final-summary", + group_schema=group, + routing_path_block_ids=routing_path_block_ids, + schema=self._schema, + data_stores=self._data_stores, + location=self.current_location, + language=self._language, + summary_type="CalculatedSummary", + return_location=ReturnLocation( + return_to=return_to, + return_to_block_id=return_to_block_id, + return_to_list_item_id=self.return_location.return_to_list_item_id, + return_to_answer_id=( + self.return_location.return_to_answer_id + if self.return_location.return_to == "grand-calculated-summary" + else None + ), + ), ).serialize() for group in section["groups"] ] - def build_view_context_for_calculated_summary(self, current_location): - block = self._schema.get_block(current_location.block_id) - - calculated_section = self._build_calculated_summary_section( - block, current_location + def build_view_context(self) -> dict[str, dict]: + calculated_section: dict = self._build_calculated_summary_section( + self.rendered_block ) - calculation = block["calculation"] + calculation = self.rendered_block["calculation"] - groups = self.build_groups_for_section(calculated_section) + groups = self.build_groups_for_section( + section=calculated_section, + routing_path_block_ids=self.routing_path_block_ids, + ) formatted_total = self._get_formatted_total( - groups or [], - current_location=current_location, - calculation_operator=ValueSourceResolver.get_calculation_operator( - calculation["calculation_type"] + groups=groups or [], + calculation=( + ValueSourceResolver.get_calculation_operator( + calculation["calculation_type"] + ) + if calculation.get("answers_to_calculate") + else calculation["operation"] ), ) - context = { + return self._build_formatted_summary( + groups=groups, calculation=calculation, formatted_total=formatted_total + ) + + def _build_formatted_summary( + self, + *, + groups: Iterable[Mapping], + calculation: Mapping, + formatted_total: str, + ) -> dict[str, dict]: + collapsible = self.rendered_block.get("collapsible") or False + block_title = self.rendered_block["title"] + + sections = [{"id": self.current_location.section_id, "groups": groups}] + + return { "summary": { - "groups": groups, + "sections": sections, "answers_are_editable": True, "calculated_question": self._get_calculated_question( - calculation, formatted_total + calculation_question=calculation, + formatted_total=formatted_total, ), - "title": block.get("title") % dict(total=formatted_total), - "collapsible": block.get("collapsible", False), - "summary_type": "CalculatedSummary", + "title": block_title % {"total": formatted_total}, + "collapsible": collapsible, + "summary_type": self.rendered_block["type"], } } - return context - - def _build_calculated_summary_section(self, rendered_block, current_location): + def _build_calculated_summary_section(self, rendered_block: Mapping) -> dict: """Build up the list of blocks only including blocks / questions / answers which are relevant to the summary""" - section_id = self._schema.get_section_id_for_block_id(current_location.block_id) - group = self._schema.get_group_for_block_id(current_location.block_id) + # type ignores added as block will exist at this point + block_id: str = self.current_location.block_id # type: ignore + group: ImmutableDict = self._schema.get_group_for_block_id(block_id) # type: ignore + # type ignores it is not valid to not have a section at this point + section_id: str = self._schema.get_section_id_for_block_id(block_id) # type: ignore + blocks = [] - answers_to_calculate = rendered_block["calculation"]["answers_to_calculate"] - blocks_to_calculate = [ - self._schema.get_block_for_answer_id(answer_id) + if rendered_block["calculation"].get("answers_to_calculate"): + answers_to_calculate = rendered_block["calculation"]["answers_to_calculate"] + else: + answers_to_calculate = get_calculated_summary_answer_ids(rendered_block) + + blocks_to_calculate: list[ImmutableDict] = [ + # Type ignore: the answer blocks will always exist at this point + self._schema.get_block_for_answer_id(answer_id) # type: ignore for answer_id in answers_to_calculate ] + unique_blocks = list( {block["id"]: block for block in blocks_to_calculate}.values() ) @@ -87,7 +162,7 @@ def _build_calculated_summary_section(self, rendered_block, current_location): for block in unique_blocks: if QuestionnaireSchema.is_question_block_type(block["type"]): transformed_block = self._remove_unwanted_questions_answers( - block, answers_to_calculate, current_location=current_location + block, answers_to_calculate ) if set(get_answer_ids_in_block(transformed_block)) & set( answers_to_calculate @@ -97,22 +172,20 @@ def _build_calculated_summary_section(self, rendered_block, current_location): return {"id": section_id, "groups": [{"id": group["id"], "blocks": blocks}]} def _remove_unwanted_questions_answers( - self, block, answer_ids_to_keep, current_location - ): + self, block: ImmutableDict, answer_ids_to_keep: Iterable[str] + ) -> dict: """ Evaluates questions in a block and removes any questions not containing a relevant answer """ - transformed_block = transform_variants( + block_to_transform: ImmutableDict = transform_variants( block, self._schema, - self._metadata, - self._response_metadata, - self._answer_store, - self._list_store, - current_location=current_location, + self._data_stores, + self.current_location, + ) + transformed_block: dict = QuestionnaireSchema.get_mutable_deepcopy( + block_to_transform ) - transformed_block = deepcopy(transformed_block) - transformed_block = QuestionnaireSchema.get_mutable_deepcopy(transformed_block) block_question = transformed_block["question"] matching_answers = [] @@ -124,28 +197,66 @@ def _remove_unwanted_questions_answers( ] if block_question["id"] in questions_to_keep: - answers_to_keep = [ - answer - for answer in block_question["answers"] - if answer["id"] in answer_ids_to_keep - ] - block_question["answers"] = answers_to_keep + if answers := block_question.get("answers"): + answers_to_keep = [ + answer for answer in answers if answer["id"] in answer_ids_to_keep + ] + block_question["answers"] = answers_to_keep + if dynamic_answers := block_question.get("dynamic_answers"): + dynamic_answers_to_keep = [ + answer + for answer in dynamic_answers["answers"] + if answer["id"] in answer_ids_to_keep + ] + block_question["dynamic_answers"]["answers"] = dynamic_answers_to_keep return transformed_block - def _get_formatted_total(self, groups, current_location, calculation_operator): - values_to_calculate = [] - answer_format = {"type": None} + def _get_evaluated_total( + self, + *, + calculation: Mapping, + routing_path_block_ids: Iterable[str], + ) -> NumericType: + """ + For a calculation in the new style and the list of involved block ids (possibly across sections) evaluate the total + """ + evaluate_calculated_summary = RuleEvaluator( + data_stores=self._data_stores, + schema=self._schema, + routing_path_block_ids=routing_path_block_ids, + location=self.current_location, + ) + # Type ignore: in the case of a calculated summation it will always be a numeric type + calculated_total: NumericType = evaluate_calculated_summary.evaluate(calculation) # type: ignore + return calculated_total + + def _get_formatted_total( + self, groups: list, calculation: Callable | ImmutableDict + ) -> str: + answer_format, values_to_calculate = self._get_answer_format(groups) + + if isinstance(calculation, Mapping): + calculated_total = self._get_evaluated_total( + calculation=calculation, + routing_path_block_ids=self.routing_path_block_ids, + ) + else: + calculated_total = calculation(values_to_calculate) + + return self._format_total(answer_format=answer_format, total=calculated_total) + + def _get_answer_format(self, groups: Iterable[Mapping]) -> tuple[dict, list]: + values_to_calculate: list = [] + answer_format: dict = {"type": None} + decimal_limits: list[int] = [] for group in groups: for block in group["blocks"]: question = choose_question_to_display( block, self._schema, - self._metadata, - self._response_metadata, - self._answer_store, - self._list_store, - current_location=current_location, + self._data_stores, + current_location=self.current_location, ) for answer in question["answers"]: if not answer_format["type"]: @@ -155,29 +266,48 @@ def _get_formatted_total(self, groups, current_location, calculation_operator): "unit_length": answer.get("unit_length"), "currency": answer.get("currency"), } + + if (decimal_places := answer.get("decimal_places")) is not None: + decimal_limits.append(decimal_places) + answer_value = answer.get("value") or 0 values_to_calculate.append(answer_value) + answer_format["decimal_places"] = max(decimal_limits, default=None) + return answer_format, values_to_calculate - calculated_total = calculation_operator(values_to_calculate) + @staticmethod + def _format_total( + *, + answer_format: Mapping, + total: NumericType, + ) -> str: if answer_format["type"] == "currency": - return get_formatted_currency(calculated_total, answer_format["currency"]) + return get_formatted_currency( + value=total, + currency=answer_format["currency"], + decimal_limit=answer_format.get("decimal_places"), + ) if answer_format["type"] == "unit": return format_unit( - answer_format["unit"], calculated_total, answer_format["unit_length"] + answer_format["unit"], + total, + answer_format["unit_length"], ) if answer_format["type"] == "percentage": - return format_percentage(calculated_total) + return format_percentage(total) - return format_number(calculated_total) + return format_number(total) - @staticmethod - def _get_calculated_question(calculation_question, formatted_total): - calculation_title = calculation_question.get("title") + def _get_calculated_question( + self, *, calculation_question: Mapping, formatted_total: str + ) -> dict: + calculation_title = calculation_question["title"] + block_type = pascal_case_to_hyphenated_lowercase(self.rendered_block["type"]) return { "title": calculation_title, - "id": "calculated-summary-question", - "answers": [{"id": "calculated-summary-answer", "value": formatted_total}], + "id": f"{block_type}-question", + "answers": [{"id": f"{block_type}-answer", "value": formatted_total}], } diff --git a/app/views/contexts/confirm_email_context.py b/app/views/contexts/confirm_email_context.py index f6e66426e6..489ae5cafe 100644 --- a/app/views/contexts/confirm_email_context.py +++ b/app/views/contexts/confirm_email_context.py @@ -1,9 +1,15 @@ +from typing import Mapping + from flask import url_for +from app.forms.questionnaire_form import QuestionnaireForm +from app.questionnaire import QuestionSchemaType from app.views.contexts.question import build_question_context -def build_confirm_email_context(question_schema, form): +def build_confirm_email_context( + question_schema: QuestionSchemaType, form: QuestionnaireForm +) -> dict[str, Mapping]: block = {"question": question_schema} context = build_question_context(block, form) context["hide_sign_out_button"] = False diff --git a/app/views/contexts/context.py b/app/views/contexts/context.py index b137a4391d..dadde15ee8 100644 --- a/app/views/contexts/context.py +++ b/app/views/contexts/context.py @@ -1,9 +1,6 @@ from abc import ABC -from typing import Any, Mapping -from app.data_models.answer_store import AnswerStore -from app.data_models.list_store import ListStore -from app.data_models.progress_store import ProgressStore +from app.data_models.data_stores import DataStores from app.questionnaire.placeholder_renderer import PlaceholderRenderer from app.questionnaire.questionnaire_schema import QuestionnaireSchema from app.questionnaire.router import Router @@ -14,34 +11,19 @@ def __init__( self, language: str, schema: QuestionnaireSchema, - answer_store: AnswerStore, - list_store: ListStore, - progress_store: ProgressStore, - metadata: Mapping[str, Any], - response_metadata: Mapping, - ): + data_stores: DataStores, + placeholder_preview_mode: bool = False, + ) -> None: self._language = language self._schema = schema - self._answer_store = answer_store - self._list_store = list_store - self._progress_store = progress_store - self._metadata = metadata - self._response_metadata = response_metadata + self._data_stores = data_stores + self._placeholder_preview_mode = placeholder_preview_mode - self._router = Router( - self._schema, - self._answer_store, - self._list_store, - self._progress_store, - self._metadata, - self._response_metadata, - ) + self._router = Router(schema=self._schema, data_stores=self._data_stores) self._placeholder_renderer = PlaceholderRenderer( + data_stores=data_stores, language=self._language, - answer_store=self._answer_store, - list_store=self._list_store, - metadata=self._metadata, - response_metadata=self._response_metadata, schema=self._schema, + placeholder_preview_mode=self._placeholder_preview_mode, ) diff --git a/app/views/contexts/email_form_context.py b/app/views/contexts/email_form_context.py index 1b1e21716c..4c57f1eb49 100644 --- a/app/views/contexts/email_form_context.py +++ b/app/views/contexts/email_form_context.py @@ -1,20 +1,23 @@ +from typing import Any + from flask import url_for +from app.forms.email_form import EmailForm + -def build_confirmation_email_form_context(email_confirmation_form): - context = { +def build_confirmation_email_form_context( + email_confirmation_form: EmailForm, +) -> dict[str, bool | str | Any]: + return { "hide_sign_out_button": False, "sign_out_url": url_for("session.get_sign_out"), + "form": build_email_form_context(email_confirmation_form), } - context.update(build_email_form_context(email_confirmation_form)) - return context -def build_email_form_context(email_confirmation_form): +def build_email_form_context(email_confirmation_form: EmailForm) -> dict[str, Any]: return { - "form": { - "mapped_errors": email_confirmation_form.map_errors(), - "email_field": email_confirmation_form.email, - "errors": email_confirmation_form.errors, - } + "mapped_errors": email_confirmation_form.map_errors(), + "email_field": email_confirmation_form.email, + "errors": email_confirmation_form.errors, } diff --git a/app/views/contexts/feedback_form_context.py b/app/views/contexts/feedback_form_context.py index 4a91fadabc..2757d3d831 100644 --- a/app/views/contexts/feedback_form_context.py +++ b/app/views/contexts/feedback_form_context.py @@ -1,13 +1,13 @@ -from typing import Mapping, Union - from flask import url_for +from app.forms.questionnaire_form import QuestionnaireForm +from app.questionnaire import QuestionSchemaType from app.views.contexts.question import build_question_context def build_feedback_context( - question_schema, form -) -> Mapping[str, Union[str, bool, dict]]: + question_schema: QuestionSchemaType, form: QuestionnaireForm +) -> dict[str, str | bool | dict]: block = {"question": question_schema} context = build_question_context(block, form) context["hide_sign_out_button"] = False diff --git a/app/views/contexts/grand_calculated_summary_context.py b/app/views/contexts/grand_calculated_summary_context.py new file mode 100644 index 0000000000..daec4ccd80 --- /dev/null +++ b/app/views/contexts/grand_calculated_summary_context.py @@ -0,0 +1,129 @@ +from typing import Iterable, Mapping + +from werkzeug.datastructures import ImmutableDict + +from app.questionnaire.location import SectionKey +from app.questionnaire.questionnaire_schema import ( + get_calculated_summary_ids_for_grand_calculated_summary, +) +from app.questionnaire.return_location import ReturnLocation +from app.views.contexts.calculated_summary_context import CalculatedSummaryContext +from app.views.contexts.summary.group import Group + + +class GrandCalculatedSummaryContext(CalculatedSummaryContext): + def _build_grand_calculated_summary_section(self) -> dict[str, str | list]: + """ + Build list of calculated summary blocks that the grand calculated summary will be adding up + """ + # Type ignore: the block, group and section will all exist at this point + calculated_summary_group: ImmutableDict = self._schema.get_group_for_block_id( + self.current_location.block_id # type: ignore + ) + + calculated_summary_ids = ( + get_calculated_summary_ids_for_grand_calculated_summary(self.rendered_block) + ) + blocks_to_calculate = [ + self._schema.get_block(block_id) for block_id in calculated_summary_ids + ] + + return { + "id": self.current_location.section_id, + "groups": [ + {"id": calculated_summary_group["id"], "blocks": blocks_to_calculate} + ], + } + + def _blocks_on_routing_path( + self, calculated_summary_ids: Iterable[str] + ) -> list[str]: + """ + Find all blocks on the routing path for each of the calculated summaries + """ + # Type ignore: each block must have a section id + section_ids: set[str] = { + self._schema.get_section_id_for_block_id(block_id) # type: ignore + for block_id in calculated_summary_ids + } + # find any sections involved in the grand calculated summary (but only if they have started, to avoid evaluating the path if not necessary) + started_sections = [ + key + for key, _ in self._data_stores.progress_store.started_section_keys( + section_ids + ) + ] + routing_path_block_ids: list[str] = [] + + for section_id in started_sections: + if section_id == self.current_location.section_id: + routing_path_block_ids.extend(self.routing_path_block_ids) + else: + routing_path_block_ids.extend( + # repeating calculated summaries are not supported at the moment, so no list item is needed + self._router.routing_path(SectionKey(section_id)).block_ids + ) + + return routing_path_block_ids + + def build_groups_for_section( + self, + *, + section: Mapping, + routing_path_block_ids: Iterable[str], + ) -> list[Mapping]: + return [ + Group( + group_schema=group, + routing_path_block_ids=routing_path_block_ids, + data_stores=self._data_stores, + schema=self._schema, + location=self.current_location, + language=self._language, + summary_type="GrandCalculatedSummary", + return_location=ReturnLocation( + return_to="grand-calculated-summary", + return_to_block_id=self.current_location.block_id, + return_to_list_item_id=self.current_location.list_item_id, + ), + ).serialize() + for group in section["groups"] + ] + + def build_view_context(self) -> dict[str, dict]: + """ + Build summary section with formatted total and change links for each calculated summary + """ + calculation = self.rendered_block["calculation"] + calculated_summary_ids = ( + get_calculated_summary_ids_for_grand_calculated_summary(self.rendered_block) + ) + routing_path_block_ids = self._blocks_on_routing_path(calculated_summary_ids) + + calculated_section = self._build_grand_calculated_summary_section() + + groups = self.build_groups_for_section( + section=calculated_section, + routing_path_block_ids=routing_path_block_ids, + ) + total = self._get_evaluated_total( + calculation=calculation["operation"], + routing_path_block_ids=routing_path_block_ids, + ) + + # validator ensures all calculated summaries are of the same type, so the first can be used for the format + answer_format = self._schema.get_answer_format_for_calculated_summary( + calculated_summary_ids[0] + ) + answer_format["decimal_places"] = ( + self._schema.get_decimal_limit_from_calculated_summaries( + calculated_summary_ids + ) + ) + formatted_total = self._format_total(answer_format=answer_format, total=total) + + return self._build_formatted_summary( + groups=groups, + calculation=calculation, + formatted_total=formatted_total, + ) diff --git a/app/views/contexts/hub_context.py b/app/views/contexts/hub_context.py index d6834f2e1d..937f2cfb8e 100644 --- a/app/views/contexts/hub_context.py +++ b/app/views/contexts/hub_context.py @@ -1,11 +1,15 @@ from functools import cached_property -from typing import List, Mapping, Union +from typing import Any, Iterable, Mapping from flask import url_for from flask_babel import lazy_gettext +from werkzeug.datastructures import ImmutableDict -from app.data_models.progress_store import CompletionStatus -from app.views.contexts import Context +from app.data_models import CompletionStatus +from app.questionnaire.location import SectionKey +from app.views.contexts import Context # pylint: disable=cyclic-import + +# Removing Pylint disable causes linting fail in GHA but not locally this issue has been raised here: https://github.com/pylint-dev/pylint/issues/9168 class HubContext(Context): @@ -14,21 +18,21 @@ class HubContext(Context): "text": lazy_gettext("Completed"), "link": { "text": lazy_gettext("View answers"), - "aria_label": lazy_gettext("View answers for {section_name}"), + "aria_label": lazy_gettext("View answers: {section_name}"), }, }, CompletionStatus.IN_PROGRESS: { "text": lazy_gettext("Partially completed"), "link": { "text": lazy_gettext("Continue with section"), - "aria_label": lazy_gettext("Continue with {section_name} section"), + "aria_label": lazy_gettext("Continue with section: {section_name}"), }, }, CompletionStatus.NOT_STARTED: { "text": lazy_gettext("Not started"), "link": { "text": lazy_gettext("Start section"), - "aria_label": lazy_gettext("Start {section_name} section"), + "aria_label": lazy_gettext("Start section: {section_name}"), }, }, CompletionStatus.INDIVIDUAL_RESPONSE_REQUESTED: { @@ -40,7 +44,9 @@ class HubContext(Context): }, } - def __call__(self, survey_complete, enabled_section_ids) -> Mapping: + def __call__( + self, survey_complete: bool, enabled_section_ids: Iterable[str] + ) -> dict[str, Any]: rows = self._get_rows(enabled_section_ids) if survey_complete: @@ -75,10 +81,14 @@ def __call__(self, survey_complete, enabled_section_ids) -> Mapping: } def get_row_context_for_section( - self, section_name: str, section_status: str, section_url: str, row_id: str - ) -> Mapping[str, Union[str, List]]: + self, + section_name: str | None, + section_status: CompletionStatus, + section_url: str, + row_id: str, + ) -> dict[str, str | list]: section_content = self.SECTION_CONTENT_STATES[section_status] - context: Mapping = { + context: dict = { "rowItems": [ { "rowTitle": section_name, @@ -88,9 +98,9 @@ def get_row_context_for_section( "actions": [ { "text": section_content["link"]["text"], - "ariaLabel": section_content["link"]["aria_label"].format( - section_name=section_name - ), + "visuallyHiddenText": section_content["link"][ + "aria_label" + ].format(section_name=section_name), "url": section_url, "attributes": {"data-qa": f"hub-row-{row_id}-link"}, } @@ -108,7 +118,9 @@ def get_row_context_for_section( return context @staticmethod - def get_section_url(section_id, list_item_id, section_status) -> str: + def get_section_url( + section_id: str, list_item_id: str | None, section_status: CompletionStatus + ) -> str: if section_status == CompletionStatus.INDIVIDUAL_RESPONSE_REQUESTED: return url_for( "individual_response.individual_response_change", @@ -124,11 +136,15 @@ def get_section_url(section_id, list_item_id, section_status) -> str: return url_for("questionnaire.get_section", section_id=section_id) - def _get_row_for_repeating_section(self, section_id, list_item_id, list_item_index): - repeating_title = self._schema.get_repeating_title_for_section(section_id) + def _get_row_for_repeating_section( + self, section_id: str, list_item_id: str, list_item_index: int | None + ) -> dict[str, str | list]: + # Type ignore: section id will be valid and repeat will be present at this stage + repeating_title: ImmutableDict = self._schema.get_repeating_title_for_section(section_id) # type: ignore - title = self._placeholder_renderer.render_placeholder( - repeating_title, list_item_id + title: str = self._placeholder_renderer.render_placeholder( + repeating_title, + list_item_id, ) return self._get_row_for_section( @@ -136,12 +152,16 @@ def _get_row_for_repeating_section(self, section_id, list_item_id, list_item_ind ) def _get_row_for_section( - self, section_title, section_id, list_item_id=None, list_item_index=None - ): + self, + section_title: str | None, + section_id: str, + list_item_id: str | None = None, + list_item_index: int | None = None, + ) -> dict[str, str | list]: row_id = f"{section_id}-{list_item_index}" if list_item_index else section_id - section_status = self._progress_store.get_section_status( - section_id, list_item_id + section_status = self._data_stores.progress_store.get_section_status( + SectionKey(section_id, list_item_id) ) return self.get_row_context_for_section( @@ -151,8 +171,10 @@ def _get_row_for_section( row_id, ) - def _get_rows(self, enabled_section_ids) -> List[Mapping[str, Union[str, List]]]: - rows: List[Mapping] = [] + def _get_rows( + self, enabled_section_ids: Iterable[str] + ) -> list[dict[str, str | list]]: + rows: list[dict] = [] for section_id in enabled_section_ids: show_on_hub = self._schema.get_show_on_hub_for_section(section_id) @@ -163,7 +185,7 @@ def _get_rows(self, enabled_section_ids) -> List[Mapping[str, Union[str, List]]] if repeating_list: for list_item_index, list_item_id in enumerate( - self._list_store[repeating_list].items, start=1 + self._data_stores.list_store[repeating_list].items, start=1 ): rows.append( self._get_row_for_repeating_section( @@ -182,12 +204,12 @@ def _individual_response_enabled(self) -> bool: for_list = self._schema.json["individual_response"]["for_list"] - if not self._list_store[for_list].non_primary_people: + if not self._data_stores.list_store[for_list].non_primary_people: return False return True @cached_property - def _individual_response_url(self) -> Union[str, None]: + def _individual_response_url(self) -> str | None: if ( self._individual_response_enabled and self._schema.get_individual_response_show_on_hub() diff --git a/app/views/contexts/list_context.py b/app/views/contexts/list_context.py index d1d608e34a..20bce2146e 100644 --- a/app/views/contexts/list_context.py +++ b/app/views/contexts/list_context.py @@ -1,32 +1,38 @@ from functools import partial +from typing import Any, Generator, Mapping, Sequence from flask import url_for from flask_babel import lazy_gettext -from . import Context +from app.questionnaire.location import SectionKey +from app.views.contexts.context import Context class ListContext(Context): def __call__( self, - summary_definition, - for_list, - return_to=None, - edit_block_id=None, - remove_block_id=None, - primary_person_edit_block_id=None, - for_list_item_ids=None, - ): + summary_definition: Mapping, + for_list: str, + section_id: str, + has_repeating_blocks: bool, + return_to: str | None = None, + edit_block_id: str | None = None, + remove_block_id: str | None = None, + primary_person_edit_block_id: str | None = None, + for_list_item_ids: Sequence[str] | None = None, + ) -> dict[str, Any]: list_items = ( list( self._build_list_items_context( - for_list, - return_to, - summary_definition, - edit_block_id, - remove_block_id, - primary_person_edit_block_id, - for_list_item_ids, + for_list=for_list, + section_id=section_id, + has_repeating_blocks=has_repeating_blocks, + return_to=return_to, + summary_definition=summary_definition, + edit_block_id=edit_block_id, + remove_block_id=remove_block_id, + primary_person_edit_block_id=primary_person_edit_block_id, + for_list_item_ids=for_list_item_ids, ) ) if summary_definition @@ -37,27 +43,31 @@ def __call__( "list": { "list_items": list_items, "editable": any([edit_block_id, remove_block_id]), - } + }, } + # pylint: disable=too-many-locals def _build_list_items_context( self, - for_list, - return_to, - summary_definition, - edit_block_id, - remove_block_id, - primary_person_edit_block_id, - for_list_item_ids, - ): - list_item_ids = self._list_store[for_list] + *, + for_list: str, + section_id: str, + has_repeating_blocks: bool, + return_to: str | None, + summary_definition: Mapping, + edit_block_id: str | None, + remove_block_id: str | None, + primary_person_edit_block_id: str | None, + for_list_item_ids: Sequence[str] | None, + ) -> Generator[dict, None, None]: + list_item_ids = self._data_stores.list_store[for_list] if for_list_item_ids: list_item_ids = [ - list_item_id + list_item_id # type: ignore for list_item_id in list_item_ids if list_item_id in for_list_item_ids ] - primary_person = self._list_store[for_list].primary_person + primary_person = self._data_stores.list_store[for_list].primary_person for list_item_id in list_item_ids: partial_url_for = partial( @@ -76,17 +86,24 @@ def _build_list_items_context( ), "primary_person": is_primary, "list_item_id": list_item_id, + "is_complete": self._data_stores.progress_store.is_section_complete( + SectionKey(section_id, list_item_id) + ), + "repeating_blocks": has_repeating_blocks, } if edit_block_id: - if is_primary and primary_person_edit_block_id: - list_item_context["edit_link"] = partial_url_for( - block_id=primary_person_edit_block_id - ) - else: - list_item_context["edit_link"] = partial_url_for( - block_id=edit_block_id - ) + block_id = ( + primary_person_edit_block_id + if is_primary and primary_person_edit_block_id + else edit_block_id + ) + # return to answer id is used to snap back to the appropriate list item when editing from a summary page + # unlike other repeating answers that use answer_id-list_item_id, the edit-block is linked to item-label and anchored by list item id + return_to_answer_id = list_item_id if return_to else None + list_item_context["edit_link"] = partial_url_for( + block_id=block_id, return_to_answer_id=return_to_answer_id + ) if remove_block_id: list_item_context["remove_link"] = partial_url_for( @@ -95,12 +112,17 @@ def _build_list_items_context( yield list_item_context - def _get_item_title(self, summary_definition, list_item_id, is_primary): - rendered_summary = self._placeholder_renderer.render( - summary_definition, list_item_id + def _get_item_title( + self, + summary_definition: Mapping[str, Any], + list_item_id: str | None, + is_primary: bool, + ) -> str: + rendered_summary: dict[str, Any] = self._placeholder_renderer.render( + data_to_render=summary_definition, list_item_id=list_item_id ) - if is_primary: rendered_summary["item_title"] += lazy_gettext(" (You)") - return rendered_summary["item_title"] + title: str = rendered_summary["item_title"] + return title diff --git a/app/views/contexts/preview/__init__.py b/app/views/contexts/preview/__init__.py new file mode 100644 index 0000000000..4adc806531 --- /dev/null +++ b/app/views/contexts/preview/__init__.py @@ -0,0 +1,3 @@ +from app.views.contexts.preview.preview_group import PreviewGroup + +__all__ = ["PreviewGroup"] diff --git a/app/views/contexts/preview/preview_block.py b/app/views/contexts/preview/preview_block.py new file mode 100644 index 0000000000..9181879d36 --- /dev/null +++ b/app/views/contexts/preview/preview_block.py @@ -0,0 +1,30 @@ +from typing import Any + +from werkzeug.datastructures import ImmutableDict + +from app.views.contexts.preview.preview_question import PreviewQuestion + + +class PreviewBlock: + def __init__( + self, + *, + block: ImmutableDict, + ): + self._block = block + self._question = self._get_question( + block=self._block, + ) + + @staticmethod + def _get_question( + block: ImmutableDict, + ) -> dict[str, str | dict]: + return PreviewQuestion( + block=block, + ).serialize() + + def serialize(self) -> dict[str, str | dict | Any]: + return { + "question": self._question, + } diff --git a/app/views/contexts/preview/preview_group.py b/app/views/contexts/preview/preview_group.py new file mode 100644 index 0000000000..f3e86e6268 --- /dev/null +++ b/app/views/contexts/preview/preview_group.py @@ -0,0 +1,31 @@ +from typing import Any, Mapping + +from app.views.contexts.preview.preview_block import PreviewBlock + + +class PreviewGroup: + def __init__( + self, + *, + group_schema: Mapping[str, Any], + ): + self._blocks = self._build_blocks( + group_schema=group_schema, + ) + + @staticmethod + def _build_blocks( + group_schema: Mapping[str, Any], + ) -> list[dict]: + return [ + PreviewBlock( + block=block, + ).serialize() + for block in group_schema["blocks"] + if block["type"] == "Question" + ] + + def serialize( + self, + ) -> dict[str, Any]: + return {"blocks": self._blocks} diff --git a/app/views/contexts/preview/preview_question.py b/app/views/contexts/preview/preview_question.py new file mode 100644 index 0000000000..30630f890a --- /dev/null +++ b/app/views/contexts/preview/preview_question.py @@ -0,0 +1,67 @@ +from typing import Any + +from flask_babel import lazy_gettext +from werkzeug.datastructures import ImmutableDict + + +class PreviewQuestion: + def __init__( + self, + *, + block: ImmutableDict, + ): + self._block = block + self._block_id = block["id"] + self._question = self.get_question() + self._title = self._question["title"] + self._answers = self._build_answers() + self._descriptions = self._question.get("description") + self._guidance = self._question.get("guidance") + self._type = self._question.get("type") + + def _build_answers(self) -> list[dict]: + answers_list = [] + for answer in self._question.get("answers", []): + answer_dict = {} + answer_type = answer.get("type") + if options := answer.get("options"): + options_list = [option["label"] for option in options] + answer_dict["options"] = options_list + if answer_type == "Checkbox": + answer_dict["options_text"] = lazy_gettext( + "You can answer with the following options:" + ) + else: + answer_dict["options_text"] = lazy_gettext( + "You can answer with one of the following options:" + ) + elif answer_label := answer.get("label"): + answer_dict["label"] = answer_label + + if description := answer.get("description"): + answer_dict["description"] = description + + if guidance := answer.get("guidance"): + answer_dict["guidance"] = guidance + + if answer_type == "TextArea" and (length := answer.get("max_length")): + answer_dict["max_length"] = length + + answers_list.append(answer_dict) + return answers_list + + def serialize(self) -> dict[str, str | dict | Any]: + return { + "id": self._block_id, + "title": self._title, + "answers": self._answers, + "descriptions": self._descriptions, + "guidance": self._guidance, + "type": self._type, + } + + def get_question(self) -> Any: + if "question_variants" in self._block: + return self._block["question_variants"][0]["question"] + + return self._block["question"] diff --git a/app/views/contexts/preview_context.py b/app/views/contexts/preview_context.py new file mode 100644 index 0000000000..2abc9a9005 --- /dev/null +++ b/app/views/contexts/preview_context.py @@ -0,0 +1,52 @@ +from typing import Generator + +from flask_babel import lazy_gettext + +from app.data_models.data_stores import DataStores +from app.questionnaire import QuestionnaireSchema +from app.views.contexts import Context +from app.views.contexts.section_preview_context import SectionPreviewContext + + +class PreviewNotEnabledException(Exception): + pass + + +class PreviewContext(Context): + def __init__( + self, language: str, schema: QuestionnaireSchema, data_stores: DataStores + ): + if not schema.preview_enabled: + raise PreviewNotEnabledException + + super().__init__( + language, + schema, + data_stores, + placeholder_preview_mode=True, + ) + + def __call__(self) -> dict[str, str | list | bool]: + sections = list(self.build_all_sections()) + return { + "sections": sections, + } + + def build_all_sections(self) -> Generator[dict, None, None]: + """NB: Does not support repeating sections""" + + for section in self._schema.get_sections(): + section_id = section["id"] + section_preview_context = SectionPreviewContext( + language=self._language, + schema=self._schema, + data_stores=self._data_stores, + section_id=section_id, + ) + + yield from section_preview_context()["preview"] + + @staticmethod + def get_page_title() -> str: + title: str = lazy_gettext("Preview survey questions") + return title diff --git a/app/views/contexts/question.py b/app/views/contexts/question.py index a3460a2b91..d09a612408 100644 --- a/app/views/contexts/question.py +++ b/app/views/contexts/question.py @@ -1,7 +1,13 @@ -def build_question_context(rendered_block, form): +from app.forms.questionnaire_form import QuestionnaireForm +from app.questionnaire import QuestionSchemaType + + +def build_question_context( + rendered_block: dict[str, QuestionSchemaType], form: QuestionnaireForm +) -> dict: question = rendered_block["question"] - context = { + context: dict[str, dict] = { "block": rendered_block, "form": { "errors": form.errors, @@ -12,15 +18,17 @@ def build_question_context(rendered_block, form): }, } - answer_ids = [] + answer_ids: list[str] = [] for answer in question["answers"]: answer_ids.append(answer["id"]) if answer["type"] in ("Checkbox", "Radio"): - for option in answer.get("options", []): - if "detail_answer" in option: - answer_ids.append(option["detail_answer"]["id"]) + answer_ids.extend( + option["detail_answer"]["id"] + for option in answer.get("options", []) + if "detail_answer" in option + ) for answer_id in answer_ids: context["form"]["answer_errors"][answer_id] = form.answer_errors(answer_id) diff --git a/app/views/contexts/section_preview_context.py b/app/views/contexts/section_preview_context.py new file mode 100644 index 0000000000..0b5e6d1942 --- /dev/null +++ b/app/views/contexts/section_preview_context.py @@ -0,0 +1,40 @@ +from app.data_models.data_stores import DataStores +from app.questionnaire import QuestionnaireSchema +from app.views.contexts.context import Context +from app.views.contexts.preview import PreviewGroup + + +class SectionPreviewContext(Context): + def __init__( + self, + *, + language: str, + schema: QuestionnaireSchema, + data_stores: DataStores, + section_id: str, + ): + super().__init__( + language, + schema, + data_stores, + placeholder_preview_mode=True, + ) + self._section_id = section_id + + def __call__(self) -> dict: + return {"preview": self._build_preview()} + + def _build_preview(self) -> list[dict]: + # Type ignore: The section has to exist at this point + section = self._placeholder_renderer.render(data_to_render=self._schema.get_section(self._section_id), list_item_id=None) # type: ignore + + groups = [ + PreviewGroup(group_schema=group).serialize() for group in section["groups"] + ] + section_dict: dict = { + "title": section["title"], + "id": section["id"], + "blocks": [block for group in groups for block in group["blocks"]], + } + + return [section_dict] diff --git a/app/views/contexts/section_summary_context.py b/app/views/contexts/section_summary_context.py index df3c87ada4..bd83394339 100644 --- a/app/views/contexts/section_summary_context.py +++ b/app/views/contexts/section_summary_context.py @@ -1,17 +1,17 @@ from functools import cached_property -from typing import Any, Mapping, Optional +from typing import Any, Generator, Iterable, Mapping -from flask import url_for +from werkzeug.datastructures import ImmutableDict -from app.data_models import AnswerStore, ListStore, ProgressStore -from app.questionnaire import QuestionnaireSchema -from app.questionnaire.location import Location +from app.data_models.data_stores import DataStores +from app.questionnaire import Location, QuestionnaireSchema +from app.questionnaire.questionnaire_schema import LIST_COLLECTORS_WITH_REPEATING_BLOCKS +from app.questionnaire.return_location import ReturnLocation from app.questionnaire.routing_path import RoutingPath from app.utilities import safe_content - -from .context import Context -from .list_context import ListContext -from .summary import Group +from app.views.contexts.context import Context +from app.views.contexts.summary.group import Group +from app.views.contexts.summary.list_collector_block import ListCollectorBlock class SectionSummaryContext(Context): @@ -19,29 +19,26 @@ def __init__( self, language: str, schema: QuestionnaireSchema, - answer_store: AnswerStore, - list_store: ListStore, - progress_store: ProgressStore, - metadata: Mapping[str, Any], - response_metadata: Mapping, + data_stores: DataStores, routing_path: RoutingPath, current_location: Location, - ): + ) -> None: super().__init__( language, schema, - answer_store, - list_store, - progress_store, - metadata, - response_metadata, + data_stores, ) self.routing_path = routing_path self.current_location = current_location + self.data_stores = data_stores - def __call__(self, return_to: Optional[str] = "section-summary") -> Mapping: - summary = self._build_summary(return_to) - title_for_location = self._title_for_location() + def __call__( + self, + return_to: str | None = "section-summary", + view_submitted_response: bool = False, + ) -> Mapping[str, Any]: + summary = self.build_summary(return_to, view_submitted_response) + title_for_location = self.title_for_location() title = ( self._placeholder_renderer.render_placeholder( title_for_location, self.current_location.list_item_id @@ -52,34 +49,35 @@ def __call__(self, return_to: Optional[str] = "section-summary") -> Mapping: page_title = self.get_page_title(title_for_location) - return { + summary_context = { "summary": { "title": title, "page_title": page_title, "summary_type": "SectionSummary", "answers_are_editable": True, - **summary, - } + "collapsible": summary.get("collapsible"), + }, } - @cached_property - def section(self): - return self._schema.get_section(self.current_location.section_id) - - @property - def list_context(self): - return ListContext( - self._language, - self._schema, - self._answer_store, - self._list_store, - self._progress_store, - self._metadata, - self._response_metadata, - ) + if custom_summary := summary.get("custom_summary"): + summary_context["summary"]["custom_summary"] = custom_summary + elif groups := summary.get("groups"): + summary_context["summary"]["sections"] = [ + { + "title": title, + "groups": groups, + } + ] - def get_page_title(self, title_for_location: str) -> str: + return summary_context + @cached_property + def section(self) -> ImmutableDict: + # Type ignore: The section has to exist at this point + section: ImmutableDict = self._schema.get_section(self.current_location.section_id) # type: ignore + return section + + def get_page_title(self, title_for_location: Mapping | str) -> str: section_repeating_page_title = ( self._schema.get_repeating_page_title_for_section( self.current_location.section_id @@ -93,13 +91,17 @@ def get_page_title(self, title_for_location: str) -> str: page_title = f"{page_title}: {section_repeating_page_title}" if self.current_location.list_item_id and self.current_location.list_name: - list_item_position = self._list_store.list_item_position( + list_item_position = self._data_stores.list_store.list_item_position( self.current_location.list_name, self.current_location.list_item_id ) page_title = page_title.format(list_item_position=list_item_position) return page_title - def _build_summary(self, return_to: Optional[str]): + def build_summary( + self, + return_to: str | None, + view_submitted_response: bool, + ) -> dict: """ Build a summary context for a particular location. @@ -108,7 +110,9 @@ def _build_summary(self, return_to: Optional[str]): summary = self.section.get("summary", {}) collapsible = {"collapsible": summary.get("collapsible", False)} - if summary.get("items"): + show_non_item_answers = summary.get("show_non_item_answers", False) + + if summary.get("items") and not show_non_item_answers: summary_elements = { "custom_summary": list( self._custom_summary_elements( @@ -117,122 +121,106 @@ def _build_summary(self, return_to: Optional[str]): ) } - return {**collapsible, **summary_elements} + return collapsible | summary_elements + + refactored_groups = self._get_refactored_groups(self.section["groups"]) - return { + groups = { **collapsible, "groups": [ Group( - group, - self.routing_path, - self._answer_store, - self._list_store, - self._metadata, - self._response_metadata, - self._schema, - self.current_location, - self._language, - return_to, + group_schema=group, + routing_path_block_ids=self.routing_path.block_ids, + schema=self._schema, + data_stores=self._data_stores, + location=self.current_location, + language=self._language, + return_location=ReturnLocation( + return_to=return_to, + ), + view_submitted_response=view_submitted_response, ).serialize() - for group in self.section["groups"] + for group in refactored_groups ], } - def _title_for_location(self): + return groups + + def title_for_location(self) -> str | dict: section_id = self.current_location.section_id - title = ( - self._schema.get_repeating_title_for_section(section_id) + return ( + # Type ignore: section id should exist at this point + self._schema.get_repeating_title_for_section(section_id) # type: ignore or self._schema.get_summary_title_for_section(section_id) or self._schema.get_title_for_section(section_id) ) - return title - def _custom_summary_elements(self, section_summary): + def _custom_summary_elements( + self, section_summary: Iterable[Mapping] + ) -> Generator[dict[str, Any], Any, None]: for summary_element in section_summary: if summary_element["type"] == "List": - yield self._list_summary_element(summary_element) - - def _list_summary_element(self, summary) -> Mapping: - list_collector_block = None - edit_block_id, remove_block_id, primary_person_edit_block_id = None, None, None - current_list = self._list_store[summary["for_list"]] - - list_collector_blocks = list( - self._schema.get_list_collectors_for_list( - self.section, for_list=summary["for_list"] - ) - ) - - list_collector_blocks_on_path = [ - list_collector_block - for list_collector_block in list_collector_blocks - if list_collector_block["id"] in self.routing_path.block_ids - ] - - if list_collector_blocks_on_path: - list_collector_block = list_collector_blocks_on_path[0] - edit_block_id = list_collector_block["edit_block"]["id"] - remove_block_id = list_collector_block["remove_block"]["id"] - - add_link = self._add_link(summary, list_collector_block) - - if len(current_list) == 1 and current_list.primary_person: - - if primary_person_block := self._schema.get_list_collector_for_list( - self.section, for_list=summary["for_list"], primary=True - ): - primary_person_edit_block_id = primary_person_block[ - "add_or_edit_block" - ]["id"] - edit_block_id = primary_person_block["add_or_edit_block"]["id"] - - rendered_summary = self._placeholder_renderer.render( - summary, self.current_location.list_item_id - ) - - list_collector_block = list_collector_block or list_collector_blocks[0] - - list_summary_context = self.list_context( - list_collector_block["summary"], - for_list=list_collector_block["for_list"], - return_to="section-summary", - edit_block_id=edit_block_id, - remove_block_id=remove_block_id, - primary_person_edit_block_id=primary_person_edit_block_id, - ) - - return { - "title": rendered_summary["title"], - "type": rendered_summary["type"], - "add_link": add_link, - "add_link_text": rendered_summary["add_link_text"], - "empty_list_text": rendered_summary.get("empty_list_text"), - "list_name": rendered_summary["for_list"], - **list_summary_context, - } - - def _add_link(self, summary, list_collector_block): - - if list_collector_block: - return url_for( - "questionnaire.block", - list_name=summary["for_list"], - block_id=list_collector_block["add_block"]["id"], - return_to="section-summary", - ) - - driving_question_block = QuestionnaireSchema.get_driving_question_for_list( - self.section, summary["for_list"] - ) - - if driving_question_block: - return url_for( - "questionnaire.block", - block_id=driving_question_block["id"], - return_to="section-summary", - ) + list_collector_block = ListCollectorBlock( + routing_path_block_ids=self.routing_path.block_ids, + data_stores=self._data_stores, + schema=self._schema, + location=self.current_location, + language=self._language, + return_location=ReturnLocation(return_to="section-summary"), + ) + yield list_collector_block.list_summary_element(summary_element) - def _get_safe_page_title(self, title): + def _get_safe_page_title(self, title: Mapping | str) -> str: return ( safe_content(self._schema.get_single_string_value(title)) if title else "" ) + + @staticmethod + def _get_refactored_groups(original_groups: dict) -> list[dict[str, Any]]: + """original schema groups are refactored into groups based on block types, it follows the order/sequence of blocks in the original groups, all the + non list collector blocks are put together into groups, list collectors are put into separate groups, this way summary groups are displayed correctly + on section summary""" + refactored_groups = [] + group_number = 0 + + for group in list(original_groups): + group_name = group["id"] + non_list_collector_blocks: list[dict[str, str]] = [] + list_collector_blocks: list[dict[str, str]] = [] + for block in group["blocks"]: + if block["type"] in LIST_COLLECTORS_WITH_REPEATING_BLOCKS: + # if list collector block encountered, close the previously started non list collector blocks list if exists + if non_list_collector_blocks: + previously_started_group = { + "id": f"{group_name}-{group_number}", + "blocks": non_list_collector_blocks, + } + # add previous non list collector blocks group to all groups and increase the group number for the list collector group + # that you handle next + refactored_groups.append(previously_started_group) + group_number += 1 + list_collector_blocks.append(block) + list_collector_group = { + "id": f"{group_name}-{group_number}", + "blocks": list_collector_blocks, + } + # add current list collector group to all groups and increase the group number for the next group + refactored_groups.append(list_collector_group) + group_number += 1 + # reset both types of block lists for next iterations of this loop if any + list_collector_blocks = [] + non_list_collector_blocks = [] + + else: + # if list collector not encountered keep adding blocks or add first one to an empty non list collector blocks list + non_list_collector_blocks.append(block) + + # on exiting the loop, accumulated list of blocks gets added as a group + non_list_collector_group = { + "id": f"{group_name}-{group_number}", + "blocks": non_list_collector_blocks, + "title": group.get("title"), + } + refactored_groups.append(non_list_collector_group) + + return refactored_groups diff --git a/app/views/contexts/submission_metadata_context.py b/app/views/contexts/submission_metadata_context.py index 9efd264aa0..9b31816ce6 100644 --- a/app/views/contexts/submission_metadata_context.py +++ b/app/views/contexts/submission_metadata_context.py @@ -1,13 +1,15 @@ from datetime import datetime +from typing import Any from flask_babel import format_datetime, lazy_gettext from app.libs.utils import convert_tx_id +from app.survey_config.survey_type import SurveyType def build_submission_metadata_context( - survey_type: str, submitted_at: datetime, tx_id: str -) -> dict: + survey_type: SurveyType, submitted_at: datetime, tx_id: str +) -> dict[str, Any]: submitted_on = { "term": lazy_gettext("Submitted on:"), "descriptions": [ @@ -19,11 +21,12 @@ def build_submission_metadata_context( } ], } + submission_reference = { "term": lazy_gettext("Submission reference:"), "descriptions": [{"description": convert_tx_id(tx_id)}], } - if survey_type == "social": + if survey_type in {SurveyType.SOCIAL, SurveyType.HEALTH}: return { "data-qa": "metadata", "termCol": 3, diff --git a/app/views/contexts/submit_questionnaire_context.py b/app/views/contexts/submit_questionnaire_context.py index 4d7a6ac1ad..8f3aed9977 100644 --- a/app/views/contexts/submit_questionnaire_context.py +++ b/app/views/contexts/submit_questionnaire_context.py @@ -1,13 +1,13 @@ -from typing import Mapping, Union +from typing import Mapping from flask_babel import lazy_gettext -from .context import Context -from .summary_context import SummaryContext +from app.views.contexts.context import Context +from app.views.contexts.summary_context import SummaryContext class SubmitQuestionnaireContext(Context): - def __call__(self) -> dict[str, Union[str, dict]]: + def __call__(self) -> dict[str, str | dict]: submission_schema: Mapping = self._schema.get_submission() title = submission_schema.get("title") or lazy_gettext( @@ -34,11 +34,8 @@ def __call__(self) -> dict[str, Union[str, dict]]: summary_context = SummaryContext( language=self._language, schema=self._schema, - answer_store=self._answer_store, - list_store=self._list_store, - progress_store=self._progress_store, - metadata=self._metadata, - response_metadata=self._response_metadata, + data_stores=self._data_stores, + view_submitted_response=False, ) context["summary"] = summary_context( answers_are_editable=True, return_to="final-summary" diff --git a/app/views/contexts/summary/__init__.py b/app/views/contexts/summary/__init__.py index 2019c3b40b..e9aa8cc742 100644 --- a/app/views/contexts/summary/__init__.py +++ b/app/views/contexts/summary/__init__.py @@ -1,3 +1,3 @@ -from .group import Group +from app.views.contexts.summary.group import Group __all__ = ["Group"] diff --git a/app/views/contexts/summary/answer.py b/app/views/contexts/summary/answer.py index 60a74d8114..226ac9c043 100644 --- a/app/views/contexts/summary/answer.py +++ b/app/views/contexts/summary/answer.py @@ -1,17 +1,30 @@ +from typing import Mapping + from flask import url_for +from app.data_models.answer import AnswerValueEscapedTypes +from app.questionnaire.return_location import ReturnLocation + +RadioCheckboxTypes = dict[str, str | AnswerValueEscapedTypes | None] +DateRangeTypes = dict[str, AnswerValueEscapedTypes | None] + +InferredAnswerValueTypes = ( + None | DateRangeTypes | str | AnswerValueEscapedTypes | RadioCheckboxTypes +) + class Answer: def __init__( self, *, - answer_schema, - answer_value, - block_id, - list_name, - list_item_id, - return_to, - ): + answer_schema: Mapping[str, str], + answer_value: InferredAnswerValueTypes, + block_id: str, + list_name: str | None, + list_item_id: str | None, + return_location: ReturnLocation, + is_in_repeating_section: bool, + ) -> None: self.id = answer_schema["id"] self.label = answer_schema.get("label") self.value = answer_value @@ -19,14 +32,17 @@ def __init__( self.unit = answer_schema.get("unit") self.unit_length = answer_schema.get("unit_length") self.currency = answer_schema.get("currency") + self.decimal_places = answer_schema.get("decimal_places") + self._original_answer_id = answer_schema.get("original_answer_id") self.link = self._build_link( block_id=block_id, list_name=list_name, list_item_id=list_item_id, - return_to=return_to, + return_location=return_location, + is_in_repeating_section=is_in_repeating_section, ) - def serialize(self): + def serialize(self) -> dict: return { "id": self.id, "label": self.label, @@ -36,15 +52,60 @@ def serialize(self): "unit_length": self.unit_length, "currency": self.currency, "link": self.link, + "decimal_places": self.decimal_places, } - def _build_link(self, *, block_id, list_name, list_item_id, return_to): + def _build_link( + self, + *, + block_id: str, + list_name: str | None, + list_item_id: str | None, + return_location: ReturnLocation, + is_in_repeating_section: bool, + ) -> str: + return_to_answer_id = self._return_to_answer_id( + return_to=return_location.return_to, + list_item_id=list_item_id, + is_in_repeating_section=is_in_repeating_section, + return_to_answer_id=return_location.return_to_answer_id, + ) return url_for( - "questionnaire.block", + endpoint="questionnaire.block", list_name=list_name, block_id=block_id, list_item_id=list_item_id, - return_to=return_to, - return_to_answer_id=self.id, + return_to=return_location.return_to, + return_to_answer_id=return_to_answer_id, + return_to_block_id=return_location.return_to_block_id, + return_to_list_item_id=return_location.return_to_list_item_id, _anchor=self.id, ) + + def _return_to_answer_id( + self, + *, + return_to: str | None, + list_item_id: str | None, + is_in_repeating_section: bool, + return_to_answer_id: str | None, + ) -> str | None: + """ + If the summary page using this answer has repeating answers, but it is not in a repeating section, + then the answer ids will be suffixed with list item id, so the return to answer id link also needs this to work correctly + """ + answer_id = None + if return_to: + if ( + list_item_id + and not is_in_repeating_section + and not self._original_answer_id # original answer would mean id has already been suffixed + ): + answer_id = f"{self.id}-{list_item_id}" + else: + answer_id = self.id + + if return_to_answer_id: + answer_id += f",{return_to_answer_id}" + + return answer_id diff --git a/app/views/contexts/summary/block.py b/app/views/contexts/summary/block.py index f5ce6d3ae6..998594c793 100644 --- a/app/views/contexts/summary/block.py +++ b/app/views/contexts/summary/block.py @@ -1,97 +1,90 @@ -from app.questionnaire.rules.rule_evaluator import RuleEvaluator -from app.questionnaire.value_source_resolver import ValueSourceResolver +from typing import Mapping + +from jsonpointer import resolve_pointer + +from app.data_models.data_stores import DataStores +from app.questionnaire import QuestionnaireSchema +from app.questionnaire.return_location import ReturnLocation +from app.questionnaire.schema_utils import find_pointers_containing from app.questionnaire.variants import choose_variant +from app.utilities.types import LocationType from app.views.contexts.summary.question import Question class Block: def __init__( self, - block_schema, + block_schema: Mapping, *, - answer_store, - list_store, - metadata, - response_metadata, - schema, - location, - return_to, - ): + data_stores: DataStores, + schema: QuestionnaireSchema, + location: LocationType, + return_location: ReturnLocation, + language: str, + ) -> None: self.id = block_schema["id"] self.title = block_schema.get("title") self.number = block_schema.get("number") - - self._rule_evaluator = RuleEvaluator( - schema=schema, - answer_store=answer_store, - list_store=list_store, - metadata=metadata, - response_metadata=response_metadata, - location=location, - ) - - self._value_source_resolver = ValueSourceResolver( - answer_store=answer_store, - list_store=list_store, - metadata=metadata, - response_metadata=response_metadata, - schema=schema, - location=location, - list_item_id=location.list_item_id if location else None, - use_default_answer=True, - ) + self.location = location + self.schema = schema + self.data_stores = data_stores self.question = self.get_question( block_schema=block_schema, - answer_store=answer_store, - list_store=list_store, - metadata=metadata, - response_metadata=response_metadata, - schema=schema, - location=location, - return_to=return_to, + data_stores=self.data_stores, + return_location=return_location, + language=language, ) def get_question( self, *, - block_schema, - answer_store, - list_store, - metadata, - response_metadata, - schema, - location, - return_to, - ): - """ Taking question variants into account, return the question which was displayed to the user """ + data_stores: DataStores, + block_schema: Mapping, + return_location: ReturnLocation, + language: str, + ) -> dict[str, Question]: + """Taking question variants into account, return the question which was displayed to the user""" variant = choose_variant( block_schema, - schema, - metadata, - response_metadata, - answer_store, - list_store, + self.schema, + data_stores, variants_key="question_variants", single_key="question", - current_location=location, + current_location=self.location, ) return Question( variant, - answer_store=answer_store, - schema=schema, - rule_evaluator=self._rule_evaluator, - value_source_resolver=self._value_source_resolver, - location=location, + data_stores=self.data_stores, + schema=self.schema, + location=self.location, block_id=self.id, - return_to=return_to, + return_location=return_location, + language=language, ).serialize() - def serialize(self): - return { - "id": self.id, - "title": self.title, - "number": self.number, - "question": self.question, - } + def _handle_id_suffixing(self, block: dict) -> dict: + """ + If the block is repeating but not within a repeating section, summary pages will render it multiple times, once per list item + so the block id, as well as any other ids (e.g. question, answer) need suffixing with list_item_id to ensure the HTML rendered is valid and doesn't + have duplicate div ids + """ + if ( + self.location.list_item_id + and not self.schema.is_block_in_repeating_section(self.id) + ): + for pointer in find_pointers_containing(block, "id"): + data = resolve_pointer(block, pointer) + data["id"] = f"{data['id']}-{self.location.list_item_id}" + return block + + def serialize(self) -> dict: + return self._handle_id_suffixing( + { + "id": self.id, + "title": self.title, + "number": self.number, + "question": self.question, + } + ) diff --git a/app/views/contexts/summary/calculated_summary_block.py b/app/views/contexts/summary/calculated_summary_block.py new file mode 100644 index 0000000000..fb96b44970 --- /dev/null +++ b/app/views/contexts/summary/calculated_summary_block.py @@ -0,0 +1,94 @@ +from decimal import Decimal +from typing import Iterable, Mapping, TypeAlias + +from flask import url_for + +from app.data_models.data_stores import DataStores +from app.questionnaire import QuestionnaireSchema +from app.questionnaire.return_location import ReturnLocation +from app.questionnaire.rules.rule_evaluator import RuleEvaluator +from app.utilities.types import LocationType + +NumericType: TypeAlias = int | float | Decimal + + +class CalculatedSummaryBlock: + def __init__( + self, + block_schema: Mapping, + *, + data_stores: DataStores, + schema: QuestionnaireSchema, + location: LocationType, + return_location: ReturnLocation, + routing_path_block_ids: Iterable[str], + ) -> None: + """ + A Calculated summary block that is rendered as part of a grand calculated summary + + If the GCS is in a repeating section, and the calculated summary also is + then the list name and item id need to be used to build the calculated summary change link + but if the GCS is repeating and the CS is not, the list parameters in the change link should be set to None + """ + + self.id = block_schema["id"] + self.title = block_schema["calculation"]["title"] + self._return_location = return_location + self._block_schema = block_schema + self._schema = schema + self._data_stores = data_stores + if self._schema.is_block_in_repeating_section(self.id): + self._list_item_id = location.list_item_id + self._list_name = location.list_name + else: + self._list_item_id = None + self._list_name = None + + self._rule_evaluator = RuleEvaluator( + schema=schema, + data_stores=self._data_stores, + location=location, + routing_path_block_ids=routing_path_block_ids, + ) + + # Type ignore: for a calculated summary the resolved answer would only ever be one of these 3 + calculated_total: NumericType = self._rule_evaluator.evaluate(block_schema["calculation"]["operation"]) # type: ignore + answer_format = self._schema.get_answer_format_for_calculated_summary(self.id) + self.answers = [ + { + "id": self.id, + "label": self.title, + "value": calculated_total, + "link": self._build_link(), + **answer_format, + } + ] + + def _build_link(self) -> str: + # not required if the calculated summary is in the repeat alongside the GCS + return_to_list_item_id = ( + self._return_location.return_to_list_item_id + if not self._list_item_id + else None + ) + return url_for( + "questionnaire.block", + block_id=self.id, + list_name=self._list_name, + list_item_id=self._list_item_id, + return_to=self._return_location.return_to, + return_to_answer_id=self.id, + return_to_block_id=self._return_location.return_to_block_id, + return_to_list_item_id=return_to_list_item_id, + _anchor=self.id, + ) + + def _calculated_summary(self) -> dict: + return {"id": self.id, "title": self.title, "answers": self.answers} + + def serialize(self) -> dict: + return { + "id": self.id, + "title": self.title, + "calculated_summary": self._calculated_summary(), + } diff --git a/app/views/contexts/summary/group.py b/app/views/contexts/summary/group.py index 58b934c3df..e906e4825a 100644 --- a/app/views/contexts/summary/group.py +++ b/app/views/contexts/summary/group.py @@ -1,80 +1,201 @@ +from typing import Iterable, Mapping + +from werkzeug.datastructures import ImmutableDict + +from app.data_models.data_stores import DataStores +from app.questionnaire import QuestionnaireSchema from app.questionnaire.placeholder_renderer import PlaceholderRenderer +from app.questionnaire.questionnaire_schema import ( + LIST_COLLECTORS_WITH_REPEATING_BLOCKS, + is_list_collector_block_editable, +) +from app.questionnaire.return_location import ReturnLocation +from app.survey_config.link import Link +from app.utilities.types import LocationType from app.views.contexts.summary.block import Block +from app.views.contexts.summary.calculated_summary_block import CalculatedSummaryBlock +from app.views.contexts.summary.list_collector_block import ListCollectorBlock +from app.views.contexts.summary.list_collector_content_block import ( + ListCollectorContentBlock, +) class Group: def __init__( self, - group_schema, - routing_path, - answer_store, - list_store, - metadata, - response_metadata, - schema, - location, - language, - return_to, - ): + *, + group_schema: Mapping, + routing_path_block_ids: Iterable[str], + schema: QuestionnaireSchema, + data_stores: DataStores, + location: LocationType, + language: str, + return_location: ReturnLocation, + summary_type: str | None = None, + view_submitted_response: bool | None = False, + ) -> None: self.id = group_schema["id"] + self.title = group_schema.get("title") self.location = location - self.blocks = self._build_blocks( + self.placeholder_text = None + self.links: dict[str, Link] = {} + self.data_stores = data_stores + + self.blocks = self._build_blocks_and_links( group_schema=group_schema, - routing_path=routing_path, - answer_store=answer_store, - list_store=list_store, - metadata=metadata, - response_metadata=response_metadata, + routing_path_block_ids=routing_path_block_ids, + data_stores=self.data_stores, schema=schema, - location=location, - return_to=return_to, + location=self.location, + return_location=return_location, + language=language, + view_submitted_response=view_submitted_response, + summary_type=summary_type, ) + self.placeholder_renderer = PlaceholderRenderer( language=language, - answer_store=answer_store, - list_store=list_store, - metadata=metadata, - response_metadata=response_metadata, + data_stores=data_stores, + location=self.location, schema=schema, ) - @staticmethod - def _build_blocks( + # pylint: disable=too-many-locals + def _build_blocks_and_links( + self, *, - group_schema, - routing_path, - answer_store, - list_store, - metadata, - response_metadata, - schema, - location, - return_to, - ): + group_schema: Mapping, + routing_path_block_ids: Iterable[str], + data_stores: DataStores, + schema: QuestionnaireSchema, + location: LocationType, + return_location: ReturnLocation, + language: str, + view_submitted_response: bool | None = False, + summary_type: str | None = None, + ) -> list[dict[str, Block]]: blocks = [] for block in group_schema["blocks"]: - if block["id"] in routing_path and block["type"] == "Question": + # the block type will only be ListRepeatingQuestion when in the context of a calculated summary or grand calculated summary + # any other summary like section-summary will use the parent list collector instead and render items as part of the ListCollector check further down + if block["type"] == "ListRepeatingQuestion": + # list repeating questions aren't themselves on the path, it's determined by the parent list collector + parent_list_collector_block_id = schema.parent_id_map[block["id"]] + if parent_list_collector_block_id not in routing_path_block_ids: + continue + + list_collector_block_class: type[ + ListCollectorBlock | ListCollectorContentBlock + ] = ( + ListCollectorBlock + if is_list_collector_block_editable( + # Type ignore: return types differ + schema.get_block(parent_list_collector_block_id) # type: ignore + ) + else ListCollectorContentBlock + ) + + list_collector_block = list_collector_block_class( + routing_path_block_ids=routing_path_block_ids, + data_stores=data_stores, + schema=schema, + location=location, + language=language, + return_location=return_location, + ) + repeating_answer_blocks = ( + list_collector_block.get_repeating_block_related_answer_blocks( + block + ) + ) + blocks.extend(repeating_answer_blocks) + + if block["id"] not in routing_path_block_ids: + continue + if block["type"] in [ + "Question", + "ListCollectorDrivingQuestion", + ]: blocks.extend( [ Block( block, - answer_store=answer_store, - list_store=list_store, - metadata=metadata, - response_metadata=response_metadata, + data_stores=data_stores, schema=schema, location=location, - return_to=return_to, + return_location=return_location, + language=language, ).serialize() ] ) + # check the summary_type as opposed to the block type + # otherwise this gets called on section summaries as well + elif summary_type == "GrandCalculatedSummary": + blocks.extend( + [ + CalculatedSummaryBlock( + block, + data_stores=self.data_stores, + schema=schema, + location=location, + return_location=return_location, + routing_path_block_ids=routing_path_block_ids, + ).serialize() + ] + ) + + elif block["type"] in LIST_COLLECTORS_WITH_REPEATING_BLOCKS: + section: ImmutableDict | None = schema.get_section(location.section_id) + + summary_item: ImmutableDict | None + if summary_item := schema.get_summary_item_for_list_for_section( + # Type ignore: section id will not be optional at this point + section_id=section["id"], # type: ignore + list_name=block["for_list"], + ): + list_collector_block_class = ( + ListCollectorBlock + if is_list_collector_block_editable(block) + else ListCollectorContentBlock + ) + list_collector_block = list_collector_block_class( + data_stores=self.data_stores, + routing_path_block_ids=routing_path_block_ids, + schema=schema, + location=location, + language=language, + return_location=return_location, + ) + list_summary_element = list_collector_block.list_summary_element( + summary_item + ) + blocks.extend([list_summary_element]) + + if ( + not view_submitted_response + and is_list_collector_block_editable(block) + ): + self.links["add_link"] = Link( + target="_self", + text=list_summary_element["add_link_text"], + url=list_summary_element["add_link"], + attributes={"data-qa": "add-item-link"}, + ) + + self.placeholder_text = list_summary_element["empty_list_text"] return blocks - def serialize(self): + def serialize(self) -> Mapping: return self.placeholder_renderer.render( - {"id": self.id, "title": self.title, "blocks": self.blocks}, - self.location.list_item_id, + data_to_render={ + "id": self.id, + "title": self.title, + "blocks": self.blocks, + "links": self.links, + "placeholder_text": self.placeholder_text, + }, + list_item_id=self.location.list_item_id if self.location else None, ) diff --git a/app/views/contexts/summary/list_collector_base_block.py b/app/views/contexts/summary/list_collector_base_block.py new file mode 100644 index 0000000000..a6c1f8de1e --- /dev/null +++ b/app/views/contexts/summary/list_collector_base_block.py @@ -0,0 +1,169 @@ +from collections import defaultdict +from typing import Iterable, Mapping, Sequence + +from werkzeug.datastructures import ImmutableDict + +from app.data_models.data_stores import DataStores +from app.data_models.list_store import ListModel +from app.questionnaire import Location, QuestionnaireSchema +from app.questionnaire.placeholder_renderer import PlaceholderRenderer +from app.questionnaire.questionnaire_schema import is_list_collector_block_editable +from app.questionnaire.return_location import ReturnLocation +from app.utilities.types import LocationType +from app.views.contexts import list_context +from app.views.contexts.summary.block import Block + + +class ListCollectorBaseBlock: + def __init__( + self, + *, + routing_path_block_ids: Iterable[str], + data_stores: DataStores, + schema: QuestionnaireSchema, + location: LocationType, + language: str, + return_location: ReturnLocation, + ) -> None: + self._location = location + self._data_stores = data_stores + self._placeholder_renderer = PlaceholderRenderer( + data_stores=data_stores, language=language, schema=schema, location=location + ) + self._schema = schema + self._location = location + # type ignore added as section should exist + self._section: ImmutableDict = self._schema.get_section(self._location.section_id) # type: ignore + self._language = language + self._routing_path_block_ids = routing_path_block_ids + self._return_location = return_location + + @property + def list_context(self) -> list_context.ListContext: + return list_context.ListContext(self._language, self._schema, self._data_stores) + + def _list_collector_block_on_path(self, for_list: str) -> list[ImmutableDict]: + list_collector_blocks = list( + self._schema.get_list_collectors_for_list_for_sections( + [self._section["id"]], for_list=for_list + ) + ) + + return [ + list_collector_block + for list_collector_block in list_collector_blocks + if list_collector_block["id"] in self._routing_path_block_ids + ] + + def _list_collector_block( + self, for_list: str, list_collector_blocks_on_path: list[ImmutableDict] + ) -> ImmutableDict: + list_collector_blocks = list( + self._schema.get_list_collectors_for_list_for_sections( + [self._section["id"]], for_list=for_list + ) + ) + return ( + list_collector_blocks_on_path[0] + if list_collector_blocks_on_path + else list_collector_blocks[0] + ) + + def _get_related_answer_blocks_by_list_item_id( + self, *, list_model: ListModel, repeating_blocks: Sequence[ImmutableDict] + ) -> dict[str, list[dict]] | None: + section_id = self._section["id"] + + related_answers = self._schema.get_related_answers_for_list_for_section( + section_id=section_id, list_name=list_model.name + ) + + blocks: list[dict | ImmutableDict] = [] + + if related_answers: + blocks += self._get_blocks_for_related_answers(related_answers) + + if len(list_model): + blocks += repeating_blocks + + if not blocks: + return None + + related_answers_blocks = {} + + for list_id in list_model: + serialized_blocks = [ + # related answers for repeating blocks may use placeholders, so each block needs rendering here + self._placeholder_renderer.render( + data_to_render=Block( + block, + data_stores=self._data_stores, + schema=self._schema, + location=Location( + list_name=list_model.name, + list_item_id=list_id, + section_id=section_id, + ), + return_location=self._return_location, + language=self._language, + ).serialize(), + list_item_id=list_id, + ) + for block in blocks + ] + + related_answers_blocks[list_id] = serialized_blocks + + return related_answers_blocks + + def _get_blocks_for_related_answers(self, related_answers: tuple) -> list[dict]: + blocks = [] + answers_by_block = defaultdict(list) + + for answer in related_answers: + answer_id = answer["identifier"] + # block is not optional at this point + block: Mapping = self._schema.get_block_for_answer_id(answer_id) # type: ignore + + block_to_keep = ( + block["edit_block"] + if is_list_collector_block_editable(block) + else block + ) + answers_by_block[block_to_keep].append(answer_id) + + for immutable_block, answer_ids in answers_by_block.items(): + mutable_block = self._schema.get_mutable_deepcopy(immutable_block) + + # We need to filter out answers for both variants and normal questions + for variant_or_block in mutable_block.get( + "question_variants", [mutable_block] + ): + answers = [ + answer + for answer in variant_or_block["question"].get("answers", {}) + if answer["id"] in answer_ids + ] + # Mutate the answers to only keep the related answers + variant_or_block["question"]["answers"] = answers + + blocks.append(mutable_block) + + return blocks + + def get_repeating_block_related_answer_blocks( + self, block: ImmutableDict + ) -> list[dict]: + """ + Given a repeating block question to render, + return the list of rendered question blocks for each list item id + """ + list_name = self._schema.list_names_by_list_repeating_block_id[block["id"]] + list_model = self._data_stores.list_store[list_name] + blocks: list[dict] = [] + if answer_blocks_by_list_item_id := self._get_related_answer_blocks_by_list_item_id( + list_model=list_model, repeating_blocks=[block] + ): + for answer_blocks in answer_blocks_by_list_item_id.values(): + blocks.extend(answer_blocks) + return blocks diff --git a/app/views/contexts/summary/list_collector_block.py b/app/views/contexts/summary/list_collector_block.py new file mode 100644 index 0000000000..8d8a3baa27 --- /dev/null +++ b/app/views/contexts/summary/list_collector_block.py @@ -0,0 +1,103 @@ +from typing import Mapping + +from flask import url_for + +from app.views.contexts.summary.list_collector_base_block import ListCollectorBaseBlock + + +class ListCollectorBlock(ListCollectorBaseBlock): + # pylint: disable=too-many-locals + def list_summary_element(self, summary: Mapping) -> dict: + list_collector_block = None + ( + edit_block_id, + remove_block_id, + primary_person_edit_block_id, + related_answers, + item_label, + item_anchor, + ) = (None, None, None, None, None, None) + list_model = self._data_stores.list_store[summary["for_list"]] + + add_link = self._add_link(summary, list_collector_block) + + list_collector_blocks_on_path = self._list_collector_block_on_path( + summary["for_list"] + ) + + list_collector_block = self._list_collector_block( + summary["for_list"], list_collector_blocks_on_path + ) + + rendered_summary = self._placeholder_renderer.render( + data_to_render=summary, list_item_id=self._location.list_item_id + ) + + section_id = self._section["id"] + if list_collector_blocks_on_path: + edit_block_id = list_collector_block["edit_block"]["id"] + remove_block_id = list_collector_block["remove_block"]["id"] + add_link = self._add_link(summary, list_collector_block) + repeating_blocks = list_collector_block.get("repeating_blocks", []) + related_answers = self._get_related_answer_blocks_by_list_item_id( + list_model=list_model, repeating_blocks=repeating_blocks + ) + item_anchor = self._schema.get_item_anchor(section_id, list_model.name) + item_label = self._schema.get_item_label(section_id, list_model.name) + + if len(list_model) == 1 and list_model.primary_person: + if primary_list_collectors := self._schema.get_list_collectors_for_list_for_sections( + sections=[self._section["id"]], + for_list=summary["for_list"], + primary=True, + ): + for primary_person_block in primary_list_collectors: + primary_person_edit_block_id = edit_block_id = primary_person_block[ + "add_or_edit_block" + ]["id"] + + list_summary_context = self.list_context( + list_collector_block["summary"], + for_list=list_collector_block["for_list"], + section_id=self._location.section_id, + has_repeating_blocks=bool(list_collector_block.get("repeating_blocks")), + return_to=self._return_location.return_to, + edit_block_id=edit_block_id, + remove_block_id=remove_block_id, + primary_person_edit_block_id=primary_person_edit_block_id, + ) + + return { + "title": rendered_summary["title"], + "type": rendered_summary["type"], + "add_link": add_link, + "add_link_text": rendered_summary["add_link_text"], + "empty_list_text": rendered_summary.get("empty_list_text"), + "list_name": rendered_summary["for_list"], + "related_answers": related_answers, + "item_label": item_label, + "item_anchor": item_anchor, + **list_summary_context, + } + + def _add_link( + self, + summary: Mapping, + list_collector_block: Mapping | None, + ) -> str | None: + if list_collector_block: + return url_for( + "questionnaire.block", + list_name=summary["for_list"], + block_id=list_collector_block["add_block"]["id"], + return_to=self._return_location.return_to, + ) + + if driving_question_block := self._schema.get_driving_question_for_list( + self._section, summary["for_list"] + ): + return url_for( + "questionnaire.block", + block_id=driving_question_block["id"], + return_to=self._return_location.return_to, + ) diff --git a/app/views/contexts/summary/list_collector_content_block.py b/app/views/contexts/summary/list_collector_content_block.py new file mode 100644 index 0000000000..03e7ea8974 --- /dev/null +++ b/app/views/contexts/summary/list_collector_content_block.py @@ -0,0 +1,51 @@ +from typing import Any, Mapping + +from app.views.contexts.summary.list_collector_base_block import ListCollectorBaseBlock + + +class ListCollectorContentBlock(ListCollectorBaseBlock): + def list_summary_element(self, summary: Mapping[str, Any]) -> dict[str, Any]: + related_answers = None + + item_label = None + + current_list = self._data_stores.list_store[summary["for_list"]] + + list_collector_blocks_on_path = self._list_collector_block_on_path( + summary["for_list"] + ) + + list_collector_block = self._list_collector_block( + summary["for_list"], list_collector_blocks_on_path + ) + + rendered_summary = self._placeholder_renderer.render( + data_to_render=summary, list_item_id=self._location.list_item_id + ) + + if list_collector_blocks_on_path: + repeating_blocks = list_collector_block.get("repeating_blocks", []) + related_answers = self._get_related_answer_blocks_by_list_item_id( + list_model=current_list, repeating_blocks=repeating_blocks + ) + item_label = self._schema.get_item_label( + self._section["id"], current_list.name + ) + + list_summary_context = self.list_context( + list_collector_block["summary"], + for_list=list_collector_block["for_list"], + section_id=self._location.section_id, + has_repeating_blocks=bool(list_collector_block.get("repeating_blocks")), + return_to=self._return_location.return_to, + ) + + return { + "title": rendered_summary["title"], + "type": rendered_summary["type"], + "empty_list_text": rendered_summary.get("empty_list_text"), + "list_name": rendered_summary["for_list"], + "related_answers": related_answers, + "item_label": item_label, + **list_summary_context, + } diff --git a/app/views/contexts/summary/question.py b/app/views/contexts/summary/question.py index fe1998ae07..da54ae3790 100644 --- a/app/views/contexts/summary/question.py +++ b/app/views/contexts/summary/question.py @@ -1,57 +1,99 @@ +from typing import Any, Mapping + from flask import url_for -from markupsafe import escape +from markupsafe import Markup, escape +from werkzeug.datastructures import ImmutableDict -from app.data_models.answer import escape_answer_value +from app.data_models import AnswerStore +from app.data_models.answer import AnswerValueEscapedTypes, escape_answer_value +from app.data_models.data_stores import DataStores from app.forms.field_handlers.select_handlers import DynamicAnswerOptions -from app.views.contexts.summary.answer import Answer - - +from app.questionnaire import QuestionnaireSchema, QuestionSchemaType +from app.questionnaire.placeholder_renderer import PlaceholderRenderer +from app.questionnaire.return_location import ReturnLocation +from app.questionnaire.rules.rule_evaluator import RuleEvaluator +from app.questionnaire.value_source_resolver import ValueSourceResolver +from app.utilities.types import LocationType +from app.views.contexts.summary.answer import ( + Answer, + InferredAnswerValueTypes, + RadioCheckboxTypes, +) + + +# pylint: disable=too-many-locals class Question: def __init__( self, - question_schema, + question_schema: QuestionSchemaType, *, - answer_store, - schema, - rule_evaluator, - value_source_resolver, - location, - block_id, - return_to, - ): + data_stores: DataStores, + schema: QuestionnaireSchema, + location: LocationType, + block_id: str, + return_location: ReturnLocation, + language: str, + ) -> None: self.list_item_id = location.list_item_id if location else None self.id = question_schema["id"] self.type = question_schema["type"] self.schema = schema - self.answer_schemas = iter(question_schema["answers"]) + self.data_stores = data_stores + self.answer_schemas = iter(question_schema.get("answers", [])) + self.location = location self.summary = question_schema.get("summary") self.title = ( question_schema.get("title") or question_schema["answers"][0]["label"] ) self.number = question_schema.get("number", None) - self.rule_evaluator = rule_evaluator - self.value_source_resolver = value_source_resolver + self._rule_evaluator = RuleEvaluator( + schema=self.schema, + data_stores=data_stores, + location=self.location, + ) + + self._value_source_resolver = ValueSourceResolver( + data_stores=data_stores, + schema=self.schema, + location=self.location, + list_item_id=self.list_item_id, + use_default_answer=True, + ) + + # no need to call the method if no list item id + self._is_in_repeating_section = bool( + self.list_item_id and self.schema.is_block_in_repeating_section(block_id) + ) self.answers = self._build_answers( - answer_store=answer_store, + answer_store=self.data_stores.answer_store, question_schema=question_schema, block_id=block_id, list_name=location.list_name if location else None, - return_to=return_to, + language=language, + return_location=return_location, ) - def get_answer(self, answer_store, answer_id): + def get_answer( + self, answer_store: AnswerStore, answer_id: str, list_item_id: str | None = None + ) -> AnswerValueEscapedTypes | None: answer = answer_store.get_answer( - answer_id, self.list_item_id + answer_id, list_item_id or self.list_item_id ) or self.schema.get_default_answer(answer_id) return escape_answer_value(answer.value) if answer else None def _build_answers( - self, *, answer_store, question_schema, block_id, list_name, return_to - ): - + self, + *, + answer_store: AnswerStore, + question_schema: QuestionSchemaType, + block_id: str, + list_name: str | None, + return_location: ReturnLocation, + language: str, + ) -> list[dict[str, Any]]: if self.summary: answer_id = f"{self.id}-concatenated-answer" link = url_for( @@ -59,9 +101,10 @@ def _build_answers( list_name=list_name, block_id=block_id, list_item_id=self.list_item_id, - return_to=return_to, - return_to_answer_id=answer_id, - _anchor=answer_id, + return_to=return_location.return_to, + return_to_answer_id=answer_id if return_location.return_to else None, + return_to_list_item_id=return_location.return_to_list_item_id, + _anchor=question_schema["answers"][0]["id"], ) return [ @@ -75,8 +118,16 @@ def _build_answers( ] summary_answers = [] - for answer_schema in self.answer_schemas: - answer_value = self.get_answer(answer_store, answer_schema["id"]) + + for answer_schema in self._get_resolved_answers( + question_schema=question_schema, + language=language, + ): + list_item_id = answer_schema.get("list_item_id") + answer_id = answer_schema.get("original_answer_id") or answer_schema["id"] + answer_value = self.get_answer( + answer_store, answer_id, list_item_id=list_item_id + ) answer = self._build_answer( answer_store, question_schema, answer_schema, answer_value ) @@ -86,8 +137,9 @@ def _build_answers( answer_value=answer, block_id=block_id, list_name=list_name, - list_item_id=self.list_item_id, - return_to=return_to, + list_item_id=list_item_id or self.list_item_id, + return_location=return_location, + is_in_repeating_section=self._is_in_repeating_section, ).serialize() summary_answers.append(summary_answer) @@ -98,8 +150,9 @@ def _build_answers( return summary_answers[:-1] return summary_answers - def _concatenate_answers(self, answer_store, concatenation_type): - + def _concatenate_answers( + self, answer_store: AnswerStore, concatenation_type: str + ) -> str: answer_separators = {"Newline": "
", "Space": " "} answer_separator = answer_separators.get(concatenation_type, " ") @@ -108,20 +161,25 @@ def _concatenate_answers(self, answer_store, concatenation_type): for answer_schema in self.answer_schemas ] - values_to_concatenate = [] + values_to_concatenate: list[AnswerValueEscapedTypes] = [] for answer_value in answer_values: if not answer_value: continue - values_to_concatenate.extend( - answer_value if isinstance(answer_value, list) else [answer_value] - ) + if isinstance(answer_value, list): + values_to_concatenate.extend(answer_value) + else: + values_to_concatenate.append(answer_value) return answer_separator.join(str(value) for value in values_to_concatenate) def _build_answer( - self, answer_store, question_schema, answer_schema, answer_value=None - ): + self, + answer_store: AnswerStore, + question_schema: QuestionSchemaType, + answer_schema: Mapping[str, Any], + answer_value: AnswerValueEscapedTypes | None = None, + ) -> InferredAnswerValueTypes: if answer_value is None: return None @@ -135,40 +193,47 @@ def _build_answer( "Checkbox": self._build_checkbox_answers, "Radio": self._build_radio_answer, } - + # Type ignore: Answer value will be a Markup(String) at this stage if answer_schema["type"] in answer_builder: - return answer_builder[answer_schema["type"]]( - answer_value, answer_schema, answer_store - ) + return answer_builder[answer_schema["type"]](answer_value, answer_schema, answer_store) # type: ignore return answer_value - def _build_date_range_answer(self, answer_store, answer): + def _build_date_range_answer( + self, answer_store: AnswerStore, answer: AnswerValueEscapedTypes | None + ) -> dict[str, AnswerValueEscapedTypes | None]: next_answer = next(self.answer_schemas) to_date = self.get_answer(answer_store, next_answer["id"]) return {"from": answer, "to": to_date} def _get_dynamic_answer_options( self, - answer_schema, - ): + answer_schema: Mapping[str, Any], + ) -> tuple[dict[str, str], ...]: if not (dynamic_options_schema := answer_schema.get("dynamic_options")): return () dynamic_options = DynamicAnswerOptions( dynamic_options_schema=dynamic_options_schema, - rule_evaluator=self.rule_evaluator, - value_source_resolver=self.value_source_resolver, + rule_evaluator=self._rule_evaluator, + value_source_resolver=self._value_source_resolver, ) return dynamic_options.evaluate() - def get_answer_options(self, answer_schema): + def get_answer_options( + self, answer_schema: Mapping[str, Any] + ) -> tuple[dict[str, str], ...]: return tuple(answer_schema.get("options", ())) + tuple( self._get_dynamic_answer_options(answer_schema) ) - def _build_checkbox_answers(self, answer, answer_schema, answer_store): + def _build_checkbox_answers( + self, + answer: Markup, + answer_schema: Mapping[str, Any], + answer_store: AnswerStore, + ) -> list[RadioCheckboxTypes] | None: multiple_answers = [] for option in self.get_answer_options(answer_schema): if escape(option["value"]) in answer: @@ -185,7 +250,12 @@ def _build_checkbox_answers(self, answer, answer_schema, answer_store): return multiple_answers or None - def _build_radio_answer(self, answer, answer_schema, answer_store): + def _build_radio_answer( + self, + answer: Markup, + answer_schema: Mapping[str, Any], + answer_store: AnswerStore, + ) -> RadioCheckboxTypes | None: for option in self.get_answer_options(answer_schema): if answer == escape(option["value"]): detail_answer_value = self._get_detail_answer_value( @@ -196,16 +266,42 @@ def _build_radio_answer(self, answer, answer_schema, answer_store): "detail_answer_value": detail_answer_value, } - def _get_detail_answer_value(self, option, answer_store): + def _get_detail_answer_value( + self, option: dict, answer_store: AnswerStore + ) -> AnswerValueEscapedTypes | None: if "detail_answer" in option: return self.get_answer(answer_store, option["detail_answer"]["id"]) - def _build_dropdown_answer(self, answer, answer_schema): + def _build_dropdown_answer( + self, + answer: AnswerValueEscapedTypes | None, + answer_schema: Mapping[str, Any], + ) -> str | None: for option in self.get_answer_options(answer_schema): if answer == option["value"]: return option["label"] - def serialize(self): + def _get_resolved_answers( + self, + *, + question_schema: QuestionSchemaType, + language: str, + ) -> Any: + resolved_question = ImmutableDict({"answers": self.answer_schemas}) + + if "dynamic_answers" in question_schema: + placeholder_renderer = PlaceholderRenderer( + data_stores=self.data_stores, language=language, schema=self.schema + ) + + resolved_question = ImmutableDict( + placeholder_renderer.render( + data_to_render=question_schema, list_item_id=self.list_item_id + ) + ) + return resolved_question["answers"] + + def serialize(self) -> dict[str, Any]: return { "id": self.id, "type": self.type, diff --git a/app/views/contexts/summary_context.py b/app/views/contexts/summary_context.py index a3b3c8f19d..1bd5919009 100644 --- a/app/views/contexts/summary_context.py +++ b/app/views/contexts/summary_context.py @@ -1,49 +1,93 @@ -from typing import Generator, Mapping, Optional, Union - +from app.data_models.data_stores import DataStores from app.questionnaire.location import Location - -from .context import Context -from .section_summary_context import SectionSummaryContext +from app.questionnaire.questionnaire_schema import QuestionnaireSchema +from app.views.contexts.context import Context +from app.views.contexts.section_summary_context import SectionSummaryContext class SummaryContext(Context): - def __call__( - self, answers_are_editable: bool = False, return_to: Optional[str] = None - ) -> dict[str, Union[str, list, bool]]: + def __init__( + self, + language: str, + schema: QuestionnaireSchema, + data_stores: DataStores, + view_submitted_response: bool, + ) -> None: + super().__init__( + language, + schema, + data_stores, + ) + self.view_submitted_response = view_submitted_response + self.summaries: list[dict] = [] - groups = list(self._build_all_groups(return_to)) + def __call__( + self, answers_are_editable: bool = False, return_to: str | None = None + ) -> dict[str, dict | str | list | bool]: + self._build_all_groups(return_to) summary_options = self._schema.get_summary_options() collapsible = summary_options.get("collapsible", False) + self.set_unique_group_ids() + return { - "groups": groups, + "sections": self.summaries, "answers_are_editable": answers_are_editable, "collapsible": collapsible, "summary_type": "Summary", + "view_submitted_response": self.view_submitted_response, } - def _build_all_groups( - self, return_to: Optional[str] - ) -> Generator[dict, None, None]: - """NB: Does not support repeating sections""" - + def _build_all_groups(self, return_to: str | None) -> None: for section_id in self._router.enabled_section_ids: - location = Location(section_id=section_id) - section_summary_context = SectionSummaryContext( - language=self._language, - schema=self._schema, - answer_store=self._answer_store, - list_store=self._list_store, - progress_store=self._progress_store, - metadata=self._metadata, - response_metadata=self._response_metadata, - current_location=location, - routing_path=self._router.routing_path(section_id), - ) - section: Mapping = self._schema.get_section(section_id) or {} - if section.get("summary", {}).get("items"): - break - - for group in section_summary_context(return_to=return_to)["summary"][ - "groups" - ]: - yield group + if repeat := self._schema.get_repeat_for_section(section_id): + for_repeat = self._data_stores.list_store[repeat["for_list"]] + + if for_repeat.count > 0: + for item in for_repeat.items: + self.build_summary_item( + section_id=section_id, + return_to=return_to, + list_item_id=item, + list_name=for_repeat.name, + ) + else: + self.build_summary_item(section_id=section_id, return_to=return_to) + + def build_summary_item( + self, + section_id: str, + return_to: str | None, + list_name: str | None = None, + list_item_id: str | None = None, + ) -> None: + location = Location( + section_id=section_id, list_name=list_name, list_item_id=list_item_id + ) + section_summary_context = SectionSummaryContext( + language=self._language, + schema=self._schema, + data_stores=self._data_stores, + current_location=location, + routing_path=self._router.routing_path(location.section_key), + ) + + summary = section_summary_context( + view_submitted_response=self.view_submitted_response, return_to=return_to + )["summary"] + + for section in summary.get("sections", []): + if any(group["blocks"] for group in section["groups"]): + self.summaries.extend(summary["sections"]) + + def set_unique_group_ids(self) -> None: + checked_ids = set() + id_value = 0 + + for section in self.summaries: + if groups := section.get("groups"): + for group in groups: + group_id = group["id"] + if group_id in checked_ids: + id_value += 1 + group["id"] = f"{group_id}-{id_value}" + checked_ids.add(group_id) diff --git a/app/views/contexts/thank_you_context.py b/app/views/contexts/thank_you_context.py index 1fe1bb98dc..925208f1e4 100644 --- a/app/views/contexts/thank_you_context.py +++ b/app/views/contexts/thank_you_context.py @@ -1,15 +1,17 @@ from datetime import datetime -from typing import Mapping, Optional +from typing import Any from flask import url_for from flask_babel import lazy_gettext -from app.data_models.session_data import SessionData +from app.data_models.metadata_proxy import MetadataProxy +from app.forms.email_form import EmailForm from app.globals import ( get_view_submitted_response_expiration_time, has_view_submitted_response_expired, ) from app.questionnaire import QuestionnaireSchema +from app.survey_config.survey_type import SurveyType from app.views.contexts.email_form_context import build_email_form_context from app.views.contexts.submission_metadata_context import ( build_submission_metadata_context, @@ -18,37 +20,53 @@ def build_thank_you_context( schema: QuestionnaireSchema, - session_data: SessionData, + metadata: MetadataProxy, submitted_at: datetime, - survey_type: str, - guidance_content: Optional[dict] = None, -) -> Mapping: - if survey_type == "social": - submission_text = lazy_gettext("Your answers have been submitted.") - elif session_data.trad_as and session_data.ru_name: + survey_type: SurveyType, + guidance_content: dict | None = None, + confirmation_email_form: EmailForm | None = None, +) -> dict[str, Any]: + if (ru_name := metadata["ru_name"]) and (trad_as := metadata["trad_as"]): submission_text = lazy_gettext( "Your answers have been submitted for {company_name} ({trading_name})" - ).format(company_name=session_data.ru_name, trading_name=session_data.trad_as) - else: + ).format( + company_name=ru_name, + trading_name=trad_as, + ) + elif ru_name: submission_text = lazy_gettext( "Your answers have been submitted for {company_name}" - ).format(company_name=session_data.ru_name) - metadata = build_submission_metadata_context( - survey_type, submitted_at, session_data.tx_id # type: ignore + ).format(company_name=ru_name) + else: + submission_text = lazy_gettext("Your answers have been submitted.") + context_metadata = build_submission_metadata_context( + survey_type, + submitted_at, + metadata.tx_id, ) - return { + + context = { "hide_sign_out_button": True, "submission_text": submission_text, - "metadata": metadata, + "metadata": context_metadata, "guidance": guidance_content, "view_submitted_response": build_view_submitted_response_context( schema, submitted_at ), } + if confirmation_email_form: + context["confirmation_email_form"] = build_email_form_context( + confirmation_email_form + ) + return context -def build_view_submitted_response_context(schema, submitted_at): - view_submitted_response = {"enabled": schema.is_view_submitted_response_enabled} +def build_view_submitted_response_context( + schema: QuestionnaireSchema, submitted_at: datetime +) -> dict[str, bool | str]: + view_submitted_response: dict[str, bool | str] = { + "enabled": schema.is_view_submitted_response_enabled + } if schema.is_view_submitted_response_enabled: expired = has_view_submitted_response_expired(submitted_at) @@ -63,17 +81,3 @@ def build_view_submitted_response_context(schema, submitted_at): "post_submission.get_view_submitted_response" ) return view_submitted_response - - -def build_census_thank_you_context( - session_data: SessionData, confirmation_email_form, form_type -) -> Mapping: - context = { - "display_address": session_data.display_address, - "form_type": form_type, - "hide_sign_out_button": False, - "sign_out_url": url_for("session.get_sign_out"), - } - if confirmation_email_form: - context.update(build_email_form_context(confirmation_email_form)) - return context diff --git a/app/views/contexts/view_submitted_response_context.py b/app/views/contexts/view_submitted_response_context.py index 2c6805d893..fc2a348d37 100644 --- a/app/views/contexts/view_submitted_response_context.py +++ b/app/views/contexts/view_submitted_response_context.py @@ -1,12 +1,13 @@ from datetime import datetime -from typing import Union from flask import url_for from flask_babel import lazy_gettext from app.data_models import QuestionnaireStore +from app.data_models.metadata_proxy import NoMetadataException from app.globals import has_view_submitted_response_expired from app.questionnaire.questionnaire_schema import QuestionnaireSchema +from app.survey_config.survey_type import SurveyType from app.views.contexts.submission_metadata_context import ( build_submission_metadata_context, ) @@ -17,28 +18,31 @@ def build_view_submitted_response_context( language: str, schema: QuestionnaireSchema, questionnaire_store: QuestionnaireStore, - survey_type: str, -) -> dict[str, Union[str, datetime, dict]]: - + survey_type: SurveyType, +) -> dict[str, str | datetime | dict]: view_submitted_response_expired = has_view_submitted_response_expired( questionnaire_store.submitted_at # type: ignore ) - if survey_type == "social": - submitted_text = lazy_gettext("Answers submitted.") - elif trad_as := questionnaire_store.metadata.get("trad_as"): + metadata = questionnaire_store.data_stores.metadata + if not metadata: + raise NoMetadataException + + if (ru_name := metadata["ru_name"]) and (trad_as := metadata["trad_as"]): submitted_text = lazy_gettext( "Answers submitted for {ru_name} ({trad_as})" - ).format(ru_name=questionnaire_store.metadata["ru_name"], trad_as=trad_as) - else: + ).format(ru_name=ru_name, trad_as=trad_as) + elif ru_name: submitted_text = lazy_gettext( "Answers submitted for {ru_name}" - ).format(ru_name=questionnaire_store.metadata["ru_name"]) + ).format(ru_name=ru_name) + else: + submitted_text = lazy_gettext("Answers submitted.") metadata = build_submission_metadata_context( survey_type, questionnaire_store.submitted_at, # type: ignore - questionnaire_store.metadata["tx_id"], + metadata.tx_id, ) context = { "hide_sign_out_button": True, @@ -53,11 +57,8 @@ def build_view_submitted_response_context( summary_context = SummaryContext( language=language, schema=schema, - answer_store=questionnaire_store.answer_store, - list_store=questionnaire_store.list_store, - progress_store=questionnaire_store.progress_store, - metadata=questionnaire_store.metadata, # type: ignore - response_metadata=questionnaire_store.response_metadata, + data_stores=questionnaire_store.data_stores, + view_submitted_response=True, ) context["summary"] = summary_context() context["pdf_url"] = url_for("post_submission.get_view_submitted_response_pdf") diff --git a/app/views/handlers/__init__.py b/app/views/handlers/__init__.py index fd05706cd8..12202065dc 100644 --- a/app/views/handlers/__init__.py +++ b/app/views/handlers/__init__.py @@ -1,10 +1,7 @@ -from typing import Union - from flask import url_for from app.data_models import QuestionnaireStore - -from .individual_response import ( +from app.views.handlers.individual_response import ( IndividualResponseHandler, IndividualResponseHowHandler, IndividualResponsePostAddressConfirmHandler, @@ -19,16 +16,15 @@ def individual_response_url( - individual_response_for_list: str, + individual_response_for_list: str | None, list_item_id: str, questionnaire_store: QuestionnaireStore, - journey: str = None, -) -> Union[str, None]: + journey: str | None = None, +) -> str | None: if individual_response_for_list: - if ( list_item_id - != questionnaire_store.list_store[ + != questionnaire_store.data_stores.list_store[ individual_response_for_list ].primary_person ): diff --git a/app/views/handlers/block.py b/app/views/handlers/block.py index 1133c32eed..c2104ec0b9 100644 --- a/app/views/handlers/block.py +++ b/app/views/handlers/block.py @@ -1,8 +1,9 @@ from datetime import datetime, timezone from functools import cached_property -from typing import MutableMapping, Optional, Union +from typing import Mapping, MutableMapping from structlog import get_logger +from werkzeug.datastructures import ImmutableDict, ImmutableMultiDict from app.data_models import QuestionnaireStore from app.questionnaire.location import InvalidLocationException, Location @@ -10,8 +11,11 @@ from app.questionnaire.questionnaire_schema import QuestionnaireSchema from app.questionnaire.questionnaire_store_updater import QuestionnaireStoreUpdater from app.questionnaire.relationship_location import RelationshipLocation +from app.questionnaire.return_location import ReturnLocation from app.questionnaire.router import Router +from app.questionnaire.routing_path import RoutingPath from app.utilities import safe_content +from app.utilities.types import LocationType logger = get_logger() @@ -22,9 +26,9 @@ def __init__( schema: QuestionnaireSchema, questionnaire_store: QuestionnaireStore, language: str, - current_location: Union[Location, RelationshipLocation], + current_location: Location, request_args: MutableMapping, - form_data: MutableMapping, + form_data: ImmutableMultiDict, ): self._schema = schema self._questionnaire_store = questionnaire_store @@ -34,115 +38,122 @@ def __init__( self._form_data = form_data if self._current_location.block_id: - self.block = self._schema.get_block(self._current_location.block_id) + # Type ignore: Block has to exist at this point. Block existence is checked beforehand in block_factory.py + self.block: ImmutableDict = self._schema.get_block(self._current_location.block_id) # type: ignore self._routing_path = self._get_routing_path() - self.page_title = None - self._return_to = request_args.get("return_to") - self._return_to_answer_id = request_args.get("return_to_answer_id") - self.resume = "resume" in request_args + self.page_title: str | None = None + + self._return_location = ReturnLocation( + return_to=request_args.get("return_to"), + return_to_block_id=request_args.get("return_to_block_id"), + return_to_answer_id=request_args.get("return_to_answer_id"), + return_to_list_item_id=request_args.get("return_to_list_item_id"), + ) + self.resume = "resume" in request_args + location_error_message = f"location {self._current_location} is not valid" if not self.is_location_valid(): - raise InvalidLocationException( - f"location {self._current_location} is not valid" - ) + raise InvalidLocationException(location_error_message) @property - def current_location(self): + def current_location(self) -> LocationType: return self._current_location + @property + def return_location(self) -> ReturnLocation: + return self._return_location + @cached_property - def questionnaire_store_updater(self): + def questionnaire_store_updater(self) -> QuestionnaireStoreUpdater: return QuestionnaireStoreUpdater( self._current_location, self._schema, self._questionnaire_store, + self.router, self.block.get("question"), ) @cached_property - def placeholder_renderer(self): + def placeholder_renderer(self) -> PlaceholderRenderer: return PlaceholderRenderer( self._language, - answer_store=self._questionnaire_store.answer_store, - list_store=self._questionnaire_store.list_store, - metadata=self._questionnaire_store.metadata, - response_metadata=self._questionnaire_store.response_metadata, + data_stores=self._questionnaire_store.data_stores, schema=self._schema, location=self._current_location, ) @cached_property - def router(self): + def router(self) -> Router: return Router( schema=self._schema, - answer_store=self._questionnaire_store.answer_store, - list_store=self._questionnaire_store.list_store, - progress_store=self._questionnaire_store.progress_store, - metadata=self._questionnaire_store.metadata, - response_metadata=self._questionnaire_store.response_metadata, + data_stores=self._questionnaire_store.data_stores, ) - def is_location_valid(self): + def is_location_valid(self) -> bool: return self.router.can_access_location( self._current_location, self._routing_path ) - def get_previous_location_url(self): + def get_previous_location_url(self) -> str | None: return self.router.get_previous_location_url( self._current_location, self._routing_path, - self._return_to, - self._return_to_answer_id, + self.return_location, ) - def get_next_location_url(self): + def get_next_location_url(self) -> str: return self.router.get_next_location_url( self._current_location, self._routing_path, - self._return_to, + self.return_location, ) - def handle_post(self): + def handle_post(self) -> None: self._set_started_at_metadata() - self.questionnaire_store_updater.add_completed_location() + self.questionnaire_store_updater.add_completed_location(self._current_location) + self.questionnaire_store_updater.remove_dependent_blocks_and_capture_dependent_sections() self._update_section_completeness() + self.questionnaire_store_updater.update_progress_for_dependent_sections() self.questionnaire_store_updater.save() - def _get_routing_path(self): - return self.router.routing_path( - section_id=self._current_location.section_id, - list_item_id=self._current_location.list_item_id, - ) + def _get_routing_path(self) -> RoutingPath: + return self.router.routing_path(self._current_location.section_key) def _update_section_completeness( - self, location: Optional[Union[Location, RelationshipLocation]] = None - ): + self, location: Location | RelationshipLocation | None = None + ) -> None: location = location or self._current_location self.questionnaire_store_updater.update_section_status( is_complete=self.router.is_path_complete(self._routing_path), - section_id=location.section_id, - list_item_id=location.list_item_id, + section_key=location.section_key, ) - def _set_started_at_metadata(self): - response_metadata = self._questionnaire_store.response_metadata + def _set_started_at_metadata(self) -> str | None: + response_metadata = self._questionnaire_store.data_stores.response_metadata if not response_metadata.get("started_at"): started_at = datetime.now(timezone.utc).isoformat() logger.info("Survey started", started_at=started_at) response_metadata["started_at"] = started_at - def _get_safe_page_title(self, page_title): + def _get_safe_page_title(self, page_title: Mapping | str) -> str: page_title = self._schema.get_single_string_value(page_title) return safe_content(page_title) def _resolve_custom_page_title_vars(self) -> MutableMapping: - list_item_position = self._questionnaire_store.list_store.list_item_position( - self.current_location.list_name, self.current_location.list_item_id + # Type ignore: list_item_id and list_name are populated at this stage + list_item_position = ( + self._questionnaire_store.data_stores.list_store.list_item_position( + self.current_location.list_name, # type: ignore + self.current_location.list_item_id, # type: ignore + ) ) return {"list_item_position": list_item_position} - def _set_page_title(self, page_title): + def _set_page_title(self, page_title: str | None) -> str | None: + if not page_title: + return None + section_repeating_page_title = ( self._schema.get_repeating_page_title_for_section( self._current_location.section_id diff --git a/app/views/handlers/block_factory.py b/app/views/handlers/block_factory.py index 4aed203139..aa1300aa44 100644 --- a/app/views/handlers/block_factory.py +++ b/app/views/handlers/block_factory.py @@ -1,11 +1,22 @@ +from typing import Any + +from werkzeug.datastructures import ImmutableMultiDict, MultiDict + +from app.data_models import QuestionnaireStore +from app.questionnaire import QuestionnaireSchema from app.questionnaire.location import InvalidLocationException, Location from app.questionnaire.relationship_location import RelationshipLocation -from app.views.handlers.calculated_summary import CalculatedSummary +from app.views.handlers.calculation_summary import ( + CalculatedSummary, + GrandCalculatedSummary, +) from app.views.handlers.content import Content from app.views.handlers.list_add_question import ListAddQuestion from app.views.handlers.list_collector import ListCollector +from app.views.handlers.list_collector_content import ListCollectorContent from app.views.handlers.list_edit_question import ListEditQuestion from app.views.handlers.list_remove_question import ListRemoveQuestion +from app.views.handlers.list_repeating_question import ListRepeatingQuestion from app.views.handlers.primary_person_list_collector import PrimaryPersonListCollector from app.views.handlers.primary_person_question import PrimaryPersonQuestion from app.views.handlers.question import Question @@ -16,9 +27,11 @@ "ConfirmationQuestion": Question, "ListCollectorDrivingQuestion": Question, "ListCollector": ListCollector, + "ListCollectorContent": ListCollectorContent, "ListAddQuestion": ListAddQuestion, "ListEditQuestion": ListEditQuestion, "ListRemoveQuestion": ListRemoveQuestion, + "ListRepeatingQuestion": ListRepeatingQuestion, "PrimaryPersonListCollector": PrimaryPersonListCollector, "PrimaryPersonListAddOrEditQuestion": PrimaryPersonQuestion, "RelationshipCollector": RelationshipCollector, @@ -26,44 +39,47 @@ "Introduction": Content, "Interstitial": Content, "CalculatedSummary": CalculatedSummary, + "GrandCalculatedSummary": GrandCalculatedSummary, } def get_block_handler( - schema, - block_id, - list_item_id, - questionnaire_store, - language, - list_name=None, - to_list_item_id=None, - request_args=None, - form_data=None, -): + schema: QuestionnaireSchema, + block_id: str, + list_item_id: str | None, + questionnaire_store: QuestionnaireStore, + language: str | None, + list_name: str | None = None, + to_list_item_id: str | None = None, + request_args: MultiDict[str, str] | None = None, + form_data: ImmutableMultiDict[str, str] | None = None, +) -> Any: block = schema.get_block(block_id) + block_error_message = { + "invalid_block_id": f"block id {block_id} is not valid for this schema", + "invalid_list": f"block id {block_id} is in a repeating section without valid list_name/list_item_id", + "invalid_block_type": f"block id {block_id} is not valid for this schema", + } if not block: - raise InvalidLocationException( - f"block id {block_id} is not valid for this schema" - ) + raise InvalidLocationException(block_error_message["invalid_block_id"]) if schema.is_block_in_repeating_section(block_id=block["id"]) and not all( (list_name, list_item_id) ): - raise InvalidLocationException( - f"block id {block_id} is in a repeating section without valid list_name/list_item_id" - ) + raise InvalidLocationException(block_error_message["invalid_list"]) block_type = block["type"] block_class = BLOCK_MAPPINGS.get(block_type) if not block_class: - raise ValueError(f"block type {block_type} is not valid") + raise ValueError(block_error_message["invalid_block_type"]) section_id = schema.get_section_id_for_block_id(block_id) if to_list_item_id or block_type == "UnrelatedQuestion": location = RelationshipLocation( - section_id=section_id, + # Type ignore: Block is fetched from schema so must have a corresponding section + section_id=section_id, # type: ignore block_id=block_id, list_item_id=list_item_id, to_list_item_id=to_list_item_id, @@ -71,7 +87,8 @@ def get_block_handler( ) else: location = Location( - section_id=section_id, + # Type ignore: Block is fetched from schema so must have a corresponding section + section_id=section_id, # type: ignore block_id=block_id, list_name=list_name, list_item_id=list_item_id, diff --git a/app/views/handlers/calculated_summary.py b/app/views/handlers/calculated_summary.py deleted file mode 100644 index 65084badc6..0000000000 --- a/app/views/handlers/calculated_summary.py +++ /dev/null @@ -1,18 +0,0 @@ -from app.views.contexts.calculated_summary_context import CalculatedSummaryContext -from app.views.handlers.content import Content - - -class CalculatedSummary(Content): - def get_context(self): - calculated_summary_context = CalculatedSummaryContext( - self._language, - self._schema, - self._questionnaire_store.answer_store, - self._questionnaire_store.list_store, - self._questionnaire_store.progress_store, - self._questionnaire_store.metadata, - self._questionnaire_store.response_metadata, - ) - return calculated_summary_context.build_view_context_for_calculated_summary( - self._current_location - ) diff --git a/app/views/handlers/calculation_summary.py b/app/views/handlers/calculation_summary.py new file mode 100644 index 0000000000..f40fd3b788 --- /dev/null +++ b/app/views/handlers/calculation_summary.py @@ -0,0 +1,39 @@ +from app.views.contexts import GrandCalculatedSummaryContext +from app.views.contexts.calculated_summary_context import CalculatedSummaryContext +from app.views.handlers.content import Content + + +class _SummaryWithCalculation(Content): + summary_class: type[CalculatedSummaryContext] | type[GrandCalculatedSummaryContext] + + def get_context(self) -> dict[str, dict]: + summary_context = self.summary_class( + language=self._language, + schema=self._schema, + data_stores=self._questionnaire_store.data_stores, + current_location=self._current_location, + routing_path=self._routing_path, + return_location=self._return_location, + rendered_block=self.rendered_block, + ) + context = summary_context.build_view_context() + + if not self.page_title: + self.page_title = context["summary"]["calculated_question"]["title"] + + return context + + def handle_post(self) -> None: + # We prematurely set the current as complete, so that dependent sections can be updated accordingly + self.questionnaire_store_updater.add_completed_location(self.current_location) + # Then we update dependent sections + self.questionnaire_store_updater.capture_progress_section_dependencies() + return super().handle_post() + + +class CalculatedSummary(_SummaryWithCalculation): + summary_class = CalculatedSummaryContext + + +class GrandCalculatedSummary(_SummaryWithCalculation): + summary_class = GrandCalculatedSummaryContext diff --git a/app/views/handlers/confirm_email.py b/app/views/handlers/confirm_email.py index d1054d54e7..45d2c31457 100644 --- a/app/views/handlers/confirm_email.py +++ b/app/views/handlers/confirm_email.py @@ -4,10 +4,16 @@ from flask import current_app, url_for from flask_babel import gettext, lazy_gettext +from google.cloud.tasks_v2.types.task import Task from itsdangerous import BadSignature from markupsafe import escape +from werkzeug.datastructures import MultiDict from werkzeug.exceptions import BadRequest +from app.cloud_tasks.cloud_task_publishers import ( + CloudTaskPublisher, + LogCloudTaskPublisher, +) from app.cloud_tasks.exceptions import CloudTaskCreationFailed from app.data_models import ( FulfilmentRequest, @@ -15,9 +21,10 @@ SessionData, SessionStore, ) -from app.forms.questionnaire_form import generate_form +from app.data_models.metadata_proxy import MetadataProxy +from app.forms.questionnaire_form import QuestionnaireForm, generate_form from app.helpers import url_safe_serializer -from app.questionnaire import QuestionnaireSchema, QuestionSchema +from app.questionnaire import QuestionnaireSchema, QuestionSchemaType from app.settings import ( EQ_SUBMISSION_CONFIRMATION_CLOUD_FUNCTION_NAME, EQ_SUBMISSION_CONFIRMATION_QUEUE, @@ -34,7 +41,7 @@ class ConfirmationEmailFulfilmentRequestPublicationFailed(Exception): pass -CONFIRM_EMAIL_YES_VALUE = lazy_gettext("Yes, send the confirmation email") +CONFIRM_EMAIL_YES_VALUE: str = lazy_gettext("Yes, send the confirmation email") class ConfirmEmail: @@ -43,10 +50,9 @@ def __init__( questionnaire_store: QuestionnaireStore, schema: QuestionnaireSchema, session_store: SessionStore, - serialized_email, - form_data: Mapping, + serialized_email: str, + form_data: MultiDict, ): - if not ConfirmationEmail.is_enabled(schema): raise ConfirmationEmailNotEnabled @@ -65,23 +71,19 @@ def __init__( self._session_store = session_store self._form_data = form_data self._email = email - self.page_title = lazy_gettext("Confirm your email address") + self.page_title: str = lazy_gettext("Confirm your email address") @cached_property - def form(self): + def form(self) -> QuestionnaireForm: return generate_form( schema=self._schema, question_schema=self.question_schema, - answer_store=self._questionnaire_store.answer_store, - list_store=self._questionnaire_store.list_store, - metadata=self._questionnaire_store.metadata, - response_metadata=self._questionnaire_store.metadata, - data=None, + data_stores=self._questionnaire_store.data_stores, form_data=self._form_data, ) @cached_property - def question_schema(self) -> QuestionSchema: + def question_schema(self) -> QuestionSchemaType: return { "type": "General", "id": "confirm-email", @@ -107,13 +109,13 @@ def question_schema(self) -> QuestionSchema: } @cached_property - def is_email_correct(self): + def is_email_correct(self) -> bool: return self._form_data.get("confirm-email") == CONFIRM_EMAIL_YES_VALUE - def get_context(self): + def get_context(self) -> dict: return build_confirm_email_context(self.question_schema, self.form) - def get_next_location_url(self): + def get_next_location_url(self) -> str: if self.is_email_correct: return url_for( ".get_confirmation_email_sent", @@ -121,26 +123,35 @@ def get_next_location_url(self): ) return url_for(".send_confirmation_email", email=self._serialized_email) - def get_page_title(self): - # pylint: disable=no-member - # wtforms Form parents are not discoverable in the 2.3.3 implementation + def get_page_title(self) -> str: if self.form.errors: - return gettext("Error: {page_title}").format(page_title=self.page_title) + formatted_errors: str = gettext("Error: {page_title}").format( + page_title=self.page_title + ) + return formatted_errors return self.page_title - def handle_post(self): + def handle_post(self) -> None: if self.is_email_correct: self._publish_fulfilment_request() - self._session_store.session_data.confirmation_email_count += 1 + # Type ignore: session data would be populated at this point + self._session_store.session_data.confirmation_email_count += 1 # type: ignore self._session_store.save() - def _publish_fulfilment_request(self): + def _publish_fulfilment_request(self) -> Task | None: fulfilment_request = ConfirmationEmailFulfilmentRequest( - self._email, self._session_store.session_data, self._schema + self._email, + # Type ignore: session data would be populated at this point + self._session_store.session_data, # type: ignore + # Type ignore: metadata will be populated as we reach this stage + self._questionnaire_store.data_stores.metadata, # type: ignore + self._schema, ) try: - return current_app.eq["cloud_tasks"].create_task( + # Type ignore: mypy not aware of eq attribute but it is a cloud task publisher + cloud_task_publisher: CloudTaskPublisher | LogCloudTaskPublisher = current_app.eq["cloud_tasks"] # type: ignore + return cloud_task_publisher.create_task( body=fulfilment_request.message, queue_name=EQ_SUBMISSION_CONFIRMATION_QUEUE, function_name=EQ_SUBMISSION_CONFIRMATION_CLOUD_FUNCTION_NAME, @@ -154,16 +165,17 @@ def _publish_fulfilment_request(self): class ConfirmationEmailFulfilmentRequest(FulfilmentRequest): email_address: str session_data: SessionData + metadata: MetadataProxy schema: QuestionnaireSchema def _payload(self) -> Mapping: return { "fulfilmentRequest": { "email_address": self.email_address, - "display_address": self.session_data.display_address, + "display_address": self.metadata["display_address"], "form_type": self.schema.form_type, "language_code": self.session_data.language_code, "region_code": self.schema.region_code, - "tx_id": self.session_data.tx_id, + "tx_id": self.metadata.tx_id, } } diff --git a/app/views/handlers/confirmation_email.py b/app/views/handlers/confirmation_email.py index 2651480b9c..f6213074a8 100644 --- a/app/views/handlers/confirmation_email.py +++ b/app/views/handlers/confirmation_email.py @@ -1,8 +1,7 @@ from functools import cached_property -from typing import Optional from flask import current_app -from flask_babel import gettext, lazy_gettext +from flask_babel import LazyString, gettext, lazy_gettext from itsdangerous import BadSignature from werkzeug.exceptions import BadRequest @@ -26,13 +25,13 @@ def __init__( self, session_store: SessionStore, schema: QuestionnaireSchema, - page_title: Optional[str] = None, - serialised_email: Optional[str] = None, + page_title: str | None = None, + serialised_email: str | None = None, ): - if not self.is_enabled(schema): raise ConfirmationEmailNotEnabled + # Type ignore: session_data is populated at login therefore won't be None for confirmation email if self.is_limit_reached(session_store.session_data): # type: ignore raise ConfirmationEmailLimitReached @@ -42,11 +41,11 @@ def __init__( self._serialised_email = serialised_email @property - def page_title(self): + def page_title(self) -> str: return self._page_title or lazy_gettext("Confirmation email") @cached_property - def form(self): + def form(self) -> EmailForm: if self._serialised_email: try: email = url_safe_serializer().loads(self._serialised_email) @@ -55,28 +54,25 @@ def form(self): return EmailForm(email=email) return EmailForm() - def get_context(self): + def get_context(self) -> dict[str, bool | str]: return build_confirmation_email_form_context(self.form) - def get_url_safe_serialized_email(self): + def get_url_safe_serialized_email(self) -> str | bytes: return url_safe_serializer().dumps(self.form.email.data) - def get_page_title(self): - # pylint: disable=no-member - # wtforms Form parents are not discoverable in the 2.3.3 implementation + def get_page_title(self) -> str | LazyString | None: if self.form.errors: return gettext("Error: {page_title}").format(page_title=self.page_title) return self.page_title @staticmethod def is_limit_reached(session_data: SessionData) -> bool: - return ( - session_data.confirmation_email_count - >= current_app.config["CONFIRMATION_EMAIL_LIMIT"] - ) + # Type ignore: confirmation_email_count already declared an int + return session_data.confirmation_email_count >= current_app.config["CONFIRMATION_EMAIL_LIMIT"] # type: ignore @staticmethod def is_enabled(schema: QuestionnaireSchema) -> bool: if submission_schema := schema.get_post_submission(): - return submission_schema.get("confirmation_email", False) + # Type ignore: the type of the .get() returned value is Any + return submission_schema.get("confirmation_email", False) # type: ignore return False diff --git a/app/views/handlers/content.py b/app/views/handlers/content.py index 55c9be7a9c..1c192fbbb5 100644 --- a/app/views/handlers/content.py +++ b/app/views/handlers/content.py @@ -1,5 +1,7 @@ from functools import cached_property +from werkzeug.datastructures import ImmutableDict + from app.questionnaire.variants import transform_variants from app.views.handlers import individual_response_url from app.views.handlers.block import BlockHandler @@ -7,14 +9,11 @@ class Content(BlockHandler): @cached_property - def rendered_block(self): + def rendered_block(self) -> dict: transformed_block = transform_variants( self.block, self._schema, - self._questionnaire_store.metadata, - self._questionnaire_store.response_metadata, - self._questionnaire_store.answer_store, - self._questionnaire_store.list_store, + self._questionnaire_store.data_stores, self._current_location, ) @@ -23,28 +22,30 @@ def rendered_block(self): ) or self._get_content_title(transformed_block) self._set_page_title(content_page_title) return self.placeholder_renderer.render( - transformed_block, self._current_location.list_item_id + data_to_render=transformed_block, + list_item_id=self._current_location.list_item_id, ) - def get_context(self): + def get_context(self) -> dict: return { "block": self.rendered_block, - "metadata": dict(self._questionnaire_store.metadata), - "individual_response_url": individual_response_url( - self._schema.get_individual_response_list(), - self._current_location.list_item_id, - self._questionnaire_store, - ) - if self._is_block_first_block_in_individual_response() - else None, + "individual_response_url": ( + individual_response_url( + self._schema.get_individual_response_list(), + self._current_location.list_item_id, # type: ignore + self._questionnaire_store, + ) + if self._is_block_first_block_in_individual_response() + else None + ), } - def _get_content_title(self, transformed_block): + def _get_content_title(self, transformed_block: ImmutableDict) -> str | None: content = transformed_block.get("content") if content: return self._get_safe_page_title(content["title"]) - def _is_block_first_block_in_individual_response(self): + def _is_block_first_block_in_individual_response(self) -> bool: individual_section_id = ( self._schema.get_individual_response_individual_section_id() ) diff --git a/app/views/handlers/feedback.py b/app/views/handlers/feedback.py index 42340440a4..72a63f32fc 100644 --- a/app/views/handlers/feedback.py +++ b/app/views/handlers/feedback.py @@ -1,13 +1,15 @@ from datetime import datetime, timezone from functools import cached_property -from typing import Any, Mapping, Optional, Union +from typing import Any, Mapping, MutableMapping from flask import current_app from flask_babel import gettext, lazy_gettext from sdc.crypto.encrypter import encrypt from werkzeug.datastructures import MultiDict +from app.authentication.auth_payload_versions import AuthPayloadVersion from app.data_models import QuestionnaireStore +from app.data_models.metadata_proxy import MetadataProxy, NoMetadataException from app.data_models.session_data import SessionData from app.data_models.session_store import SessionStore from app.forms.questionnaire_form import QuestionnaireForm, generate_form @@ -16,12 +18,9 @@ DEFAULT_LANGUAGE_CODE, QuestionnaireSchema, ) -from app.submitter.converter import ( - build_collection, - build_metadata, - get_optional_payload_properties, -) +from app.submitter import GCSFeedbackSubmitter, LogFeedbackSubmitter, converter_v2 from app.views.contexts.feedback_form_context import build_feedback_context +from app.views.handlers.submission import get_receipting_metadata class FeedbackNotEnabled(Exception): @@ -44,7 +43,7 @@ def __init__( questionnaire_store: QuestionnaireStore, schema: QuestionnaireSchema, session_store: SessionStore, - form_data: Optional[MultiDict[str, Any]], + form_data: MultiDict[str, Any] | None, ): if not self.is_enabled(schema): raise FeedbackNotEnabled @@ -61,20 +60,15 @@ def form(self) -> QuestionnaireForm: return generate_form( schema=self._schema, question_schema=self.question_schema, - answer_store=self._questionnaire_store.answer_store, - list_store=self._questionnaire_store.list_store, - metadata=self._questionnaire_store.metadata, - response_metadata=self._questionnaire_store.response_metadata, + data_stores=self._questionnaire_store.data_stores, data=None, form_data=self._form_data, ) - def get_context(self) -> Mapping[str, Union[str, bool, dict]]: + def get_context(self) -> Mapping[str, str | bool | dict]: return build_feedback_context(self.question_schema, self.form) def get_page_title(self) -> str: - # pylint: disable=no-member - # wtforms Form parents are not discoverable in the 2.3.3 implementation if self.form.errors: title: str = gettext("Error: {page_title}").format( page_title=self.PAGE_TITLE @@ -86,37 +80,42 @@ def handle_post(self) -> None: session_data: SessionData = self._session_store.session_data # type: ignore session_data.feedback_count += 1 - feedback_metadata = FeedbackMetadata(session_data.case_id, session_data.tx_id) # type: ignore + metadata = self._questionnaire_store.data_stores.metadata + if not metadata: + raise NoMetadataException # pragma: no cover + + case_id = metadata.case_id + tx_id = metadata.tx_id - # pylint: disable=no-member - # wtforms Form parents are not discoverable in the 2.3.3 implementation - feedback_message = FeedbackPayload( - metadata=self._questionnaire_store.metadata, - response_metadata=self._questionnaire_store.response_metadata, + feedback_message = FeedbackPayloadV2( + metadata=metadata, + response_metadata=self._questionnaire_store.data_stores.response_metadata, schema=self._schema, - case_id=session_data.case_id, + case_id=case_id, submission_language_code=session_data.language_code, feedback_count=session_data.feedback_count, feedback_text=self.form.data.get("feedback-text"), feedback_type=self.form.data.get("feedback-type"), ) - message = feedback_message() - metadata = feedback_metadata() - message.update(metadata) + encrypted_message = encrypt( - message, current_app.eq["key_store"], KEY_PURPOSE_SUBMISSION # type: ignore + feedback_message(), current_app.eq["key_store"], KEY_PURPOSE_SUBMISSION # type: ignore ) - if not current_app.eq["feedback_submitter"].upload( # type: ignore - metadata, encrypted_message - ): + additional_metadata = get_receipting_metadata(metadata) + + feedback_metadata = FeedbackMetadata( + tx_id=tx_id, case_id=case_id, **additional_metadata + ) + + submitter: GCSFeedbackSubmitter | LogFeedbackSubmitter = current_app.eq["feedback_submitter"] # type: ignore + if not submitter.upload(feedback_metadata(), encrypted_message): raise FeedbackUploadFailed() self._session_store.save() @cached_property - def question_schema(self) -> Mapping[str, Union[str, list]]: - + def question_schema(self) -> Mapping[str, str | list]: return { "type": "General", "id": "feedback", @@ -182,55 +181,28 @@ def is_limit_reached(session_data: SessionData) -> bool: @staticmethod def is_enabled(schema: QuestionnaireSchema) -> bool: if submission_schema := schema.get_post_submission(): - return submission_schema.get("feedback", False) + # Type ignore: the type of the .get() returned value is Any + return submission_schema.get("feedback", False) # type: ignore return False class FeedbackMetadata: - def __init__(self, case_id: str, tx_id: str): + def __init__(self, case_id: str, tx_id: str, **kwargs: dict): self.case_id = case_id self.tx_id = tx_id + for key, value in kwargs.items(): + setattr(self, key, value) + def __call__(self) -> dict[str, str]: return vars(self) -class FeedbackPayload: +class FeedbackPayloadV2: """ Create the feedback payload object for down stream processing in the following format: - ``` - { - "collection": { - "exercise_sid": "eedbdf46-adac-49f7-b4c3-2251807381c3", - "schema_name": "carbon_0007", - "period": "3003" - }, - "data": { - "feedback_text": "Page design feedback", - "feedback_type": "Page design and structure", - "feedback_count": "7", - }, - "metadata": { - "ref_period_end_date": "2021-03-29", - "ref_period_start_date": "2021-03-01", - "ru_ref": "11110000022H", - "user_id": "d98d78eb-d23a-494d-b67c-e770399de383" - }, - "origin": "uk.gov.ons.edc.eq", - "submitted_at": "2021-10-12T10:41:23+00:00", - "started_at": "2021-10-12T10:41:23+00:00", - "case_id": "c39e1246-debd-473a-894f-85c8397ba5ea", - "case_type": "I", - "flushed": False, - "survey_id": "001", - "form_type: "0007", - "submission_language_code": "en", - "tx_id": "5d4e1a37-ed21-440a-8c4d-3054a124a104", - "type": "uk.gov.ons.edc.eq:feedback", - "launch_language_code: "en", - "submission_language_code: "en", - "version": "0.0.1" - } + v0.0.1: https://github.com/ONSdigital/ons-schema-definitions/blob/main/examples/eq_runner_to_downstream/payload_v2/business/feedback_0_0_1.json + v0.0.3: https://github.com/ONSdigital/ons-schema-definitions/blob/main/examples/eq_runner_to_downstream/payload_v2/business/feedback_0_0_3.json ``` :param metadata: Questionnaire metadata :param response_metadata: Response metadata @@ -247,11 +219,11 @@ class FeedbackPayload: def __init__( self, - metadata: Mapping[str, Union[str, int, list]], - response_metadata: Mapping[str, Union[str, int, list]], + metadata: MetadataProxy, + response_metadata: MutableMapping[str, str | int | list], schema: QuestionnaireSchema, - case_id: Optional[str], - submission_language_code: Optional[str], + case_id: str | None, + submission_language_code: str | None, feedback_count: int, feedback_text: str, feedback_type: str, @@ -267,25 +239,28 @@ def __init__( def __call__(self) -> dict[str, Any]: payload = { + "tx_id": self.metadata.tx_id, + "type": "uk.gov.ons.edc.eq:feedback", + "version": AuthPayloadVersion.V2.value, + "data_version": self.schema.json["data_version"], "origin": "uk.gov.ons.edc.eq", - "case_id": self.case_id, - "submitted_at": datetime.now(tz=timezone.utc).isoformat(), "flushed": False, - "collection": build_collection(self.metadata), - "metadata": build_metadata(self.metadata), - "survey_id": self.schema.json["survey_id"], + "submitted_at": datetime.now(tz=timezone.utc).isoformat(), + "launch_language_code": self.metadata.language_code + or DEFAULT_LANGUAGE_CODE, "submission_language_code": ( self.submission_language_code or DEFAULT_LANGUAGE_CODE ), - "tx_id": self.metadata["tx_id"], - "type": "uk.gov.ons.edc.eq:feedback", - "launch_language_code": self.metadata.get( - "language_code", DEFAULT_LANGUAGE_CODE - ), - "version": "0.0.1", + "collection_exercise_sid": self.metadata.collection_exercise_sid, + "schema_name": self.metadata.schema_name, + "case_id": self.case_id, + "survey_metadata": {"survey_id": self.schema.json["survey_id"]}, } - optional_properties = get_optional_payload_properties( + if self.metadata.survey_metadata: + payload["survey_metadata"] |= self.metadata.survey_metadata.data + + optional_properties = converter_v2.get_optional_payload_properties( self.metadata, self.response_metadata ) diff --git a/app/views/handlers/individual_response.py b/app/views/handlers/individual_response.py index 21ee27ea79..e34bc0e7f2 100644 --- a/app/views/handlers/individual_response.py +++ b/app/views/handlers/individual_response.py @@ -1,20 +1,26 @@ from datetime import datetime, timezone from functools import cached_property -from typing import List, Mapping, Optional +from typing import Any, Mapping from uuid import uuid4 from flask import current_app, redirect from flask.helpers import url_for -from flask_babel import lazy_gettext +from flask_babel import LazyString, lazy_gettext from itsdangerous import BadSignature +from werkzeug.datastructures import ImmutableMultiDict, MultiDict from werkzeug.exceptions import BadRequest, NotFound +from werkzeug.wrappers.response import Response -from app.data_models import CompletionStatus, FulfilmentRequest -from app.forms.questionnaire_form import generate_form +from app.data_models import CompletionStatus, FulfilmentRequest, QuestionnaireStore +from app.data_models.list_store import ListModel +from app.data_models.metadata_proxy import MetadataProxy +from app.forms.questionnaire_form import QuestionnaireForm, generate_form from app.forms.validators import sanitise_mobile_number from app.helpers import url_safe_serializer from app.helpers.template_helpers import render_template from app.publisher.exceptions import PublicationFailed +from app.questionnaire import QuestionnaireSchema +from app.questionnaire.location import SectionKey from app.questionnaire.placeholder_renderer import PlaceholderRenderer from app.questionnaire.router import Router from app.views.contexts.question import build_question_context @@ -37,8 +43,10 @@ class IndividualResponsePostalDeadlinePast(Exception): class IndividualResponseHandler: + RESPONSE_LIMIT_ERROR_MESSAGE = "Individual response limit has been reached" + @staticmethod - def _person_name_transforms(list_name) -> List[Mapping]: + def _person_name_transforms(list_name: str) -> list[Mapping]: return [ { "transform": "contains", @@ -63,7 +71,7 @@ def _person_name_transforms(list_name) -> List[Mapping]: ] @staticmethod - def _person_name_placeholder(list_name) -> List[Mapping]: + def _person_name_placeholder(list_name: str) -> list[Mapping]: return [ { "placeholder": "person_name", @@ -74,8 +82,10 @@ def _person_name_placeholder(list_name) -> List[Mapping]: ] @staticmethod - def _person_name_placeholder_possessive(list_name) -> List[Mapping]: - name_transforms = IndividualResponseHandler._person_name_transforms(list_name) + def _person_name_placeholder_possessive(list_name: str) -> list[Mapping]: + name_transforms: list[Mapping] = ( + IndividualResponseHandler._person_name_transforms(list_name) + ) return [ { "placeholder": "person_name_possessive", @@ -92,48 +102,52 @@ def _person_name_placeholder_possessive(list_name) -> List[Mapping]: ] @cached_property - def has_postal_deadline_passed(self): - individual_response_postal_deadline = current_app.config[ + def has_postal_deadline_passed(self) -> bool: + individual_response_postal_deadline: datetime = current_app.config[ "EQ_INDIVIDUAL_RESPONSE_POSTAL_DEADLINE" ] return individual_response_postal_deadline < datetime.now(timezone.utc) def __init__( self, - schema, - questionnaire_store, - language, - request_args, - form_data, - list_item_id=None, + schema: QuestionnaireSchema, + questionnaire_store: QuestionnaireStore, + language: str, + request_args: dict[str, str] | None, + form_data: ImmutableMultiDict[str, str], + list_item_id: str | None = None, ): self._schema = schema self._questionnaire_store = questionnaire_store self._language = language - self._request_args = request_args or {} + self._request_args: dict[str, str] = request_args or {} self._form_data = form_data - self._answers = None + self._answers: dict[str, Any] | None = None self._list_item_id = list_item_id - self._list_name = self._schema.get_individual_response_list() + # Type ignore: in individual_response for_list is required which is where list_name comes from + self._list_name: str = self._schema.get_individual_response_list() # type: ignore - self._metadata = self._questionnaire_store.metadata + self._metadata = self._questionnaire_store.data_stores.metadata - self._response_metadata = self._questionnaire_store.response_metadata + self._response_metadata = ( + self._questionnaire_store.data_stores.response_metadata + ) if not self._is_location_valid(): raise NotFound @cached_property - def _list_model(self): - return self._questionnaire_store.list_store[self._list_name] + def _list_model(self) -> ListModel: + return self._questionnaire_store.data_stores.list_store[self._list_name] @cached_property - def _list_item_position(self): - return self._questionnaire_store.list_store.list_item_position( - self._list_name, self._list_item_id + def _list_item_position(self) -> int: + # Type ignore: Current usages of this cached property occur when List Name and List Item ID exist and be not None + return self._questionnaire_store.data_stores.list_store.list_item_position( + self._list_name, self._list_item_id # type: ignore ) - def page_title(self, page_title): + def page_title(self, page_title: str) -> str: if self._list_item_id: page_title += ": " + lazy_gettext( "Person {list_item_position}".format( # pylint: disable=consider-using-f-string @@ -142,7 +156,7 @@ def page_title(self, page_title): ) return page_title - def _is_location_valid(self): + def _is_location_valid(self) -> bool: if not self._list_model: return False @@ -156,60 +170,54 @@ def _is_location_valid(self): return True @cached_property - def rendered_block(self) -> Mapping: + def rendered_block(self) -> dict: return self._render_block() @cached_property - def placeholder_renderer(self): + def placeholder_renderer(self) -> PlaceholderRenderer: return PlaceholderRenderer( language=self._language, - answer_store=self._questionnaire_store.answer_store, - list_store=self._questionnaire_store.list_store, - metadata=self._questionnaire_store.metadata, - response_metadata=self._questionnaire_store.response_metadata, + data_stores=self._questionnaire_store.data_stores, schema=self._schema, location=None, ) @cached_property - def router(self): + def router(self) -> Router: return Router( schema=self._schema, - answer_store=self._questionnaire_store.answer_store, - list_store=self._questionnaire_store.list_store, - progress_store=self._questionnaire_store.progress_store, - metadata=self._questionnaire_store.metadata, - response_metadata=self._questionnaire_store.response_metadata, + data_stores=self._questionnaire_store.data_stores, ) @cached_property - def individual_section_id(self): - return self._schema.get_individual_response_individual_section_id() + def individual_section_id(self) -> str: + # Type ignore: In an individual response handler this will not be none + return self._schema.get_individual_response_individual_section_id() # type: ignore @cached_property - def form(self): + def form(self) -> QuestionnaireForm: return generate_form( schema=self._schema, question_schema=self.rendered_block["question"], - answer_store=self._questionnaire_store.answer_store, - list_store=self._questionnaire_store.list_store, - metadata=self._questionnaire_store.metadata, - response_metadata=self._questionnaire_store.response_metadata, + data_stores=self._questionnaire_store.data_stores, data=self._answers, form_data=self._form_data, ) - def get_context(self): + def get_context(self) -> dict: return build_question_context(self.rendered_block, self.form) - def _publish_fulfilment_request(self, mobile_number=None): + def _publish_fulfilment_request(self, mobile_number: str | None = None) -> None: self._check_individual_response_count() topic_id = current_app.config["EQ_FULFILMENT_TOPIC_ID"] fulfilment_request = IndividualResponseFulfilmentRequest( - self._metadata, mobile_number + # Type ignore: _metadata will exist at point of publish + self._metadata, # type: ignore + mobile_number, ) try: - return current_app.eq["publisher"].publish( + # Type ignore: Instance attribute 'eq' is a dict with key "publisher" with value of abstract type Publisher + return current_app.eq["publisher"].publish( # type: ignore topic_id, message=fulfilment_request.message, fulfilment_request_transaction_id=fulfilment_request.transaction_id, @@ -217,31 +225,30 @@ def _publish_fulfilment_request(self, mobile_number=None): except PublicationFailed as exc: raise IndividualResponseFulfilmentRequestPublicationFailed from exc - def _check_individual_response_count(self): + def _check_individual_response_count(self) -> None: if ( - self._questionnaire_store.response_metadata.get( + self._questionnaire_store.data_stores.response_metadata.get( "individual_response_count", 0 ) >= current_app.config["EQ_INDIVIDUAL_RESPONSE_LIMIT"] ): - raise IndividualResponseLimitReached( - "Individual response limit has been reached" - ) + raise IndividualResponseLimitReached(self.RESPONSE_LIMIT_ERROR_MESSAGE) - def _update_individual_response_count(self): - response_metadata = self._questionnaire_store.response_metadata + def _update_individual_response_count(self) -> None: + response_metadata = self._questionnaire_store.data_stores.response_metadata + # Type ignore: if response_metadata.get("individual_response_count"): response_metadata["individual_response_count"] += 1 else: response_metadata["individual_response_count"] = 1 - def _update_questionnaire_store_on_publish(self): + def _update_questionnaire_store_on_publish(self) -> None: self._update_section_status(CompletionStatus.INDIVIDUAL_RESPONSE_REQUESTED) self._update_individual_response_count() self._questionnaire_store.save() - def handle_get(self): + def handle_get(self) -> str: return render_template( template="individual_response/interstitial", language=self._language, @@ -252,8 +259,8 @@ def handle_get(self): ), ) - def _get_next_location_url(self): - list_model = self._questionnaire_store.list_store[self._list_name] + def _get_next_location_url(self) -> str: + list_model = self._questionnaire_store.data_stores.list_store[self._list_name] if self._list_item_id: return url_for( @@ -271,7 +278,7 @@ def _get_next_location_url(self): return url_for(".individual_response_who", journey="hub") - def _get_previous_location_url(self): + def _get_previous_location_url(self) -> str: if self._request_args.get("journey") == "remove-person": return url_for( "questionnaire.block", @@ -293,24 +300,25 @@ def _get_previous_location_url(self): return url_for("questionnaire.get_questionnaire") - def _render_block(self): + def _render_block(self) -> dict[str, Any]: return self.placeholder_renderer.render( - self.block_definition, self._list_item_id + data_to_render=self.block_definition, list_item_id=self._list_item_id ) - def _update_section_status(self, status): - self._questionnaire_store.progress_store.update_section_status( - status, self.individual_section_id, self._list_item_id + def _update_section_status(self, status: CompletionStatus) -> None: + self._questionnaire_store.data_stores.progress_store.update_section_status( + status, + SectionKey(self.individual_section_id, self._list_item_id), ) @property - def block_definition(self): # pragma: no cover + def block_definition(self) -> Mapping[str, Any]: # pragma: no cover raise NotImplementedError class IndividualResponseHowHandler(IndividualResponseHandler): @cached_property - def block_definition(self) -> Mapping: + def block_definition(self) -> Mapping[str, Any]: return { "type": "IndividualResponse", "id": "individual-response", @@ -319,7 +327,7 @@ def block_definition(self) -> Mapping: "id": "individual-response-how", "title": { "text": lazy_gettext( - "How would you like {person_name} to receive a separate census?" + "How would you like {person_name} to receive a separate census?" ), "placeholders": IndividualResponseHandler._person_name_placeholder( self._list_name @@ -338,7 +346,7 @@ def block_definition(self) -> Mapping: }, } - def _build_handler_answer_options(self): + def _build_handler_answer_options(self) -> list[dict[str, str | LazyString]]: handler_options = [ { "label": lazy_gettext("Text message"), @@ -360,7 +368,7 @@ def _build_handler_answer_options(self): ) return handler_options - def _build_question_description(self): + def _build_question_description(self) -> list[LazyString]: description = ( lazy_gettext("It is no longer possible to receive an access code by post.") if self.has_postal_deadline_passed @@ -374,11 +382,11 @@ def _build_question_description(self): ] @cached_property - def selected_option(self): + def selected_option(self) -> str: answer_id = self.rendered_block["question"]["answers"][0]["id"] return self.form.get_data(answer_id) - def handle_get(self): + def handle_get(self) -> str: if self._request_args.get("journey") == "hub": if len(self._list_model.non_primary_people) == 1: previous_location_url = url_for( @@ -416,7 +424,7 @@ def handle_get(self): page_title=self.page_title(lazy_gettext("Send individual access code")), ) - def handle_post(self): + def handle_post(self) -> Response: if self.selected_option == "Post": return redirect( url_for( @@ -436,7 +444,7 @@ def handle_post(self): class IndividualResponseChangeHandler(IndividualResponseHandler): @cached_property - def block_definition(self) -> Mapping: + def block_definition(self) -> Mapping[str, Any]: return { "type": "IndividualResponse", "id": "individual-response-change", @@ -445,7 +453,7 @@ def block_definition(self) -> Mapping: "id": "individual-response-change-question", "title": { "text": lazy_gettext( - "How would you like to answer {person_name_possessive} questions?" + "How would you like to answer {person_name_possessive} questions?" ), "placeholders": IndividualResponseHandler._person_name_placeholder_possessive( self._list_name @@ -491,23 +499,32 @@ def block_definition(self) -> Mapping: } @cached_property - def request_separate_census_option(self): - return self.rendered_block["question"]["answers"][0]["options"][0]["value"] + def request_separate_census_option(self) -> str: + value: str = self.rendered_block["question"]["answers"][0]["options"][0][ + "value" + ] + return value @cached_property - def cancel_go_to_hub_option(self): - return self.rendered_block["question"]["answers"][0]["options"][1]["value"] + def cancel_go_to_hub_option(self) -> str: + value: str = self.rendered_block["question"]["answers"][0]["options"][1][ + "value" + ] + return value @cached_property - def cancel_go_to_section_option(self): - return self.rendered_block["question"]["answers"][0]["options"][2]["value"] + def cancel_go_to_section_option(self) -> str: + value: str = self.rendered_block["question"]["answers"][0]["options"][2][ + "value" + ] + return value @cached_property - def selected_option(self): - answer_id = self.rendered_block["question"]["answers"][0]["id"] + def selected_option(self) -> str: + answer_id: str = self.rendered_block["question"]["answers"][0]["id"] return self.form.get_data(answer_id) - def handle_get(self): + def handle_get(self) -> str: self._answers = { "individual-response-change-answer": self.request_separate_census_option } @@ -520,7 +537,7 @@ def handle_get(self): page_title=self.page_title(lazy_gettext("How to answer questions")), ) - def handle_post(self): + def handle_post(self) -> Response | None: if self.selected_option == self.request_separate_census_option: return redirect( url_for( @@ -549,27 +566,27 @@ def handle_post(self): ) ) - def _update_section_completeness(self): - if not self._questionnaire_store.progress_store.get_completed_block_ids( - self.individual_section_id, self._list_item_id + def _update_section_completeness(self) -> None: + if not self._questionnaire_store.data_stores.progress_store.get_completed_block_ids( + section_key := SectionKey( + section_id=self.individual_section_id, list_item_id=self._list_item_id + ) ): status = CompletionStatus.NOT_STARTED else: - routing_path = self.router.routing_path( - self.individual_section_id, self._list_item_id - ) + routing_path = self.router.routing_path(section_key) status = ( CompletionStatus.COMPLETED if self.router.is_path_complete(routing_path) else CompletionStatus.IN_PROGRESS ) self._update_section_status(status) - if self._questionnaire_store.progress_store.is_dirty: + if self._questionnaire_store.data_stores.progress_store.is_dirty: self._questionnaire_store.save() class IndividualResponsePostAddressConfirmHandler(IndividualResponseHandler): - def __init__(self, **kwargs): + def __init__(self, **kwargs: Any): if self.has_postal_deadline_passed: raise IndividualResponsePostalDeadlinePast super().__init__(**kwargs) @@ -626,18 +643,22 @@ def block_definition(self) -> Mapping: } @cached_property - def answer_id(self): - return self.rendered_block["question"]["answers"][0]["id"] + def answer_id(self) -> str: + value: str = self.rendered_block["question"]["answers"][0]["id"] + return value @cached_property - def confirm_option(self): - return self.rendered_block["question"]["answers"][0]["options"][0]["value"] + def confirm_option(self) -> str: + value: str = self.rendered_block["question"]["answers"][0]["options"][0][ + "value" + ] + return value @cached_property - def selected_option(self): + def selected_option(self) -> str: return self.form.get_data(self.answer_id) - def handle_get(self): + def handle_get(self) -> str: previous_location_url = url_for( "individual_response.individual_response_how", list_item_id=self._list_item_id, @@ -652,7 +673,7 @@ def handle_get(self): page_title=self.page_title(lazy_gettext("Confirm address")), ) - def handle_post(self): + def handle_post(self) -> Response: if self.selected_option == self.confirm_option: self._publish_fulfilment_request() self._update_questionnaire_store_on_publish() @@ -674,9 +695,16 @@ def handle_post(self): class IndividualResponseWhoHandler(IndividualResponseHandler): - def __init__(self, schema, questionnaire_store, language, request_args, form_data): - self._list_name = schema.get_individual_response_list() - list_model = questionnaire_store.list_store[self._list_name] + def __init__( + self, + schema: QuestionnaireSchema, + questionnaire_store: QuestionnaireStore, + language: str, + request_args: dict[str, str], + form_data: ImmutableMultiDict[str, str], + ): + self._list_name: str = schema.get_individual_response_list() # type: ignore + list_model = questionnaire_store.data_stores.list_store[self._list_name] self.non_primary_people_names = {} if list_model.same_name_items: @@ -685,10 +713,13 @@ def __init__(self, schema, questionnaire_store, language, request_args, form_dat name_answer_ids = ["first-name", "last-name"] for list_item_id in list_model.non_primary_people: - name_answers = questionnaire_store.answer_store.get_answers_by_answer_id( - name_answer_ids, list_item_id=list_item_id + name_answers = ( + questionnaire_store.data_stores.answer_store.get_answers_by_answer_id( + name_answer_ids, list_item_id=list_item_id + ) ) - name = " ".join(name_answer.value for name_answer in name_answers) + # Type ignore: AnswerValues can be any type, however name_answers in this context will always be strings + name = " ".join(name_answer.value for name_answer in name_answers) # type: ignore self.non_primary_people_names[list_item_id] = name super().__init__( @@ -726,11 +757,11 @@ def block_definition(self) -> Mapping: } @cached_property - def selected_option(self): + def selected_option(self) -> str: answer_id = self.rendered_block["question"]["answers"][0]["id"] return self.form.get_data(answer_id) - def handle_get(self): + def handle_get(self) -> str: if len(self.non_primary_people_names) > 1: previous_location_url = url_for( "individual_response.request_individual_response", @@ -747,7 +778,7 @@ def handle_get(self): raise NotFound - def handle_post(self): + def handle_post(self) -> Response: return redirect( url_for( ".individual_response_how", @@ -767,7 +798,7 @@ def block_definition(self) -> Mapping: "id": "individual-response-enter-number", "title": { "text": lazy_gettext( - "What is {person_name_possessive} mobile number?" + "What is {person_name_possessive} mobile number?" ), "placeholders": IndividualResponseHandler._person_name_placeholder_possessive( self._list_name @@ -788,14 +819,16 @@ def block_definition(self) -> Mapping: } @cached_property - def answer_id(self): - return self.rendered_block["question"]["answers"][0]["id"] + def answer_id(self) -> str: + value: str = self.rendered_block["question"]["answers"][0]["id"] + return value @cached_property - def mobile_number(self): - return self.form.get_data(self.answer_id) + def mobile_number(self) -> str: + value: str = self.form.get_data(self.answer_id) + return value - def handle_get(self): + def handle_get(self) -> str: if "mobile_number" in self._request_args: mobile_number = url_safe_serializer().loads( self._request_args["mobile_number"] @@ -815,7 +848,7 @@ def handle_get(self): page_title=self.page_title(lazy_gettext("Mobile number")), ) - def handle_post(self): + def handle_post(self) -> Response: mobile_number = url_safe_serializer().dumps(self.mobile_number) return redirect( @@ -831,12 +864,12 @@ def handle_post(self): class IndividualResponseTextConfirmHandler(IndividualResponseHandler): def __init__( self, - schema, - questionnaire_store, - language, - request_args, - form_data, - list_item_id, + schema: QuestionnaireSchema, + questionnaire_store: QuestionnaireStore, + language: str, + request_args: MultiDict[str, str], + form_data: ImmutableMultiDict[str, str], + list_item_id: str, ): try: self.mobile_number = url_safe_serializer().loads( @@ -884,18 +917,22 @@ def block_definition(self) -> Mapping: } @cached_property - def answer_id(self): - return self.rendered_block["question"]["answers"][0]["id"] + def answer_id(self) -> str: + value: str = self.rendered_block["question"]["answers"][0]["id"] + return value @cached_property - def confirm_option(self): - return self.rendered_block["question"]["answers"][0]["options"][0]["value"] + def confirm_option(self) -> str: + value: str = self.rendered_block["question"]["answers"][0]["options"][0][ + "value" + ] + return value @cached_property - def selected_option(self): + def selected_option(self) -> str: return self.form.get_data(self.answer_id) - def handle_get(self): + def handle_get(self) -> str: previous_location_url = url_for( "individual_response.individual_response_text_message", list_item_id=self._list_item_id, @@ -911,7 +948,7 @@ def handle_get(self): page_title=self.page_title(lazy_gettext("Confirm mobile number")), ) - def handle_post(self): + def handle_post(self) -> Response: if self.selected_option == self.confirm_option: self._publish_fulfilment_request(self.mobile_number) self._update_questionnaire_store_on_publish() @@ -935,7 +972,7 @@ def handle_post(self): class IndividualResponseFulfilmentRequest(FulfilmentRequest): - def __init__(self, metadata: Mapping, mobile_number: Optional[str] = None): + def __init__(self, metadata: MetadataProxy, mobile_number: str | None = None): self._metadata = metadata self._mobile_number = mobile_number self._fulfilment_type = "sms" if self._mobile_number else "postal" @@ -943,7 +980,7 @@ def __init__(self, metadata: Mapping, mobile_number: Optional[str] = None): def _get_individual_case_id_mapping(self) -> Mapping: return ( {} - if self._metadata.get("case_type") in ["SPG", "CE"] + if self._metadata["case_type"] in ["SPG", "CE"] else {"individualCaseId": str(uuid4())} ) @@ -954,7 +991,7 @@ def _get_contact_mapping(self) -> Mapping: else {} ) - def _get_fulfilment_code(self) -> str: + def _get_fulfilment_code(self) -> str | None: fulfilment_codes = { "sms": { GB_ENG_REGION_CODE: "UACITA1", @@ -967,15 +1004,15 @@ def _get_fulfilment_code(self) -> str: GB_NIR_REGION_CODE: "P_UAC_UACIPA4", }, } - region_code = self._metadata["region_code"] - return fulfilment_codes[self._fulfilment_type][region_code] + if region_code := self._metadata.region_code: + return fulfilment_codes[self._fulfilment_type][region_code] def _payload(self) -> Mapping: return { "fulfilmentRequest": { **self._get_individual_case_id_mapping(), "fulfilmentCode": self._get_fulfilment_code(), - "caseId": self._metadata["case_id"], + "caseId": self._metadata.case_id, "contact": self._get_contact_mapping(), } } diff --git a/app/views/handlers/list_action.py b/app/views/handlers/list_action.py index e9175bbba4..5546db5335 100644 --- a/app/views/handlers/list_action.py +++ b/app/views/handlers/list_action.py @@ -1,77 +1,118 @@ from flask import url_for +from werkzeug.datastructures import ImmutableDict from app.questionnaire.location import Location +from app.questionnaire.return_location import ReturnLocation +from app.questionnaire.routing_path import RoutingPath from app.views.handlers.question import Question class ListAction(Question): @property - def parent_block(self): + def parent_block(self) -> ImmutableDict: parent_block_id = self._schema.parent_id_map[self.block["id"]] - return self._schema.get_block(parent_block_id) + # Type ignore: get_block is being called with a valid block_id + return self._schema.get_block(parent_block_id) # type: ignore @property - def parent_location(self): + def parent_location(self) -> Location: parent_block_id = self._schema.parent_id_map[self.block["id"]] return Location( section_id=self._current_location.section_id, block_id=parent_block_id ) - def _get_routing_path(self): - return self.router.routing_path(section_id=self.parent_location.section_id) + def _get_routing_path(self) -> RoutingPath: + return self.router.routing_path(self.parent_location.section_key) - def is_location_valid(self): + def is_location_valid(self) -> bool: can_access_parent_location = self.router.can_access_location( self.parent_location, self._routing_path ) - if ( - not can_access_parent_location - or self._current_location.list_name != self.parent_block["for_list"] - ): - return False - - return True + return bool( + can_access_parent_location + and self._current_location.list_name == self.parent_block["for_list"] + ) - def get_previous_location_url(self): - if self._return_to == "section-summary": - return self.get_section_summary_url() + def get_previous_location_url(self) -> str: + if url := self.get_section_or_final_summary_url(): + return url block_id = self._request_args.get("previous") - return self._get_location_url(block_id) - - def get_section_summary_url(self): - return url_for( - "questionnaire.get_section", section_id=self.parent_location.section_id + return self._get_location_url( + block_id=block_id, return_location=self.return_location ) - def get_next_location_url(self): - if self._return_to == "section-summary": - if self.router.can_display_section_summary( - self.parent_location.section_id, self.parent_location.list_item_id - ): - return self.get_section_summary_url() - - return self.parent_location.url() + def get_section_or_final_summary_url(self) -> str | None: + if ( + self.return_location.return_to == "section-summary" + and self.router.can_display_section_summary( + self.parent_location.section_key + ) + ): + return url_for( + "questionnaire.get_section", + section_id=self.parent_location.section_id, + _anchor=self.return_location.return_to_answer_id, + ) + if ( + self.return_location.return_to == "final-summary" + and self.router.is_questionnaire_complete + ): + return url_for( + "questionnaire.submit_questionnaire", + _anchor=self.return_location.return_to_answer_id, + ) + + def get_next_location_url(self) -> str: + if url := self.get_section_or_final_summary_url(): + return url + + if self._questionnaire_store.data_stores.progress_store.is_block_complete( + # Type ignore: block_id would exist at this point + block_id=self.parent_location.block_id, # type: ignore + section_key=self.parent_location.section_key, + ): + return self.router.get_next_location_url( + self.parent_location, + self._routing_path, + self.return_location, + ) + + return self.parent_location.url( + return_to=self.return_location.return_to, + return_to_answer_id=self.return_location.return_to_answer_id, + return_to_block_id=self.return_location.return_to_block_id, + ) - def handle_post(self): + def handle_post(self) -> None: self.questionnaire_store_updater.update_same_name_items( self.parent_block["for_list"], self.parent_block.get("same_name_answer_ids"), ) - # Clear the answer from the confirmation question on the list collector question - answer_ids_to_remove = self._schema.get_answer_ids_for_block( - self.parent_location.block_id - ) - self.questionnaire_store_updater.remove_answers(answer_ids_to_remove) - self.evaluate_and_update_section_status_on_list_change( - self.parent_block["for_list"] - ) - self.questionnaire_store_updater.save() - def _get_location_url(self, block_id): + if self.questionnaire_store_updater.is_dirty(): + self._routing_path = self._get_routing_path() + self.questionnaire_store_updater.remove_dependent_blocks_and_capture_dependent_sections() + self.questionnaire_store_updater.update_progress_for_dependent_sections() + self.questionnaire_store_updater.save() + + def _get_location_url( + self, + *, + block_id: str | None = None, + return_location: ReturnLocation, + anchor: str | None = None, + ) -> str: if block_id and self._schema.is_block_valid(block_id): - section_id = self._schema.get_section_id_for_block_id(block_id) - return Location(section_id=section_id, block_id=block_id).url() - - return self.parent_location.url() + # Type ignore: the above line check that block_id exists and is valid and therefore section exists + section_id: str = self._schema.get_section_id_for_block_id(block_id) # type: ignore + return Location(section_id=section_id, block_id=block_id).url( + **return_location.to_dict(), + _anchor=anchor, + ) + + return self.parent_location.url( + **return_location.to_dict(), + _anchor=anchor, + ) diff --git a/app/views/handlers/list_add_question.py b/app/views/handlers/list_add_question.py index cb5e61da83..599203d6c9 100644 --- a/app/views/handlers/list_add_question.py +++ b/app/views/handlers/list_add_question.py @@ -1,28 +1,65 @@ -from typing import MutableMapping +from typing import Any + +from flask import url_for from app.views.handlers.list_action import ListAction class ListAddQuestion(ListAction): - def is_location_valid(self): + def __init__(self, *args: Any) -> None: + self._list_item_id: str | None = None + super().__init__(*args) + + def is_location_valid(self) -> bool: if not super().is_location_valid() or self._current_location.list_item_id: return False return True - def handle_post(self): - list_item_id = self.questionnaire_store_updater.add_list_item( - self.parent_block["for_list"] + def get_next_location_url(self) -> str: + if self._list_item_id and ( + repeating_blocks := self.parent_block.get("repeating_blocks") + ): + return url_for( + "questionnaire.block", + list_name=self.parent_block["for_list"], + list_item_id=self._list_item_id, + block_id=repeating_blocks[0]["id"], + return_to=self.return_location.return_to, + return_to_answer_id=self.return_location.return_to_answer_id, + return_to_block_id=self.return_location.return_to_block_id, + ) + + return self.parent_location.url( + return_to=self.return_location.return_to, + return_to_answer_id=self.return_location.return_to_answer_id, + return_to_block_id=self.return_location.return_to_block_id, + ) + + def handle_post(self) -> None: + # Ensure the section is in progress when user adds an item + self._list_item_id = self.questionnaire_store_updater.add_list_item( + list_name := self.parent_block["for_list"] + ) + + # Clear the answer from the confirmation question on the list collector question + answer_ids_to_remove = self._schema.get_answer_ids_for_block( + # Type ignore: for parent_location block_id is not none + self.parent_location.block_id # type: ignore + ) + self.questionnaire_store_updater.remove_answers(answer_ids_to_remove) + self.questionnaire_store_updater.remove_completed_location(self.parent_location) + + self.questionnaire_store_updater.update_answers( + self.form.data, self._list_item_id ) - # pylint: disable=no-member - # wtforms Form parents are not discoverable in the 2.3.3 implementation - self.questionnaire_store_updater.update_answers(self.form.data, list_item_id) + self.questionnaire_store_updater.capture_dependencies_for_list_change(list_name) return super().handle_post() - def _resolve_custom_page_title_vars(self) -> MutableMapping: + def _resolve_custom_page_title_vars(self) -> dict[str, int]: # For list add blocks, no list item id is yet available. Instead, we resolve # `list_item_position` to the position in the list it would be if added. list_length = len( - self._questionnaire_store.list_store[self._current_location.list_name] # type: ignore + self._questionnaire_store.data_stores.list_store[self._current_location.list_name] # type: ignore ) return {"list_item_position": list_length + 1} diff --git a/app/views/handlers/list_collector.py b/app/views/handlers/list_collector.py index 7b24cc34d6..8ba455a74d 100644 --- a/app/views/handlers/list_collector.py +++ b/app/views/handlers/list_collector.py @@ -1,3 +1,6 @@ +from functools import cached_property +from typing import Any + from flask import url_for from app.views.contexts import ListContext @@ -5,51 +8,83 @@ class ListCollector(Question): - def __init__(self, *args): + def __init__(self, *args: Any) -> None: self._is_adding = False super().__init__(*args) - def get_next_location_url(self): + @cached_property + def repeating_block_ids(self) -> list[str]: + return [ + block["id"] for block in self.rendered_block.get("repeating_blocks", []) + ] + + @cached_property + def list_name(self) -> str: + return self.rendered_block["for_list"] # type: ignore + + def get_next_location_url(self) -> str: if self._is_adding: add_url = url_for( "questionnaire.block", list_name=self.rendered_block["for_list"], block_id=self.rendered_block["add_block"]["id"], + **self.return_location.to_dict(), ) return add_url + if incomplete_block := self.get_first_incomplete_list_repeating_block_location( + repeating_block_ids=self.repeating_block_ids, + section_id=self.current_location.section_id, + list_name=self.list_name, + ): + repeating_block_url = url_for( + "questionnaire.block", + list_name=self.list_name, + list_item_id=incomplete_block.list_item_id, + block_id=incomplete_block.block_id, + **self.return_location.to_dict(), + ) + return repeating_block_url + return super().get_next_location_url() - def get_context(self): - question_context = super().get_context() + def _get_list_context(self) -> dict[str, dict]: list_context = ListContext( self._language, self._schema, - self._questionnaire_store.answer_store, - self._questionnaire_store.list_store, - self._questionnaire_store.progress_store, - self._questionnaire_store.metadata, - self._questionnaire_store.response_metadata, + self._questionnaire_store.data_stores, ) - return { - **question_context, - **list_context( - self.rendered_block["summary"], - for_list=self.rendered_block["for_list"], - edit_block_id=self.rendered_block["edit_block"]["id"], - remove_block_id=self.rendered_block["remove_block"]["id"], - ), - } - - def handle_post(self): + return list_context( + self.rendered_block["summary"], + for_list=self.list_name, + edit_block_id=self.rendered_block.get("edit_block", {}).get("id"), + remove_block_id=self.rendered_block.get("remove_block", {}).get("id"), + return_to=self.return_location.return_to, + section_id=self.current_location.section_id, + has_repeating_blocks=bool(self.repeating_block_ids), + ) + + def _get_additional_view_context(self) -> dict: + """This is only needed so we can use it in List Collector Content class where we override the default behaviour of the Question class""" + return super().get_context() + + def get_context(self) -> dict: + return {**self._get_additional_view_context(), **self._get_list_context()} + + def handle_post(self) -> None: answer_action = self._get_answer_action() if answer_action and answer_action["type"] == "RedirectToListAddBlock": self._is_adding = True - # pylint: disable=no-member - # wtforms Form parents are not discoverable in the 2.3.3 implementation self.questionnaire_store_updater.update_answers(self.form.data) self.questionnaire_store_updater.save() - else: - return super().handle_post() + elif self._is_list_collector_complete(): + super().handle_post() + + def _is_list_collector_complete(self) -> bool: + return not self.get_first_incomplete_list_repeating_block_location( + repeating_block_ids=self.repeating_block_ids, + section_id=self.current_location.section_id, + list_name=self.list_name, + ) diff --git a/app/views/handlers/list_collector_content.py b/app/views/handlers/list_collector_content.py new file mode 100644 index 0000000000..731a53753d --- /dev/null +++ b/app/views/handlers/list_collector_content.py @@ -0,0 +1,15 @@ +from app.views.handlers.list_collector import ListCollector +from app.views.handlers.question import Question + + +class ListCollectorContent(ListCollector): + def _get_additional_view_context(self) -> dict: + # Type ignore: the type of the .get() returned value is Any + return self.rendered_block.get("content", {}) # type: ignore + + def handle_post(self) -> None: + if self._is_list_collector_complete(): + self._routing_path = self.router.routing_path( + self._current_location.section_key + ) + return super(Question, self).handle_post() diff --git a/app/views/handlers/list_edit_question.py b/app/views/handlers/list_edit_question.py index 1d640135b4..7aa6786e27 100644 --- a/app/views/handlers/list_edit_question.py +++ b/app/views/handlers/list_edit_question.py @@ -1,21 +1,54 @@ +from functools import cached_property + +from flask import url_for + from app.views.handlers.list_action import ListAction class ListEditQuestion(ListAction): - def is_location_valid(self): + @cached_property + def repeating_block_ids(self) -> list[str]: + return [ + repeating_block["id"] + for repeating_block in self.parent_block.get("repeating_blocks", []) + ] + + def is_location_valid(self) -> bool: list_item_doesnt_exist = ( self._current_location.list_item_id - not in self._questionnaire_store.list_store[ - self._current_location.list_name + not in self._questionnaire_store.data_stores.list_store[ + # Type ignore: list_name/list_item_id already exist + self._current_location.list_name # type: ignore ].items ) if not super().is_location_valid() or list_item_doesnt_exist: return False return True - def handle_post(self): - # pylint: disable=no-member - # wtforms Form parents are not discoverable in the 2.3.3 implementation + def get_next_location_url(self) -> str: + """ + Unless editing from the summary page, If there are repeating blocks and not all are complete, go to the next one + """ + if url := self.get_section_or_final_summary_url(): + return url + + if first_incomplete_block := self.get_first_incomplete_list_repeating_block_location_for_list_item( + repeating_block_ids=self.repeating_block_ids, + section_key=self.current_location.section_key, + # Type ignore: list_name will exist at this point + list_name=self.current_location.list_name, # type: ignore + ): + return url_for( + "questionnaire.block", + list_name=first_incomplete_block.list_name, + list_item_id=first_incomplete_block.list_item_id, + block_id=first_incomplete_block.block_id, + **self.return_location.to_dict(), + ) + + return super().get_next_location_url() + + def handle_post(self) -> None: self.questionnaire_store_updater.update_answers(self.form.data) return super().handle_post() diff --git a/app/views/handlers/list_remove_question.py b/app/views/handlers/list_remove_question.py index 94a329ba47..8ba7c59efa 100644 --- a/app/views/handlers/list_remove_question.py +++ b/app/views/handlers/list_remove_question.py @@ -3,16 +3,18 @@ class ListRemoveQuestion(ListAction): - def is_location_valid(self): + def is_location_valid(self) -> bool: list_item_doesnt_exist = ( self._current_location.list_item_id - not in self._questionnaire_store.list_store[ - self._current_location.list_name + not in self._questionnaire_store.data_stores.list_store[ + # Type ignore: list_name will exist within the remove block + self._current_location.list_name # type: ignore ].items ) is_primary = ( - self._questionnaire_store.list_store[ - self._current_location.list_name + self._questionnaire_store.data_stores.list_store[ + # Type ignore: list_name will exist within the remove block + self._current_location.list_name # type: ignore ].primary_person == self._current_location.list_item_id ) @@ -20,28 +22,34 @@ def is_location_valid(self): return False return True - def handle_post(self): + def handle_post(self) -> None: answer_action = self._get_answer_action() if answer_action and answer_action["type"] == "RemoveListItemAndAnswers": list_name = self.parent_block["for_list"] - self.questionnaire_store_updater.remove_list_item_and_answers( - list_name, self._current_location.list_item_id + self.questionnaire_store_updater.remove_list_item_data( + # Type ignore: list_item_id will exist within the remove block + list_name, + self._current_location.list_item_id, # type: ignore + ) + # This will result in any list collector content blocks using this list to require revisiting. This is currently the expected behaviour. + self.questionnaire_store_updater.capture_dependencies_for_list_change( + list_name ) return super().handle_post() def individual_response_enabled(self) -> bool: - return ( - self.parent_block["for_list"] == self._schema.get_individual_response_list() - ) + # Type ignore: we know "for_list" will be a string + return self.parent_block["for_list"] == self._schema.get_individual_response_list() # type: ignore - def get_context(self): - context = super().get_context() + def get_context(self) -> dict: + context: dict = super().get_context() context["individual_response_enabled"] = self.individual_response_enabled() context["individual_response_url"] = individual_response_url( self._schema.get_individual_response_list(), - self._current_location.list_item_id, + # Type ignore: in a list so we know this cannot be None + self._current_location.list_item_id, # type: ignore self._questionnaire_store, journey="remove-person", ) diff --git a/app/views/handlers/list_repeating_question.py b/app/views/handlers/list_repeating_question.py new file mode 100644 index 0000000000..d2bef07d6e --- /dev/null +++ b/app/views/handlers/list_repeating_question.py @@ -0,0 +1,68 @@ +from flask import url_for + +from app.views.handlers.list_edit_question import ListEditQuestion + + +class ListRepeatingQuestion(ListEditQuestion): + def get_previous_location_url(self) -> str: + """ + return to previous location, or when return to is None, navigate to the previous repeating block + unless this is the first repeating block, in which case, route back to the edit block + """ + if url := self.get_section_or_final_summary_url(): + return url + + # the locations list_item_id is referring to where to return to within the context of a repeating section + # since the list collector won't be in a repeating section, use the parent location which doesn't have a list item id + if url := self.router.get_return_to_location_url( + location=self.parent_location, + return_location=self.return_location, + routing_path=self._routing_path, + is_for_previous=True, + ): + return url + + repeating_block_index = self.repeating_block_ids.index( + # Type ignore: block_id will exist at this point + self.current_location.block_id # type: ignore + ) + if repeating_block_index != 0: + previous_repeating_block_id = self.repeating_block_ids[ + repeating_block_index - 1 + ] + return url_for( + "questionnaire.block", + list_name=self.current_location.list_name, + list_item_id=self.current_location.list_item_id, + block_id=previous_repeating_block_id, + **self.return_location.to_dict(), + ) + + if edit_block := self._schema.get_edit_block_for_list_collector( + self.parent_block["id"] + ): + return url_for( + "questionnaire.block", + list_name=self.current_location.list_name, + list_item_id=self.current_location.list_item_id, + block_id=edit_block["id"], + **self.return_location.to_dict(), + ) + + return self.parent_location.url( + **self.return_location.to_dict(), + ) + + def handle_post(self) -> None: + self.questionnaire_store_updater.add_completed_location(self.current_location) + if not self.get_first_incomplete_list_repeating_block_location_for_list_item( + repeating_block_ids=self.repeating_block_ids, + section_key=self.current_location.section_key, + # Type ignore: list_name will always exist at this point + list_name=self.current_location.list_name, # type: ignore + ): + self.questionnaire_store_updater.update_section_status( + is_complete=True, section_key=self.current_location.section_key + ) + + super().handle_post() diff --git a/app/views/handlers/pdf_response.py b/app/views/handlers/pdf_response.py new file mode 100644 index 0000000000..779edc5b33 --- /dev/null +++ b/app/views/handlers/pdf_response.py @@ -0,0 +1,75 @@ +import io +import re +from datetime import datetime, timezone + +import pdfkit +from flask import current_app + +from app.data_models import QuestionnaireStore +from app.questionnaire import QuestionnaireSchema + + +class PDFResponse: + """ + Responsible for the PDF generation for the view submitted response. + + Subclassed from `ViewSubmittedResponse`. + + Attributes: + schema: The questionnaire schema object representing the schema JSON. + questionnaire_store: The questionnaire store object. + language: The language the user is currently in. + + Raises: + ViewSubmittedResponseExpired: If the submitted response has expired. + """ + + # The mimetype to use for response + mimetype = "application/pdf" + + # Options to be passed to wkhtmltopdf via PDFKit + wkhtmltopdf_options = { + "quiet": "", + "margin-top": "0.30in", + "margin-right": "0.40in", + "margin-bottom": "0.30in", + "margin-left": "0.40in", + "dpi": 365, + } + + def __init__( + self, + schema: QuestionnaireSchema, + questionnaire_store: QuestionnaireStore, + language: str, + ): + self._schema = schema + self._questionnaire_store = questionnaire_store + self._language = language + + @property + def filename(self) -> str: + """The name to use for the PDF file""" + formatted_title = re.sub( + "[^0-9a-zA-Z]+", "-", self._schema.json["title"].lower() + ) + if self._questionnaire_store.submitted_at: + formatted_date = self._questionnaire_store.submitted_at.date().isoformat() + else: + formatted_date = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d") + return f"{formatted_title}-{formatted_date}.pdf" + + def _get_pdf(self, rendered_html: str) -> io.BytesIO: + """ + Generates a PDF document from the rendered html. + :return: The generated PDF document as BytesIO + :rtype: io.BytesIO + """ + content_as_bytes = pdfkit.from_string( + input=rendered_html, + output_path=None, + css=f'{current_app.config["PRINT_STYLE_SHEET_FILE_PATH"]}/print.css', + options=self.wkhtmltopdf_options, + ) + + return io.BytesIO(content_as_bytes) diff --git a/app/views/handlers/preview_questions_pdf.py b/app/views/handlers/preview_questions_pdf.py new file mode 100644 index 0000000000..87539e80b1 --- /dev/null +++ b/app/views/handlers/preview_questions_pdf.py @@ -0,0 +1,14 @@ +import io + +from app.views.handlers.pdf_response import PDFResponse +from app.views.handlers.view_preview_questions import ViewPreviewQuestions + + +class PreviewQuestionsPDF(PDFResponse, ViewPreviewQuestions): + def get_pdf(self) -> io.BytesIO: + """ + Generates a PDF document from the rendered ViewSubmittedResponse html. + :return: The generated PDF document as BytesIO + :rtype: io.BytesIO + """ + return self._get_pdf(rendered_html=self.get_rendered_html()) diff --git a/app/views/handlers/primary_person_list_collector.py b/app/views/handlers/primary_person_list_collector.py index bb986421f6..c67c890b83 100644 --- a/app/views/handlers/primary_person_list_collector.py +++ b/app/views/handlers/primary_person_list_collector.py @@ -1,15 +1,17 @@ +from typing import Any + from flask import url_for from app.views.handlers.question import Question class PrimaryPersonListCollector(Question): - def __init__(self, *args): + def __init__(self, *args: Any): self._is_adding = False - self._primary_person_id = None + self._primary_person_id: str | None = None super().__init__(*args) - def get_next_location_url(self): + def get_next_location_url(self) -> str: if self._is_adding: add_or_edit_url = url_for( "questionnaire.block", @@ -21,31 +23,33 @@ def get_next_location_url(self): return super().get_next_location_url() - def handle_post(self): + def handle_post(self) -> None: list_name = self.rendered_block["for_list"] answer_action = self._get_answer_action() if answer_action and answer_action["type"] == "RedirectToListAddBlock": self._is_adding = True - # pylint: disable=no-member - # wtforms Form parents are not discoverable in the 2.3.3 implementation self.questionnaire_store_updater.update_answers(self.form.data) self._primary_person_id = ( self.questionnaire_store_updater.add_primary_person(list_name) ) - - self.evaluate_and_update_section_status_on_list_change(list_name) + self.questionnaire_store_updater.capture_dependencies_for_list_change( + list_name + ) + self.questionnaire_store_updater.remove_dependent_blocks_and_capture_dependent_sections() + self.questionnaire_store_updater.update_progress_for_dependent_sections() self.questionnaire_store_updater.save() else: - self.questionnaire_store_updater.remove_primary_person(list_name) - - self.questionnaire_store_updater.update_same_name_items( - list_name, self.rendered_block.get("same_name_answer_ids") - ) # This method could determine the current section's status incorrectly, as # the call to update the answer store takes place in # `super().handle_post()`. The section status will eventually get # determined correctly when the parent class' `update_section_status` # method is called. - self.evaluate_and_update_section_status_on_list_change(list_name) + self.questionnaire_store_updater.remove_primary_person(list_name) + self.questionnaire_store_updater.update_same_name_items( + list_name, self.rendered_block.get("same_name_answer_ids") + ) + self.questionnaire_store_updater.capture_dependencies_for_list_change( + list_name + ) super().handle_post() diff --git a/app/views/handlers/primary_person_question.py b/app/views/handlers/primary_person_question.py index 1544458ff4..f1af5ec9f9 100644 --- a/app/views/handlers/primary_person_question.py +++ b/app/views/handlers/primary_person_question.py @@ -1,26 +1,31 @@ +from werkzeug.datastructures import ImmutableDict + from app.questionnaire.location import Location +from app.questionnaire.routing_path import RoutingPath from app.views.handlers.question import Question class PrimaryPersonQuestion(Question): @property - def parent_block(self): + def parent_block(self) -> ImmutableDict: parent_block_id = self._schema.parent_id_map[self.rendered_block["id"]] - return self._schema.get_block(parent_block_id) + # Type ignore: being called with a valid block_id + return self._schema.get_block(parent_block_id) # type: ignore @property - def parent_location(self): - parent_id = self._schema.parent_id_map[self.rendered_block["id"]] + def parent_location(self) -> Location: + parent_id = self._schema.parent_id_map[self.block["id"]] return Location( section_id=self._current_location.section_id, block_id=parent_id ) - def _get_routing_path(self): - return self.router.routing_path(section_id=self._current_location.section_id) + def _get_routing_path(self) -> RoutingPath: + return self.router.routing_path(self.parent_location.section_key) - def is_location_valid(self): - primary_person_list_item_id = self._questionnaire_store.list_store[ - self.current_location.list_name + def is_location_valid(self) -> bool: + primary_person_list_item_id = self._questionnaire_store.data_stores.list_store[ + # Type ignore: list_name will exist by this point + self.current_location.list_name # type: ignore ].primary_person return ( @@ -30,18 +35,16 @@ def is_location_valid(self): ) ) - def get_previous_location_url(self): + def get_previous_location_url(self) -> str: return self.parent_location.url() - def get_next_location_url(self): + def get_next_location_url(self) -> str: return self.router.get_next_location_url( - self.parent_location, self._routing_path, self._return_to + self.parent_location, self._routing_path, self.return_location ) - def handle_post(self): + def handle_post(self) -> None: same_name_answer_ids = self.parent_block.get("same_name_answer_ids") - # pylint: disable=no-member - # wtforms Form parents are not discoverable in the 2.3.3 implementation self.questionnaire_store_updater.update_answers(self.form.data) self.questionnaire_store_updater.update_same_name_items( self.parent_block["for_list"], same_name_answer_ids @@ -51,5 +54,7 @@ def handle_post(self): location=self.parent_location ) + self.questionnaire_store_updater.remove_dependent_blocks_and_capture_dependent_sections() self._update_section_completeness(location=self.parent_location) + self.questionnaire_store_updater.update_progress_for_dependent_sections() self.questionnaire_store_updater.save() diff --git a/app/views/handlers/question.py b/app/views/handlers/question.py index 97a574c24c..0bbf284709 100644 --- a/app/views/handlers/question.py +++ b/app/views/handlers/question.py @@ -1,12 +1,12 @@ from functools import cached_property -from typing import Any +from typing import Mapping, Sequence from flask import url_for from flask_babel import gettext -from app.forms.questionnaire_form import generate_form +from app.forms.questionnaire_form import QuestionnaireForm, generate_form from app.helpers import get_address_lookup_api_auth_token -from app.questionnaire.location import Location +from app.questionnaire.location import Location, SectionKey from app.questionnaire.questionnaire_store_updater import QuestionnaireStoreUpdater from app.questionnaire.variants import transform_variants from app.views.contexts import ListContext @@ -16,54 +16,47 @@ class Question(BlockHandler): @staticmethod - def _has_redirect_to_list_add_action(answer_action): - return answer_action and answer_action["type"] == "RedirectToListAddBlock" + def _has_redirect_to_list_add_action(answer_action: Mapping | None) -> bool: + return bool(answer_action and answer_action["type"] == "RedirectToListAddBlock") @cached_property - def form(self): - question_json = self.rendered_block.get("question") + def form(self) -> QuestionnaireForm: + question_json = self.rendered_block.get("question", {}) + if self._form_data: return generate_form( - self._schema, - question_json, - self._questionnaire_store.answer_store, - self._questionnaire_store.list_store, - self._questionnaire_store.metadata, - self._questionnaire_store.response_metadata, - self._current_location, + schema=self._schema, + question_schema=question_json, + data_stores=self._questionnaire_store.data_stores, + location=self._current_location, form_data=self._form_data, ) answers = self._get_answers_for_question(question_json) return generate_form( - self._schema, - question_json, - self._questionnaire_store.answer_store, - self._questionnaire_store.list_store, - self._questionnaire_store.metadata, - self._questionnaire_store.response_metadata, - self._current_location, + schema=self._schema, + question_schema=question_json, + data_stores=self._questionnaire_store.data_stores, + location=self._current_location, data=answers, ) @cached_property - def questionnaire_store_updater(self): + def questionnaire_store_updater(self) -> QuestionnaireStoreUpdater: return QuestionnaireStoreUpdater( self._current_location, self._schema, self._questionnaire_store, + self.router, self.rendered_block.get("question"), ) @cached_property - def rendered_block(self): + def rendered_block(self) -> dict: transformed_block = transform_variants( self.block, self._schema, - self._questionnaire_store.metadata, - self._questionnaire_store.response_metadata, - self._questionnaire_store.answer_store, - self._questionnaire_store.list_store, + self._questionnaire_store.data_stores, self._current_location, ) page_title = transformed_block.get("page_title") or self._get_safe_page_title( @@ -71,8 +64,14 @@ def rendered_block(self): ) self._set_page_title(page_title) + + # We inherit from question in list collector content block which doesn't have "question" sub-block + if not transformed_block.get("question"): + return transformed_block + rendered_question = self.placeholder_renderer.render( - transformed_block["question"], self._current_location.list_item_id + data_to_render=transformed_block["question"], + list_item_id=self._current_location.list_item_id, ) return { **transformed_block, @@ -80,64 +79,80 @@ def rendered_block(self): } @cached_property - def list_context(self): + def list_context(self) -> ListContext: return ListContext( - self._language, - self._schema, - self._questionnaire_store.answer_store, - self._questionnaire_store.list_store, - self._questionnaire_store.progress_store, - self._questionnaire_store.metadata, - self._questionnaire_store.response_metadata, + language=self._language, + schema=self._schema, + data_stores=self._questionnaire_store.data_stores, ) - def get_next_location_url(self): + def get_next_location_url(self) -> str: answer_action = self._get_answer_action() if self._has_redirect_to_list_add_action(answer_action): - location_url = self._get_list_add_question_url(answer_action["params"]) + # Type ignore: this is only called if answer_action is not none. + location_url = self._get_list_add_question_url(answer_action["params"]) # type: ignore if location_url: return location_url return self.router.get_next_location_url( - self._current_location, self._routing_path, self._return_to + self._current_location, self._routing_path, self._return_location ) - def _get_answers_for_question(self, question_json) -> dict[str, Any]: - answer_ids = self._schema.get_answer_ids_for_question(question_json) - answers = self._questionnaire_store.answer_store.get_answers_by_answer_id( - answer_ids=answer_ids, list_item_id=self._current_location.list_item_id + def _get_answers_for_question(self, question_json: Mapping) -> dict: + answers_by_answer_id = self._schema.get_answers_for_question_by_id( + question_json ) - return {answer.answer_id: answer.value for answer in answers if answer} + answer_value_by_answer_id = {} - def _get_list_add_question_url(self, params): + for answer_id, resolved_answer in answers_by_answer_id.items(): + list_item_id = ( + resolved_answer.get("list_item_id") + or self._current_location.list_item_id + ) + answer_id_to_use = resolved_answer.get("original_answer_id") or answer_id + if answer := self._questionnaire_store.data_stores.answer_store.get_answer( + answer_id=answer_id_to_use, list_item_id=list_item_id + ): + answer_value_by_answer_id[answer_id] = answer.value + + return answer_value_by_answer_id + + def _get_list_add_question_url(self, params: dict) -> str | None: block_id = params["block_id"] list_name = params["list_name"] - list_items = self._questionnaire_store.list_store[list_name].items + list_items = self._questionnaire_store.data_stores.list_store[list_name].items section_id = self._schema.get_section_id_for_block_id(block_id) if self._is_list_just_primary(list_items, list_name) or not list_items: return Location( - section_id=section_id, block_id=block_id, list_name=list_name + # Type ignore: section_id is always valid when this is called + section_id=section_id, # type: ignore + block_id=block_id, + list_name=list_name, ).url(previous=self.current_location.block_id) - def _is_list_just_primary(self, list_items, list_name): + def _is_list_just_primary(self, list_items: list[str], list_name: str) -> bool: return ( len(list_items) == 1 and list_items[0] - == self._questionnaire_store.list_store[list_name].primary_person + == self._questionnaire_store.data_stores.list_store[ + list_name + ].primary_person ) - def _get_answer_action(self): + def _get_answer_action(self) -> dict | None: + # When used by list collector content class rendered block we won't have "question" sub-block + if not self.rendered_block.get("question"): + return None + answers = self.rendered_block["question"]["answers"] for answer in answers: - # pylint: disable=no-member - # wtforms Form parents are not discoverable in the 2.3.3 implementation submitted_answer = self.form.data[answer["id"]] for option in answer.get("options", {}): - action = option.get("action") + action: dict | None = option.get("action") if action and ( option["value"] == submitted_answer @@ -145,17 +160,15 @@ def _get_answer_action(self): ): return action - def get_context(self): + def get_context(self) -> dict[str, dict]: context = build_question_context(self.rendered_block, self.form) context["return_to_hub_url"] = self.get_return_to_hub_url() - context[ - "last_viewed_question_guidance" - ] = self.get_last_viewed_question_guidance_context() + context["last_viewed_question_guidance"] = ( + self.get_last_viewed_question_guidance_context() + ) if "list_summary" in self.rendered_block: context.update(self.get_list_summary_context()) - # pylint: disable=no-member - # wtforms Form parents are not discoverable in the 2.3.3 implementation if self.form.errors or self.form.question_errors: self.page_title = gettext("Error: {page_title}").format( page_title=self.page_title @@ -168,63 +181,87 @@ def get_context(self): return context - def get_last_viewed_question_guidance_context(self): + def get_last_viewed_question_guidance_context(self) -> dict | bool | None: if self.resume: first_location_in_section_url = self.router.get_first_location_in_section( self._routing_path ).url() return {"first_location_in_section_url": first_location_in_section_url} - def get_list_summary_context(self): + def get_list_summary_context(self) -> dict: return self.list_context( - self.rendered_block["list_summary"]["summary"], - self.rendered_block["list_summary"]["for_list"], + summary_definition=self.rendered_block["list_summary"]["summary"], + for_list=self.rendered_block["list_summary"]["for_list"], + section_id=self.current_location.section_id, + has_repeating_blocks=bool(self.rendered_block.get("repeating_blocks")), ) - def handle_post(self): - # pylint: disable=no-member - # wtforms Form parents are not discoverable in the 2.3.3 implementation + def handle_post(self) -> None: self.questionnaire_store_updater.update_answers(self.form.data) - self.questionnaire_store_updater.update_progress_for_dependant_sections() if self.questionnaire_store_updater.is_dirty(): + # We prematurely complete the block, as we need it completed to build the routing path + # In order to support progress value source references of the previous block + self.questionnaire_store_updater.add_completed_location( + self.current_location + ) self._routing_path = self.router.routing_path( - section_id=self._current_location.section_id, - list_item_id=self._current_location.list_item_id, + self._current_location.section_key ) super().handle_post() - def get_return_to_hub_url(self): + def get_return_to_hub_url(self) -> str | None: if ( self.rendered_block["type"] in ["Question", "ConfirmationQuestion"] and self.router.can_access_hub() ): return url_for(".get_questionnaire") - def evaluate_and_update_section_status_on_list_change(self, list_name): - section_ids = self._schema.get_section_ids_dependent_on_list(list_name) - - section_keys_to_evaluate = ( - self.questionnaire_store_updater.started_section_keys( - section_ids=section_ids - ) - ) - - for section_id, list_item_id in section_keys_to_evaluate: - path = self.router.routing_path(section_id, list_item_id) - self.questionnaire_store_updater.update_section_status( - is_complete=self.router.is_path_complete(path), - section_id=section_id, - list_item_id=list_item_id, - ) - - def clear_radio_answers(self): - answer_ids_to_remove = [] - for answer in self.rendered_block["question"]["answers"]: - if answer["type"] == "Radio": - answer_ids_to_remove.append(answer["id"]) - + def clear_radio_answers(self) -> None: + answer_ids_to_remove = [ + answer["id"] + for answer in self.rendered_block["question"]["answers"] + if answer["type"] == "Radio" + ] if answer_ids_to_remove: self.questionnaire_store_updater.remove_answers( answer_ids_to_remove, self.current_location.list_item_id ) self.questionnaire_store_updater.save() + + def get_first_incomplete_list_repeating_block_location( + self, *, repeating_block_ids: Sequence[str], section_id: str, list_name: str + ) -> Location | None: + if not repeating_block_ids: + return None + + list_model = self._questionnaire_store.data_stores.list_store.get(list_name) + for list_item_id in list_model.items: + if incomplete_location := self.get_first_incomplete_list_repeating_block_location_for_list_item( + repeating_block_ids=repeating_block_ids, + section_key=SectionKey(section_id, list_item_id), + list_name=list_name, + ): + return incomplete_location + + def get_first_incomplete_list_repeating_block_location_for_list_item( + self, + *, + repeating_block_ids: Sequence[str], + section_key: SectionKey, + list_name: str, + ) -> Location | None: + if self._questionnaire_store.data_stores.progress_store.is_section_complete( + section_key + ): + return None + + for repeating_block_id in repeating_block_ids: + if not self._questionnaire_store.data_stores.progress_store.is_block_complete( + block_id=repeating_block_id, + section_key=section_key, + ): + return Location( + block_id=repeating_block_id, + list_name=list_name, + **section_key.to_dict(), + ) diff --git a/app/views/handlers/relationships/__init__.py b/app/views/handlers/relationships/__init__.py index f1cb95729a..2aa6662dc1 100644 --- a/app/views/handlers/relationships/__init__.py +++ b/app/views/handlers/relationships/__init__.py @@ -1,4 +1,6 @@ -from .relationship_collector import RelationshipCollector -from .unrelated_question import UnrelatedQuestion +from app.views.handlers.relationships.relationship_collector import ( + RelationshipCollector, +) +from app.views.handlers.relationships.unrelated_question import UnrelatedQuestion __all__ = ["RelationshipCollector", "UnrelatedQuestion"] diff --git a/app/views/handlers/relationships/relationship_collector.py b/app/views/handlers/relationships/relationship_collector.py index 4f9ea82335..e4e79d54ac 100644 --- a/app/views/handlers/relationships/relationship_collector.py +++ b/app/views/handlers/relationships/relationship_collector.py @@ -1,4 +1,4 @@ -from typing import MutableMapping +from typing import Mapping, MutableMapping from app.data_models.relationship_store import Relationship from app.questionnaire.location import Location @@ -6,7 +6,7 @@ class RelationshipCollector(RelationshipQuestion): - def is_location_valid(self): + def is_location_valid(self) -> bool: if isinstance(self._current_location, Location): return self.router.can_access_location( self._current_location, self._routing_path @@ -14,13 +14,12 @@ def is_location_valid(self): return super().is_location_valid() - def handle_post(self): - # pylint: disable=no-member - # wtforms Form parents are not discoverable in the 2.3.3 implementation + def handle_post(self) -> None: relationship_answer = self.form.data.get(self.relationships_answer_id) relationship = Relationship( - self._current_location.list_item_id, - self._current_location.to_list_item_id, + # Type ignore: handle_post is only called from relationships endpoint and location class is assigned to RelationshipLocation + self._current_location.list_item_id, # type: ignore + self._current_location.to_list_item_id, # type: ignore relationship_answer, ) self.relationship_store.add_or_update(relationship) @@ -39,28 +38,31 @@ def handle_post(self): self.questionnaire_store_updater.save() - def _get_answers_for_question(self, question_json): + def _get_answers_for_question(self, question_json: Mapping) -> dict: relationship = self.relationship_store.get_relationship( - self._current_location.list_item_id, - self._current_location.to_list_item_id, + # relationship only used if these exist. RelationshipLocation wil already be determined. + self._current_location.list_item_id, # type: ignore + self._current_location.to_list_item_id, # type: ignore ) if relationship: return {self.relationships_answer_id: relationship.relationship} return {} - def _is_last_relationship(self): - if self.relationship_router.get_next_location(self._current_location): + def _is_last_relationship(self) -> bool: + if self.relationship_router.get_next_location(self._current_location): # type: ignore return False return True def _resolve_custom_page_title_vars(self) -> MutableMapping: page_title_vars = super()._resolve_custom_page_title_vars() - if to_list_item_position := self.current_location.to_list_item_id: - page_title_vars[ - "to_list_item_position" - ] = self._questionnaire_store.list_store.list_item_position( - self.current_location.list_name, to_list_item_position + if to_list_item_position := self.current_location.to_list_item_id: # type: ignore + page_title_vars["to_list_item_position"] = ( + self._questionnaire_store.data_stores.list_store.list_item_position( + # Type ignore: list_name populated at this stage + self.current_location.list_name, # type: ignore + to_list_item_position, + ) ) return page_title_vars diff --git a/app/views/handlers/relationships/relationship_question.py b/app/views/handlers/relationships/relationship_question.py index 48db03399d..698d025b72 100644 --- a/app/views/handlers/relationships/relationship_question.py +++ b/app/views/handlers/relationships/relationship_question.py @@ -1,110 +1,123 @@ from functools import cached_property -from app.data_models.relationship_store import RelationshipStore +from werkzeug.datastructures import ImmutableDict + +from app.data_models.relationship_store import RelationshipDict, RelationshipStore from app.questionnaire.location import Location from app.questionnaire.relationship_router import RelationshipRouter +from app.questionnaire.router import RoutingPath from app.views.handlers.question import Question class RelationshipQuestion(Question): @cached_property - def relationships_block(self): + def relationships_block(self) -> ImmutableDict | None: return self._schema.get_block(self.block["id"]) @cached_property - def relationships_answer_id(self): + def relationships_answer_id(self) -> str: return self._schema.get_first_answer_id_for_block( - self.relationships_block["id"] + # Type ignore: block must exist when this is called + self.relationships_block["id"] # type: ignore ) @property - def parent_location(self): + def parent_location(self) -> Location: return Location( section_id=self._current_location.section_id, - block_id=self.relationships_block["id"], + # Type ignore: block must exist when this is called + block_id=self.relationships_block["id"], # type: ignore ) @cached_property - def unrelated_block_id(self): - return self.relationships_block.get("unrelated_block", {}).get("id") + def unrelated_block_id(self) -> str | None: + # Type ignore: called when relationships_block already exists + value: str = self.relationships_block.get("unrelated_block", {}).get("id") # type: ignore + return value @cached_property - def unrelated_answer_id(self): + def unrelated_answer_id(self) -> str | None: if self.unrelated_block_id: return self._schema.get_first_answer_id_for_block(self.unrelated_block_id) return None @cached_property - def unrelated_no_answer_values(self): + def unrelated_no_answer_values(self) -> list[str] | None: if self.unrelated_answer_id: return self._schema.get_unrelated_block_no_answer_values( self.unrelated_answer_id ) @cached_property - def relationship_store(self): - answer = self._questionnaire_store.answer_store.get_answer( + def relationship_store(self) -> RelationshipStore: + answer = self._questionnaire_store.data_stores.answer_store.get_answer( self.relationships_answer_id ) if answer: - return RelationshipStore(answer.value) + # Type ignore: for a relationship question the answer will always be a list of RelationshipDict + answer_value: list[RelationshipDict] = answer.value # type: ignore + return RelationshipStore(answer_value) return RelationshipStore() @property - def relationship_router(self): - list_name = self.relationships_block["for_list"] - list_items = self._questionnaire_store.list_store[list_name].items + def relationship_router(self) -> RelationshipRouter: + # Type ignore: list will be populated at this point as it is required to build relationship + list_name = self.relationships_block["for_list"] # type: ignore + list_items = self._questionnaire_store.data_stores.list_store[list_name].items return RelationshipRouter( - answer_store=self._questionnaire_store.answer_store, + answer_store=self._questionnaire_store.data_stores.answer_store, relationship_store=self.relationship_store, section_id=self._current_location.section_id, list_name=list_name, list_item_ids=list_items, - relationships_block_id=self.relationships_block["id"], + # Type ignore: block must exist before getting to this point + relationships_block_id=self.relationships_block["id"], # type: ignore unrelated_block_id=self.unrelated_block_id, unrelated_answer_id=self.unrelated_answer_id, unrelated_no_answer_values=self.unrelated_no_answer_values, ) - def _get_routing_path(self): - return self.router.routing_path(section_id=self.parent_location.section_id) + def _get_routing_path(self) -> RoutingPath: + return self.router.routing_path(self.parent_location.section_key) - def is_location_valid(self): + def is_location_valid(self) -> bool: can_access_parent_location = self.router.can_access_location( self.parent_location, self._routing_path ) can_access_relationship_location = self.relationship_router.can_access_location( - self._current_location + self._current_location # type: ignore ) if not can_access_parent_location or not can_access_relationship_location: return False return True - def get_first_location_url(self): + def get_first_location_url(self) -> str: return self.relationship_router.get_first_location().url() - def get_last_location_url(self): + def get_last_location_url(self) -> str: return self.relationship_router.get_last_location().url() - def get_previous_location_url(self): + def get_previous_location_url(self) -> str: previous_location = self.relationship_router.get_previous_location( - self._current_location + # Type ignore: block will determine type of location to be relationship location + self._current_location # type: ignore ) if previous_location: return previous_location.url() - return self.router.get_previous_location_url( - self.parent_location, self._routing_path + return self.router.get_previous_location_url( # type: ignore + self.parent_location, self._routing_path, self.return_location ) - def get_next_location_url(self): + def get_next_location_url(self) -> str: next_location = self.relationship_router.get_next_location( - self._current_location + # Type ignore: block will determine type of location to be relationship location + self._current_location # type: ignore ) if next_location: return next_location.url() return self.router.get_next_location_url( - self.parent_location, self._routing_path, self._return_to + self.parent_location, self._routing_path, self.return_location ) diff --git a/app/views/handlers/relationships/unrelated_question.py b/app/views/handlers/relationships/unrelated_question.py index 500b0cc936..dd9476c002 100644 --- a/app/views/handlers/relationships/unrelated_question.py +++ b/app/views/handlers/relationships/unrelated_question.py @@ -1,74 +1,82 @@ from functools import cached_property +from werkzeug.datastructures import ImmutableDict + from app.data_models.relationship_store import Relationship from app.views.handlers.relationships.relationship_question import RelationshipQuestion class UnrelatedQuestion(RelationshipQuestion): @cached_property - def list_name(self): - return self.block["list_summary"]["for_list"] + def list_name(self) -> str: + value: str = self.block["list_summary"]["for_list"] + return value @cached_property - def relationships_block(self): + def relationships_block(self) -> ImmutableDict | None: parent_block_id = self._schema.parent_id_map[self.block["id"]] return self._schema.get_block(parent_block_id) @cached_property - def unrelated_block_id(self): - return self.block["id"] + def unrelated_block_id(self) -> str | None: + value: str | None = self.block["id"] + return value - def get_list_summary_context(self): + def get_list_summary_context(self) -> dict[str, dict]: return self.list_context( - self.rendered_block["list_summary"]["summary"], - self.list_name, + summary_definition=self.rendered_block["list_summary"]["summary"], + for_list=self.list_name, + section_id=self.current_location.section_id, + has_repeating_blocks=bool(self.rendered_block.get("repeating_blocks")), for_list_item_ids=self.get_remaining_relationships_for_individual(), ) - def get_remaining_relationships_for_individual(self): + def get_remaining_relationships_for_individual(self) -> list[str]: """ Returns a list of 'to' item ids for the remaining relationships. These relationships won't be on the path if the user has selected "No" to the unrelated question, so we get them from the list store. """ - list_model = self._questionnaire_store.list_store[self.list_name] + list_model = self._questionnaire_store.data_stores.list_store[self.list_name] previous_location = self.relationship_router.get_previous_location( - self.current_location + # Type ignore: block will determine type of location to be relationship location + self.current_location # type: ignore ) - previous_item_index = list_model.index(previous_location.to_list_item_id) + previous_item_index = list_model.index(previous_location.to_list_item_id) # type: ignore return list_model[previous_item_index + 1 :] - def handle_post(self): + def handle_post(self) -> None: if answer_action := self._get_answer_action(): self.handle_answer_action(answer_action["type"]) - # pylint: disable=no-member - # wtforms Form parents are not discoverable in the 2.3.3 implementation self.questionnaire_store_updater.update_answers( self.form.data, list_item_id=self.current_location.list_item_id ) self.questionnaire_store_updater.save() - def handle_answer_action(self, answer_action_type): + def handle_answer_action(self, answer_action_type: str) -> None: from_list_item_id = self.current_location.list_item_id if answer_action_type == "RemoveUnrelatedRelationships": for to_list_item_id in self.get_remaining_relationships_for_individual(): relationship = self.relationship_store.get_relationship( - from_list_item_id, to_list_item_id + # Type ignore: from_list_item_id populated at this stage + from_list_item_id, # type: ignore + to_list_item_id, ) if relationship and ( relationship.relationship == self.relationship_router.UNRELATED_RELATIONSHIP_VALUE ): self.relationship_store.remove_relationship( - from_list_item_id, to_list_item_id + from_list_item_id, to_list_item_id # type: ignore ) elif answer_action_type == "AddUnrelatedRelationships": for to_list_item_id in self.get_remaining_relationships_for_individual(): relationship = Relationship( - list_item_id=from_list_item_id, + # Type ignore: from_list_item_id populated at this stage + list_item_id=from_list_item_id, # type: ignore to_list_item_id=to_list_item_id, relationship=self.relationship_router.UNRELATED_RELATIONSHIP_VALUE, ) diff --git a/app/views/handlers/section.py b/app/views/handlers/section.py index 8e2bff1107..779626fe9f 100644 --- a/app/views/handlers/section.py +++ b/app/views/handlers/section.py @@ -1,25 +1,33 @@ +from typing import Mapping + +from app.data_models import QuestionnaireStore +from app.questionnaire import QuestionnaireSchema from app.questionnaire.location import InvalidLocationException, Location from app.questionnaire.router import Router from app.views.contexts import SectionSummaryContext class SectionHandler: - def __init__(self, schema, questionnaire_store, section_id, list_item_id, language): + def __init__( + self, + schema: QuestionnaireSchema, + questionnaire_store: QuestionnaireStore, + section_id: str, + list_item_id: str | None, + language: str, + ): self._schema = schema self._questionnaire_store = questionnaire_store self._section_id = section_id self._list_item_id = list_item_id self._language = language self._router = Router( - schema, - questionnaire_store.answer_store, - questionnaire_store.list_store, - questionnaire_store.progress_store, - questionnaire_store.metadata, - questionnaire_store.response_metadata, + schema=schema, + data_stores=self._questionnaire_store.data_stores, ) if not self._is_valid_location(): - raise InvalidLocationException(f"location {self._section_id} is not valid") + location_error_message = f"location {self._section_id} is not valid" + raise InvalidLocationException(location_error_message) self.current_location = Location( section_id=self._section_id, @@ -28,36 +36,32 @@ def __init__(self, schema, questionnaire_store, section_id, list_item_id, langua ) self._routing_path = self._router.routing_path( - section_id=self._section_id, list_item_id=self._list_item_id + self.current_location.section_key ) - def get_context(self): + def get_context(self) -> Mapping: section_summary_context = SectionSummaryContext( self._language, self._schema, - self._questionnaire_store.answer_store, - self._questionnaire_store.list_store, - self._questionnaire_store.progress_store, - self._questionnaire_store.metadata, - self._questionnaire_store.response_metadata, + self._questionnaire_store.data_stores, self._routing_path, self.current_location, ) return section_summary_context() - def get_next_location_url(self): + def get_next_location_url(self) -> str: return self._router.get_next_location_url_for_end_of_section() - def get_previous_location_url(self): + def get_previous_location_url(self) -> str: return self._router.get_last_location_in_section(self._routing_path).url() - def get_resume_url(self): + def get_resume_url(self) -> str: return self._router.get_section_resume_url(self._routing_path) - def can_display_summary(self): + def can_display_summary(self) -> bool: return self._router.can_display_section_summary( - self._section_id, self._list_item_id + self.current_location.section_key ) - def _is_valid_location(self): + def _is_valid_location(self) -> bool: return self._section_id in self._router.enabled_section_ids diff --git a/app/views/handlers/submission.py b/app/views/handlers/submission.py index 081e172828..75d3a5b997 100644 --- a/app/views/handlers/submission.py +++ b/app/views/handlers/submission.py @@ -5,37 +5,71 @@ from flask import session as cookie_session from sdc.crypto.encrypter import encrypt +from app.authentication.auth_payload_versions import AuthPayloadVersion +from app.data_models import QuestionnaireStore +from app.data_models.metadata_proxy import MetadataProxy from app.globals import get_session_store from app.keys import KEY_PURPOSE_SUBMISSION -from app.questionnaire.questionnaire_schema import DEFAULT_LANGUAGE_CODE -from app.submitter.converter import convert_answers +from app.questionnaire.questionnaire_schema import ( + DEFAULT_LANGUAGE_CODE, + QuestionnaireSchema, +) +from app.questionnaire.routing_path import RoutingPath +from app.submitter.converter_v2 import convert_answers_v2 from app.submitter.submission_failed import SubmissionFailedException from app.utilities.json import json_dumps +def get_receipting_metadata(metadata: MetadataProxy) -> dict: + return ( + {item: metadata[item] for item in metadata.survey_metadata.receipting_keys} + if ( + metadata.version is AuthPayloadVersion.V2 + and metadata.survey_metadata + and metadata.survey_metadata.receipting_keys + ) + else {} + ) + + class SubmissionHandler: - def __init__(self, schema, questionnaire_store, full_routing_path): + def __init__( + self, + schema: QuestionnaireSchema, + questionnaire_store: QuestionnaireStore, + full_routing_path: list[RoutingPath], + ): self._schema = schema self._questionnaire_store = questionnaire_store self._full_routing_path = full_routing_path self._session_store = get_session_store() - self._metadata = questionnaire_store.metadata + # Type ignore: It isn't possible to not have metadata at this point + self._metadata: MetadataProxy = questionnaire_store.data_stores.metadata # type: ignore @cached_property - def submitted_at(self): + def submitted_at(self) -> datetime: return datetime.now(timezone.utc).replace(microsecond=0) - def submit_questionnaire(self): + def submit_questionnaire(self) -> None: payload = self.get_payload() + message = json_dumps(payload) encrypted_message = encrypt( - message, current_app.eq["key_store"], KEY_PURPOSE_SUBMISSION + message, + # Type ignore: current_app can return empty Local Proxy. Similar to other files, this is ignored. + current_app.eq["key_store"], # type: ignore + KEY_PURPOSE_SUBMISSION, ) - submitted = current_app.eq["submitter"].send_message( + + additional_metadata = get_receipting_metadata(self._metadata) + + # Type ignore: current_app can return empty Local Proxy. Similar to other files, this is ignored. + submitted = current_app.eq["submitter"].send_message( # type: ignore encrypted_message, - case_id=self._metadata["case_id"], - tx_id=self._metadata.get("tx_id"), + case_id=self._metadata.case_id, + tx_id=self._metadata.tx_id, + **additional_metadata, ) if not submitted: @@ -43,23 +77,20 @@ def submit_questionnaire(self): cookie_session["submitted"] = True - self._store_display_address_in_session() self._questionnaire_store.submitted_at = self.submitted_at self._questionnaire_store.save() - def get_payload(self): - payload = convert_answers( + def get_payload(self) -> dict: + payload = convert_answers_v2( self._schema, self._questionnaire_store, self._full_routing_path, self.submitted_at, ) + payload["submission_language_code"] = ( - self._session_store.session_data.language_code or DEFAULT_LANGUAGE_CODE + # Type ignore: session_data will exist by this stage + self._session_store.session_data.language_code # type: ignore + or DEFAULT_LANGUAGE_CODE ) return payload - - def _store_display_address_in_session(self): - session_data = self._session_store.session_data - session_data.display_address = self._metadata.get("display_address") - self._session_store.save() diff --git a/app/views/handlers/submit_questionnaire.py b/app/views/handlers/submit_questionnaire.py index 1b568ba789..c7227ad3cd 100644 --- a/app/views/handlers/submit_questionnaire.py +++ b/app/views/handlers/submit_questionnaire.py @@ -1,7 +1,6 @@ from __future__ import annotations from functools import cached_property -from typing import Union from app.data_models import QuestionnaireStore from app.questionnaire import QuestionnaireSchema @@ -12,6 +11,8 @@ class SubmitQuestionnaireHandler: + SUBMIT_PAGE_DISABLED_ERROR_MESSAGE = "Submit page not enabled" + def __init__( self, schema: QuestionnaireSchema, @@ -19,7 +20,7 @@ def __init__( language: str, ): if not schema.is_flow_linear: - raise InvalidLocationException("Submit page not enabled") + raise InvalidLocationException(self.SUBMIT_PAGE_DISABLED_ERROR_MESSAGE) self._schema = schema self._questionnaire_store = questionnaire_store @@ -29,26 +30,18 @@ def __init__( def router(self) -> Router: return Router( schema=self._schema, - answer_store=self._questionnaire_store.answer_store, - list_store=self._questionnaire_store.list_store, - progress_store=self._questionnaire_store.progress_store, - metadata=self._questionnaire_store.metadata, - response_metadata=self._questionnaire_store.response_metadata, + data_stores=self._questionnaire_store.data_stores, ) - def get_context(self) -> dict[str, Union[str, dict]]: + def get_context(self) -> dict[str, str | dict]: submit_questionnaire_context = SubmitQuestionnaireContext( language=self._language, schema=self._schema, - answer_store=self._questionnaire_store.answer_store, - list_store=self._questionnaire_store.list_store, - progress_store=self._questionnaire_store.progress_store, - metadata=self._questionnaire_store.metadata, - response_metadata=self._questionnaire_store.response_metadata, + data_stores=self._questionnaire_store.data_stores, ) return submit_questionnaire_context() - def get_previous_location_url(self) -> str: + def get_previous_location_url(self) -> str | None: return self.router.get_last_location_in_questionnaire_url() @property diff --git a/app/views/handlers/thank_you.py b/app/views/handlers/thank_you.py index abb9c45e1e..a9593409dd 100644 --- a/app/views/handlers/thank_you.py +++ b/app/views/handlers/thank_you.py @@ -1,16 +1,15 @@ from datetime import datetime from functools import cached_property -from flask import session as cookie_session -from flask_babel import gettext +from flask_babel import LazyString, gettext +from flask_login import current_user +from app.data_models.metadata_proxy import MetadataProxy from app.data_models.session_store import SessionStore +from app.globals import get_metadata from app.helpers.template_helpers import get_survey_type from app.questionnaire import QuestionnaireSchema -from app.views.contexts.thank_you_context import ( - build_census_thank_you_context, - build_thank_you_context, -) +from app.views.contexts.thank_you_context import build_thank_you_context from app.views.handlers.confirmation_email import ( ConfirmationEmail, ConfirmationEmailLimitReached, @@ -20,8 +19,7 @@ class ThankYou: DEFAULT_THANK_YOU_TEMPLATE = "thank-you" - CENSUS_THANK_YOU_TEMPLATE = "census-thank-you" - PAGE_TITLE = gettext("Thank you for completing the census") + PAGE_TITLE = gettext("Thank you for completing the survey") def __init__( self, @@ -33,45 +31,33 @@ def __init__( self._schema: QuestionnaireSchema = schema self._submitted_at = submitted_at - self._is_census_theme = cookie_session.get("theme") in [ - "census", - "census-nisra", - ] - self.template = ( - self.CENSUS_THANK_YOU_TEMPLATE - if self._is_census_theme - else self.DEFAULT_THANK_YOU_TEMPLATE - ) + self.template = self.DEFAULT_THANK_YOU_TEMPLATE @cached_property - def confirmation_email(self): + def confirmation_email(self) -> ConfirmationEmail | None: try: return ConfirmationEmail(self._session_store, self._schema, self.PAGE_TITLE) except (ConfirmationEmailNotEnabled, ConfirmationEmailLimitReached): return None - def get_context(self): - if not self._is_census_theme: - guidance_content = self._schema.get_post_submission().get("guidance") - return build_thank_you_context( - self._schema, - self._session_store.session_data, - self._submitted_at, - get_survey_type(), - guidance_content, - ) + def get_context(self) -> dict: + metadata: MetadataProxy = get_metadata(current_user) # type: ignore confirmation_email_form = ( self.confirmation_email.form if self.confirmation_email else None ) - return build_census_thank_you_context( - self._session_store.session_data, + guidance_content = self._schema.get_post_submission().get("guidance") + return build_thank_you_context( + self._schema, + metadata, + self._submitted_at, + get_survey_type(), + guidance_content, confirmation_email_form, - self._schema.form_type, ) - def get_page_title(self): + def get_page_title(self) -> str | LazyString: if self.confirmation_email: return self.confirmation_email.get_page_title() return self.PAGE_TITLE diff --git a/app/views/handlers/view_preview_questions.py b/app/views/handlers/view_preview_questions.py new file mode 100644 index 0000000000..bff73ac906 --- /dev/null +++ b/app/views/handlers/view_preview_questions.py @@ -0,0 +1,39 @@ +from flask import url_for + +from app.data_models import QuestionnaireStore +from app.helpers.template_helpers import render_template +from app.questionnaire import QuestionnaireSchema +from app.views.contexts.preview_context import PreviewContext + + +class ViewPreviewQuestions: + def __init__( + self, + schema: QuestionnaireSchema, + questionnaire_store: QuestionnaireStore, + language: str, + ): + self._schema = schema + self._questionnaire_store = questionnaire_store + self._language = language + + def get_context(self) -> dict[str, object]: + preview_context = PreviewContext( + language=self._language, + schema=self._schema, + data_stores=self._questionnaire_store.data_stores, + ) + context = { + "hide_sign_out_button": True, + "preview": preview_context(), + "pdf_url": url_for("questionnaire.get_preview_questions_pdf"), + } + + return context + + def get_rendered_html(self) -> str: + return render_template( + template="preview", + content=self.get_context(), + page_title=PreviewContext.get_page_title(), + ) diff --git a/app/views/handlers/view_submitted_response.py b/app/views/handlers/view_submitted_response.py index d898db1422..40ee9c059f 100644 --- a/app/views/handlers/view_submitted_response.py +++ b/app/views/handlers/view_submitted_response.py @@ -1,5 +1,4 @@ from datetime import datetime -from typing import Union from flask_babel import lazy_gettext @@ -42,7 +41,7 @@ def has_expired(self) -> bool: ) return False - def get_context(self) -> dict[str, Union[str, datetime, dict]]: + def get_context(self) -> dict[str, str | datetime | dict]: return build_view_submitted_response_context( self._language, self._schema, self._questionnaire_store, get_survey_type() ) diff --git a/app/views/handlers/view_submitted_response_pdf.py b/app/views/handlers/view_submitted_response_pdf.py index be913e4f80..d76885ae19 100644 --- a/app/views/handlers/view_submitted_response_pdf.py +++ b/app/views/handlers/view_submitted_response_pdf.py @@ -1,44 +1,15 @@ import io -import pdfkit -from flask import current_app - from app.data_models import QuestionnaireStore from app.questionnaire import QuestionnaireSchema +from app.views.handlers.pdf_response import PDFResponse from app.views.handlers.view_submitted_response import ( ViewSubmittedResponse, ViewSubmittedResponseExpired, ) -class ViewSubmittedResponsePDF(ViewSubmittedResponse): - """ - Responsible for the PDF generation for the view submitted response. - - Subclassed from `ViewSubmittedResponse`. - - Attributes: - schema: The questionnaire schema object representing the schema JSON. - questionnaire_store: The questionnaire store object. - language: The language the user is currently in. - - Raises: - ViewSubmittedResponseExpired: If the submitted response has expired. - """ - - # The mimetype to use for response - mimetype = "application/pdf" - - # Options to be passed to wkhtmltopdf via PDFKit - wkhtmltopdf_options = { - "quiet": "", - "margin-top": "0.30in", - "margin-right": "0.40in", - "margin-bottom": "0.30in", - "margin-left": "0.40in", - "dpi": 365, - } - +class ViewSubmittedResponsePDF(ViewSubmittedResponse, PDFResponse): def __init__( self, schema: QuestionnaireSchema, @@ -46,27 +17,13 @@ def __init__( language: str, ): super().__init__(schema, questionnaire_store, language) - self._metadata = self._questionnaire_store.metadata - if self.has_expired: raise ViewSubmittedResponseExpired - @property - def filename(self) -> str: - """The name to use for the PDF file""" - return f"{self._metadata['schema_name']}.pdf" - def get_pdf(self) -> io.BytesIO: """ Generates a PDF document from the rendered ViewSubmittedResponse html. :return: The generated PDF document as BytesIO :rtype: io.BytesIO """ - content_as_bytes = pdfkit.from_string( - input=self.get_rendered_html(), - output_path=None, - css=f'{current_app.config["PRINT_STYLE_SHEET_FILE_PATH"]}/print.css', - options=self.wkhtmltopdf_options, - ) - - return io.BytesIO(content_as_bytes) + return self._get_pdf(rendered_html=self.get_rendered_html()) diff --git a/application.py b/application.py index edf48d79ac..8f28c2e004 100755 --- a/application.py +++ b/application.py @@ -3,18 +3,17 @@ import os import sys -from structlog import configure +from structlog import configure, contextvars from structlog.dev import ConsoleRenderer from structlog.processors import JSONRenderer, TimeStamper, format_exc_info from structlog.stdlib import LoggerFactory, add_log_level -from structlog.threadlocal import wrap_dict from app.utilities.json import json_dumps def configure_logging(): log_level = logging.INFO - debug = os.getenv("FLASK_ENV") == "development" + debug = os.getenv("FLASK_DEBUG") == "1" if debug: log_level = logging.DEBUG @@ -46,6 +45,7 @@ def parse_exception(_, __, event_dict): ConsoleRenderer() if debug else JSONRenderer(serializer=json_dumps) ) processors = [ + contextvars.merge_contextvars, add_log_level, TimeStamper(key="created", fmt="iso"), add_service, @@ -55,7 +55,6 @@ def parse_exception(_, __, event_dict): ] configure( - context_class=wrap_dict(dict), logger_factory=LoggerFactory(), processors=processors, cache_logger_on_first_use=True, diff --git a/babel.cfg b/babel.cfg index ae4548a0fe..41a54f889d 100644 --- a/babel.cfg +++ b/babel.cfg @@ -2,4 +2,4 @@ [ignore: templates/components/**] [ignore: templates/layout/**] [jinja2: templates/**.html] -extensions=jinja2.ext.autoescape,jinja2.ext.with_,jinja2.ext.do +extensions=jinja2.ext.do,jinja2.ext.i18n diff --git a/ci/backup_questionnaire_state.yaml b/ci/backup_questionnaire_state.yaml index 39d211688a..e486ca2de0 100644 --- a/ci/backup_questionnaire_state.yaml +++ b/ci/backup_questionnaire_state.yaml @@ -1,6 +1,6 @@ platform: linux image_resource: - type: docker-image + type: registry-image source: repository: gcr.io/google.com/cloudsdktool/cloud-sdk tag: alpine diff --git a/ci/deploy_app.sh b/ci/deploy_app.sh index 488b524a9f..6adef551e4 100755 --- a/ci/deploy_app.sh +++ b/ci/deploy_app.sh @@ -61,8 +61,7 @@ ADDRESS_LOOKUP_API_AUTH_TOKEN_LEEWAY_IN_SECONDS="${ADDRESS_LOOKUP_API_AUTH_TOKEN CONFIRMATION_EMAIL_LIMIT="${CONFIRMATION_EMAIL_LIMIT:=10}" VIEW_SUBMITTED_RESPONSE_EXPIRATION_IN_SECONDS="${VIEW_SUBMITTED_RESPONSE_EXPIRATION_IN_SECONDS:=2700}" -GOOGLE_TAG_MANAGER_ID="${GOOGLE_TAG_MANAGER_ID:=}" -GOOGLE_TAG_MANAGER_AUTH="${GOOGLE_TAG_MANAGER_AUTH:=}" +GOOGLE_TAG_ID="${GOOGLE_TAG_ID:=}" gcloud beta run deploy eq-questionnaire-runner \ @@ -109,5 +108,4 @@ gcloud beta run deploy eq-questionnaire-runner \ --set-env-vars ADDRESS_LOOKUP_API_AUTH_TOKEN_LEEWAY_IN_SECONDS="${ADDRESS_LOOKUP_API_AUTH_TOKEN_LEEWAY_IN_SECONDS}" \ --set-env-vars CONFIRMATION_EMAIL_LIMIT="${CONFIRMATION_EMAIL_LIMIT}" \ --set-env-vars VIEW_SUBMITTED_RESPONSE_EXPIRATION_IN_SECONDS="${VIEW_SUBMITTED_RESPONSE_EXPIRATION_IN_SECONDS}" \ - --set-env-vars GOOGLE_TAG_MANAGER_ID="${GOOGLE_TAG_MANAGER_ID}" \ - --set-env-vars GOOGLE_TAG_MANAGER_AUTH="${GOOGLE_TAG_MANAGER_AUTH}" + --set-env-vars GOOGLE_TAG_ID="${GOOGLE_TAG_ID}" diff --git a/ci/deploy_app.yaml b/ci/deploy_app.yaml deleted file mode 100644 index cdbb8129fe..0000000000 --- a/ci/deploy_app.yaml +++ /dev/null @@ -1,74 +0,0 @@ -platform: linux -image_resource: - type: docker-image - source: - repository: gcr.io/google.com/cloudsdktool/cloud-sdk - tag: slim -inputs: - - name: eq-questionnaire-runner - - name: image-tag - optional: true -params: - SERVICE_ACCOUNT_JSON: ((gcp.service_account_json)) - PROJECT_ID: - DOCKER_REGISTRY: - IMAGE_TAG: - REGION: - - CONCURRENCY: - MIN_INSTANCES: - MAX_INSTANCES: - CPU: - MEMORY: - - WEB_SERVER_TYPE: - WEB_SERVER_WORKERS: - WEB_SERVER_THREADS: - WEB_SERVER_UWSGI_ASYNC_CORES: - HTTP_KEEP_ALIVE: - - DATASTORE_USE_GRPC: - EQ_STORAGE_BACKEND: - EQ_ENABLE_SECURE_SESSION_COOKIE: - EQ_RABBITMQ_ENABLED: - EQ_ENABLE_HTML_MINIFY: - EQ_RABBITMQ_HOST: - EQ_RABBITMQ_HOST_SECONDARY: - EQ_QUESTIONNAIRE_STATE_TABLE_NAME: - EQ_SESSION_TABLE_NAME: - EQ_USED_JTI_CLAIM_TABLE_NAME: - EQ_SUBMISSION_BACKEND: - EQ_FEEDBACK_BACKEND: - EQ_PUBLISHER_BACKEND: - EQ_SUBMISSION_CONFIRMATION_BACKEND: - EQ_FULFILMENT_TOPIC_ID: - EQ_INDIVIDUAL_RESPONSE_LIMIT: - EQ_INDIVIDUAL_RESPONSE_POSTAL_DEADLINE: - EQ_FEEDBACK_LIMIT: - CDN_URL: - CDN_ASSETS_PATH: - ADDRESS_LOOKUP_API_URL: - ADDRESS_LOOKUP_API_AUTH_ENABLED: - ADDRESS_LOOKUP_API_AUTH_TOKEN_LEEWAY_IN_SECONDS: - CONFIRMATION_EMAIL_LIMIT: - GOOGLE_TAG_MANAGER_ID: - GOOGLE_TAG_MANAGER_AUTH: - CLOUD_ARMOR_POLICY_NAME: - VIEW_SUBMITTED_RESPONSE_EXPIRATION_IN_SECONDS: - -run: - path: bash - args: - - -exc - - | - export GOOGLE_APPLICATION_CREDENTIALS=/root/gcloud-service-key.json - cat >$GOOGLE_APPLICATION_CREDENTIALS <$GOOGLE_APPLICATION_CREDENTIALS <$GOOGLE_APPLICATION_CREDENTIALS <&1); do sleep 5; printf '.'; done diff --git a/dev-keys.yml b/dev-keys.yml index 02629bd206..e5cafc714e 100644 --- a/dev-keys.yml +++ b/dev-keys.yml @@ -1,7 +1,10 @@ keys: 2225f01580a949801274a5f3e6861947018aff5b: + platform: sdc purpose: submission + service: sdx type: public + use: encryption value: | -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu4UO1frTr8LPyyKXkQNi @@ -12,9 +15,13 @@ keys: ddIEdYRuxd9A7yWjP1I8jWC/DSq8UEAooVfe6XNsoWWF2zn9XDjhWw0hwEXSX/QT 9wIDAQAB -----END PUBLIC KEY----- + version: v1 709eb42cfee5570058ce0711f730bfbb7d4c8ade: + platform: sdc purpose: authentication + service: launcher type: public + use: signing value: | -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvZzMraB96Wd1zfHS3vW3 @@ -25,9 +32,13 @@ keys: JEfyLEM0zKrkQQ796gfYpkzDYwJvkiW7fb2Yh1teNHpFR5tozzMwUxkREl/TQ4U1 kwIDAQAB -----END PUBLIC KEY----- + version: v1 e19091072f920cbf3ca9f436ceba309e7d814a62: + platform: sdc purpose: authentication + service: eq type: private + use: encryption value: | -----BEGIN RSA PRIVATE KEY----- MIIEogIBAAKCAQEAt8LZnIhuOdL/BC029GOaJkVUAqgp2PcmbFr2Qwhf/514DUUQ @@ -56,9 +67,13 @@ keys: OZrki4MBTK/6GFkHLFkF6w2Le+Y5Nos9O2UUZs45lwLEYbQ4yKcx2KlWGLZOypB8 i7/6TB95Ej2i5KgaSlcJjOyOx7g20TwDD1THtLXgY54d0Yr9T/U= -----END RSA PRIVATE KEY----- + version: v1 fe425f951a0917d7acdd49230b23a5c405c28510: + platform: sdc purpose: submission + service: eq type: private + use: signing value: | -----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAq0rFub/fC2S4kcP4l9ivxgljh8l3hYOKG5z8mJ1cggoRqTaI @@ -87,3 +102,63 @@ keys: V7ZuRp//4vIzEONqqyLUFt1JZKuCwWQB2sRjKWpqRB07C0o243XefD5f7844nmXq H0xKMFRYjDqhFdTXFWVPnk/tu/MxoSTdK0FfsgUnjs+TGvN5TnBW -----END RSA PRIVATE KEY----- + version: v1 + df88fdad2612ae1e80571120e6c6371f55896696: + platform: sdc + purpose: supplementary_data + service: sdx + type: private + use: encryption + value: | + -----BEGIN RSA PRIVATE KEY----- + MIIJKAIBAAKCAgEAvQCr8fkBjjThO2+bQ/Y2dIsO9fRcodxiC0Gyz36NKjVc0Gqh + Kthrcyvhv+birZRGRJDi6f7i0sQ1LO+ZvJGlHD5cWZMdHvzNZTa+jVIw5DswZxzc + SZuI1/EJmCVvNOEy+Tth79IPNV3KtsKIEN044Ih/BhIYYu/mjrKfM2vddFwT1BXJ + FH5grEusaeODYhPvCSwvy5QRFiQIbm/wCP+Y8Dm3EhSPulHBBFEcy4xf+gM9fYn5 + Fww+vMnUibAGU9rZhNBDI1dV2czW7/7et7hmTtrtZsPyaqHY05Td0B2a/N7ZhyQt + zTUqbxOVKw5OgnFn6UE1vmUWYSULSPKrLig76MSITyOH1h1mTK6T5UbYyzcFBUvx + nOi7M9ddqIR57DaDJqSA6vP6ngzN8UYqr36ZOBJqpfmj+mIGQN2HhhPBrI41AVvV + YU/5bC0IHZYWjd12+bHQhf41tLUsrK53p6xEG+n0Txgp8HMhFmWoKwWXNEkNvdAE + tgH7QnObjjTV10nS09EQFgA6kYA0YwTeIiVlx9Y0J7oUCmwosMpcbaAlImlgafTh + AutTqT83Iw8gs2bCRd6h8ysHi3QiqNn6dV/QlHRHqfNBwHapHRUbBTVenwOVrzyV + VnAd5a3P574zsm/gRz/9+rBIxebQdFrre+M4DLAnzLpdqv5+y/LqH3WiNMkCAwEA + AQKCAgBRVPqpNAhRU7wgwZRFGKyyVizn9nHuTVH7mhgCZmkE4tW/8kLMlzkV5KpO + 1GJzY70hQGAFZePh4wEnByxXEy3EC6nd+gqsDQmuJnK1icr0S+w2UxsQqdenZVhF + msZSMR6oVb99Xh2hT20uXGQFLc2OAe73g83utWG3wnHzxNUVf5Ig0AcpxICBZEcb + ggZFrGJOxi8DIgKATp06OP1IQgVkStHW+/YlrYyr+OO1TAD5K2/ImBkSq/hLcWb+ + oTr31tOH7b8WdDzDbvyHZlwdH0MXZ+qFMIkfDeqqkgMpzbOmYZemKhFznw9VoU2t + q4hpZbfbjm48MnAA+dnzWEoFoNa2RJMHhZsTNSuDCXXunPvWGTymonco1eqiH+zx + X5yVtMRXtpCn1vbsJ/f39DB56X0Yj+S2edAvhOHORt9RfXZ/nBM9LngOpqjKKz1v + w9Mh7g+vGv/l4BPIznbKsQZEWj5ndtoUYFPtnMA1dKLr03jhMfAzsvamF2U8IjVZ + idzgJwIkAl00ywVQswXIAa7bW/3/moXGlHFz93qYf64mROY4ASffKMkRiaeFpTW3 + nTrIy0h+F3vZxSSc2Qw55auNDRnefNr6wsa1hrYRnzIknloK/wcaIulWGIyzEW4i + mH/RWTl+pdu7zmqb0j1BMr48wbxIuusceiC4n42AJQbFIg5BSQKCAQEA4D8kHgmA + Ct9vkq+hoyl99JnfZlfDr+N0p3+7Nc4mbVsgtl604grWjg7RIEXYFzXwfVd7pG5M + P70zqUo8gIDhZGBUksr6PZ8GcAyG8RiBE+gzkhBycw5JxRvJC/3Qsu6am1hr+M7v + peQTUolgiNekIPncJbP/j/5CQiuMlCyFk9GHWvFhaqq2pRrkkUYZ6zplzPePlnB+ + CkN0+q6HWOAXh4wCfkaUwb+xFFWiRxqYxoSL6OnPcSrXVG3BEIztMbuC9AzdPhPM + M1zaiYQf6mxeT4XOQcBMdcumxxltCUH5tiuGdYOHtB6Biq+if/p7VzjxswrejAnb + 7jqlSLJZENajDwKCAQEA18Pym+HHz0HIdUuCcky/f1tyAnNijYoZP2En4ztKwcj6 + jp5CRgQC+ufdkrpJKWqhv3kHPCZvsKk066Rr03wTxvPMhb93xH4VBZ+bf58s7bUR + 4KdJhJEf8DJbJiTBDno3ddhwtSXBfW6Eoyy6x6X6Hkw4DZm0DGaFN8vfJAZ9OprJ + q4nO0NYP1A0aYzE7BDgq5Tkr03ecNHLzIqP8zIVkkn6ewbUUG7wGbjRV7XSOpvum + GUjy04eKMIIZkWztIfJS7IuzXa2wK3I8lDu0wrmrm17lVjpZ9/ja8KaTVkgabL2P + Lg25NMk1qj0AQj88RrVkmGRWpnbuc1qJ4DOnA8zKpwKCAQA3bYP45LJAfb/vSvgy + A0R93DbK7jCRXjBsYnccsorvBtJMIZamNLWZwXHRf1INUqjR4njOSPER5CtL0eyo + erK7g9ADxKYb6x3FPmNwXnUxPXjZxrTzWXnEfbyw+RjH0ZBni3CMvGGh6IEaKpiw + 2lRYTkorC5XEur0X6/nAeky+H9FMGlPQ8Mdagg4zFle7u+CDzEEylzWgRdI5UEBm + KGXIfEP1gG6ugTo843nMB3fxwbtvY7OBrmwxEzvgYmUSoN2agz+AY5Zar73Ytc7J + u+WH1HQJ7oU3rJHZrqAz5JnbfGCs1UkKrWupowYQihJImeusLKibhqhU9yv5jxPS + xKrjAoIBAFJSvAVH3wGv+rjuJ4ZOzB3emSBgP/D7COkKu7pSTBKmCRtTPLwUGcL7 + pqmuE+4Odkpk9iK4E5NW7A8ge9eEFtOo/5bkV+ELrh+oJx9Jb03+8SRDD6TZ7lKq + E+b4zQQmE3UOMOqczjd6bHcJwPYd2NGoiRZ/V5gHobqJOck4BJ3QozOk79j0Y7On + kDLafMb+Wzd8WcFkeJ/2X9gOs4yhNJ9EWnRUD6kJU3bG1yYze54wk84/7A5TP6GE + chbvdYanO4ZvQu9yLq5U9tIj+bL2PoiYa24780nOlFKPa9XWyuZEaRXMPKbsQmKC + xc+A6xGbchdG6Vy4MgCnQcXeT1H+2C8CggEBAMvwlPifgjCk2v7ejVPvA0K63aii + l8Acv6wtvELeQGHLg/ZmzbHNZG9mExhuHASeIEn/1p64dAUQ6NTvHq78bgZ4sa2M + U6surGKebDwcbgMZe5lN6jZhem9DHWyXCLco3FHWFj6bNMuBfOdUemVi4bEukcrM + ItfML9fhBIKva5aAWej+lEQydTImhLOfgXbEse7ic+ZWVddpuqNCq137Mcbc1q0t + QChlDkY6+uOdYa3xmSCHtDc64ymzBR6XAQHQnX9fqfLWdz3Ytk9s+H34Hrn6mJZ/ + /MRZOZimld2tdshUdEQ+Kaf4wVLcEwJuW5/NeMbba+iIA/sPqHRkYmVs1v8= + -----END RSA PRIVATE KEY----- + version: v1 diff --git a/doc/architecture/decisions/0010-cookies.md b/doc/architecture/decisions/0010-cookies.md index c4b4998f53..276a98fa82 100644 --- a/doc/architecture/decisions/0010-cookies.md +++ b/doc/architecture/decisions/0010-cookies.md @@ -1,8 +1,8 @@ # 10. Cookies -## Context +## Context -We should be clear about what we store in the session cookie and why. +We should be clear about what we store in the session cookie and why. We store a number of properties in the `session` cookie: @@ -17,14 +17,14 @@ We store a number of properties in the `session` cookie: | account_service_url | The link back to the launch service. Used on the signed out and session expired pages. From metadata claims (`account_service_url`) | | account_service_log_out_url | The The URL to redirect to on signout. From metadata claims (`account_service_log_out_url`) | -- The CSRF token changes per request, meaning that the session cookie is set for most requests. +- The CSRF token changes per request, meaning that the session cookie is set for most requests. -- Language is accessed from the server-side session via the Babel `get_locale` method. This means it's only available if the user has a valid session; when the session has timed out, screens are not in the appropriate language. +- Language is accessed from the server-side session via the Babel `get_locale` method. This means it's only available if the user has a valid session; when the session has timed out, screens are not in the appropriate language. ## Decision -- Continue using the session cookie for properties that are set on sign-in as they are relevant to the current session. Storing them in the cookie rather than the server-side session means that they are still accessible after the server-side session times out. -- Store `schema_name` or `survey_url` in the cookie and remove schema properties i.e. `theme`, `survey_title` and `expires_in`. This simplifies runner code as we will always be able to load a schema for any request (if they have successfully launched a questionnaire), and it provides a way to use other schema properties without adding to the cookie. +- Continue using the session cookie for properties that are set on sign-in as they are relevant to the current session. Storing them in the cookie rather than the server-side session means that they are still accessible after the server-side session times out. +- Store `schema_name` or `schema_url` in the cookie and remove schema properties i.e. `theme`, `survey_title` and `expires_in`. This simplifies runner code as we will always be able to load a schema for any request (if they have successfully launched a questionnaire), and it provides a way to use other schema properties without adding to the cookie. - The session cookie will contain: | Property | Description | @@ -34,15 +34,15 @@ We store a number of properties in the `session` cookie: | csrf_token | The CSRF token (generated for each request) | | account_service_url | The link back to the launch service | | account_service_log_out_url | The The URL to redirect to on signout | - | schema_name | The schema filename | - | survey_url | A URL to a schema | + | schema_name | The schema filename | + | schema_url | A URL to a schema | - The `session` cookie should be set once on successful authentication. To enable this, we will review our CSRF token implementation - one per session or a separate cookie. - Set `language` in a cookie, so it’s not lost as a preference when there is no server-side session. This will be stored in a separate cookie as the user can change it. - Any future user preferences that need to be remembered will be stored in separate named cookies. - + ## Additional information - An `ons_cookie_policy` cookie is set in the front end and is used to enable/disable Google tracking. diff --git a/doc/profiling.md b/doc/profiling.md index a8cf2e7c7a..3e556dde4e 100644 --- a/doc/profiling.md +++ b/doc/profiling.md @@ -37,7 +37,7 @@ The profiles can also be combined to give an overview of the profile between all Combine all the profiles in the `profiling` directory using: ```bash -pipenv run python scripts/merge_profiles.py +poetry run python scripts/merge_profiles.py ``` This will create a file called `combined_profile.prof` diff --git a/doc/python-type-hinting.md b/doc/python-type-hinting.md index c293c20f86..6b8b559b9c 100644 --- a/doc/python-type-hinting.md +++ b/doc/python-type-hinting.md @@ -1,13 +1,14 @@ # Python Type Hinting -As a team we have committed to adding type hints throughout the Python code. This document defines our approach to type hinting, to ensure consistency in the codebase and avoid repeating discussions about how we apply typing. +As a team we have committed to adding type hints throughout the Python code. This document defines our approach to type hinting, to ensure consistency in the +codebase and avoid repeating discussions about how we apply typing. ## Variables Specify types for variables initialised with `None`: ```python -description: Optional[str] = None +description: var: str | None = None ``` Specify types for empty collections: @@ -17,8 +18,6 @@ items: list[str] = [] mappings: dict[str, int] = {} ``` -In all other places, types for variables are optional. - ## Standard collections For standard collections, use lower case names e.g `list` rather than `List`: @@ -32,26 +31,56 @@ https://www.python.org/dev/peps/pep-0585/ ## Generic types -Specify at least the top-level type parameters for all generic types: +Specify at least the top-level type parameters for generic types: ```python def get_objects_matching(ids: Sequence[str]) -> dict[int, dict] ``` -## Optional arguments +If the typing used for a generic type would be Any or indeterministic, do not specify it: + +```python +items: list[Any] = ["demo", 2, true] # Incorrect +items: list[str | int | bool] = ["demo", 2, true] # Incorrect +items: list = ["demo", 2, true] # Correct +``` + +This same ruling applies for key-val types such as Mapping. + +If the key type and value types are known, they may be specified: + +```python +known_types_dict: dict[str, str] = {"name": "demo"} +``` -When `Optional` is used for function arguments, it means that the argument can have the value `None`, not that the argument is optional. +If the key type is known, and the value types are deterministic, use TypedDict: + +```python +from typing import TypedDict + +class Movie(TypedDict): + name: str + year: int +``` + +If the key type is known but the value types are indeterministic or the key type is not known, do not declare the types: + +```python +json_data: dict = json.loads(stringified_json) +``` + +## Optional arguments -For arguments that can be a single type or `None`, use `Optional`: +For arguments that can be a single type or None, use the shorthand `|` instead of using the older `Optional` keyword: ```python -def test(self, var: Optional[int]) -> None: +def test(self, var: None | int) -> None: ``` -For arguments that can be one of multiple types or `None`, use `Union` with `None`: +For arguments that can be one of multiple types or None, use the shorthand `|` instead of using the older `Union` keyword: ```python -def test(self, var: Union[None, int, str]) -> None: +def test(self, var: None | int | str) -> None: ``` ## Abstract vs concrete @@ -60,28 +89,154 @@ def test(self, var: Union[None, int, str]) -> None: - Make return types as specific as possible (to be predictable to callers) ```python - def increment_values(self, values: Sequence[int]) -> list[int]: - return [value + 1 for value in values] +def increment_values(self, values: Sequence[int]) -> list[int]: + return [value + 1 for value in values] +``` + +## Self Type + +To annotate methods that return an instance of their class, use the `Self` type is as it is bound to it's encapsulating class. In the example below, the type +checker will correctly infer the type of `Circle().set_scale(0.5)` to be `Circle`: + +```python +from typing import Self + + +class Shape: + def set_scale(self, scale: float) -> Self: + self.scale = scale + return self + + +class Circle(Shape): + def set_radius(self, radius: float) -> Self: + self.radius = radius + return self +``` + +This is recommended as forward declarations are now redundant in 3.10 +https://peps.python.org/pep-0673/ + +## Type Alias + +Use the special annotation `TypeAlias` to declare type aliases more explicitly so type checkers are able to distinguish between type aliases and ordinary +assignments: + +```python +MyType: TypeAlias = "ClassName" + + +def foo() -> MyType: ... +``` + +## Type Ignore + +To mark portions of the program that should not be covered by type hinting, use `# type: ignore` on the specific line. When used in a line by itself at the top +of a file, it silences all errors in the file: + +```python +# type: ignore +``` + +`# type: ignore` should only be used when unavoidable (or in the return `Any` case detailed below). Ensure that a comment is added to explain why it has been used and have a prefix of `Type ignore:` + +```python +def format_number(number: int) -> str: + # Type ignore: babel.format_number is untyped therefore returns Any. + formatted_number: str = babel.format_number(number) # type: ignore + return formatted_number +``` + +The `warn_return_any` flag is turned on to force type hinting the return types for third party libraries and increase the safety of the code base. + +Where type hints aren’t specific enough to identify the return type (e.g. objects like blocks where some keys correspond to strings, others to lists, others to dicts) mypy will complain if you assume the type of any attribute: + +```python +def get_id_from_block(block: dict) -> str: + return block["id"] # Returning Any from function declared to return "str" +``` + +A type ignore can be avoided here, by changing the code to this... + +```python +def get_id_from_block(block: dict) -> str: + block_id: str = block["id"] + return block_id +``` + +...but as this is a common pattern in a number of places, it results in a lot of duplicating the return type, and extra lines of code for the sake of type hinting. In this scenario, it is ok to type ignore it. + +If the value was needed for any other checks e.g. + +```python +def get_first_answer_from_block(block: dict) -> str: + answer = ... + if answer["id"] ... : + ... + return answer ``` -## Forward declarations +This would not be suitable to type ignore, and it should use the existing convention of typing the unknown variable: -To reference a type before it has been declared e.g. using a class as a type within the class declaration, add the special `annotations` import: +```python +def get_first_answer_from_block(block: dict) -> str: + answer: Answer = ... + if answer["id"] ... : + ... + return answer +``` + +## ParamSpec + +Use to forward the parameter types of one callable to another callable. + +E.g. a basic logging function decorator: + +```python +T = TypeVar('T') +P = ParamSpec('P') + + +def add_logging(f: Callable[P, T]) -> Callable[P, T]: + '''A type-safe decorator to add logging to a function.''' + + def inner(*args: P.args, **kwargs: P.kwargs) -> T: + logging.info(f'{f.__name__} was called') + return f(*args, **kwargs) + + return inner + + +@add_logging +def add_two(x: float, y: float) -> float: + '''Add two numbers together.''' + return x + y +``` + +## TypeVar + +Use `TypeVar` when the type returned by a function is the same as the type which was passed in: ```python -from __future__ import annotations +T = TypeVar('T') -class TestClass: - def test(self, var: Sequence[TestClass]) -> None: +def increment_value(self, value: T) -> T: + return value + 1 ``` -This import is not necessary in Python 3.10. +`TypeVar` also accepts extra positional arguments to restrict the type parameter for improving code documentation and error prevention. + +```python +T = TypeVar('T', int, float) -https://www.python.org/dev/peps/pep-0563/ + +def increment_value(self, value: T) -> T: + return value + 1 +``` ## Useful links - https://www.python.org/dev/peps/pep-0484/ - https://www.pythonsheets.com/notes/python-typing.html -- https://google.github.io/styleguide/pyguide.html#319-type-annotations \ No newline at end of file +- https://google.github.io/styleguide/pyguide.html#319-type-annotations diff --git a/docker-compose-dev-mac.yml b/docker-compose-dev-mac.yml deleted file mode 100644 index f36a807109..0000000000 --- a/docker-compose-dev-mac.yml +++ /dev/null @@ -1,21 +0,0 @@ -version: "3" - -services: - datastore: - image: knarz/datastore-emulator - ports: - - "8432:8432" - - redis: - image: redis:4 - ports: - - "6379:6379" - - eq-questionnaire-launcher: - image: "onsdigital/eq-questionnaire-launcher:latest" - environment: - SURVEY_RUNNER_URL: http://localhost:5000 - SURVEY_RUNNER_SCHEMA_URL: http://docker.for.mac.host.internal:5000 - restart: always - ports: - - "8000:8000" diff --git a/docker-compose-dev-linux.yml b/docker-compose-dev.yml similarity index 58% rename from docker-compose-dev-linux.yml rename to docker-compose-dev.yml index 7c5fbe10ca..eb96380f47 100644 --- a/docker-compose-dev-linux.yml +++ b/docker-compose-dev.yml @@ -1,5 +1,3 @@ -version: "3" - services: datastore: image: knarz/datastore-emulator @@ -20,12 +18,30 @@ services: environment: SURVEY_RUNNER_URL: http://localhost:5000 SURVEY_RUNNER_SCHEMA_URL: http://host.docker.internal:5000 + SDS_API_BASE_URL: http://host.docker.internal:5003 + CIR_API_BASE_URL: http://host.docker.internal:5004 networks: - eq-env restart: always ports: - "8000:8000" + sds: + image: onsdigital/eq-runner-mock-sds:latest + networks: + - eq-env + restart: always + ports: + - "5003:5003" + + cir: + image: onsdigital/eq-runner-mock-cir:latest + networks: + - eq-env + restart: always + ports: + - "5004:5004" + networks: eq-env: driver: bridge diff --git a/docker-compose-schema-validator.yml b/docker-compose-schema-validator.yml index 7055c79a43..9bd00b8f06 100644 --- a/docker-compose-schema-validator.yml +++ b/docker-compose-schema-validator.yml @@ -1,6 +1,3 @@ ---- -version: '3' - services: ajv-validator: image: onsdigital/eq-questionnaire-validator:${TAG}-ajv @@ -14,7 +11,7 @@ services: networks: - eq-schema environment: - AJV_HOST: ajv-validator + AJV_VALIDATOR_HOST: ajv-validator ports: - 5001:5000 depends_on: diff --git a/docker-compose.yml b/docker-compose.yml index 0b2f70a8d4..fd9839314b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,48 +1,65 @@ -version: '3' - services: - datastore: - image: knarz/datastore-emulator - networks: - - eq-env - - redis: - image: redis:4 - networks: - - eq-env - - eq-questionnaire-runner: - image: onsdigital/eq-questionnaire-runner:latest - build: ./ - env_file: - - ${RUNNER_ENV_FILE} - environment: - DATASTORE_EMULATOR_HOST: datastore:8432 - EQ_REDIS_HOST: "redis" - restart: always - depends_on: - - datastore - - redis - networks: - - eq-env - ports: - - "5000:5000" - - eq-questionnaire-launcher: - image: onsdigital/eq-questionnaire-launcher:latest - environment: - SURVEY_RUNNER_URL: http://localhost:5000 - SURVEY_RUNNER_SCHEMA_URL: http://eq-questionnaire-runner:5000 - restart: always - depends_on: - - eq-questionnaire-runner - networks: - - eq-env - ports: - - "8000:8000" + datastore: + image: knarz/datastore-emulator + networks: + - eq-env + + redis: + image: redis:4 + networks: + - eq-env + + eq-questionnaire-runner: + image: onsdigital/eq-questionnaire-runner:latest + build: ./ + env_file: + - ${RUNNER_ENV_FILE} + environment: + DATASTORE_EMULATOR_HOST: datastore:8432 + EQ_REDIS_HOST: "redis" + SDS_API_BASE_URL: http://sds:5003 + CIR_API_BASE_URL: http://cir:5004 + restart: always + depends_on: + - datastore + - redis + networks: + - eq-env + ports: + - "5000:5000" + + eq-questionnaire-launcher: + image: onsdigital/eq-questionnaire-launcher:latest + environment: + SURVEY_RUNNER_URL: http://localhost:5000 + SURVEY_RUNNER_SCHEMA_URL: http://eq-questionnaire-runner:5000 + SDS_API_BASE_URL: http://sds:5003 + restart: always + depends_on: + - eq-questionnaire-runner + networks: + - eq-env + ports: + - "8000:8000" + + sds: + image: onsdigital/eq-runner-mock-sds:latest + networks: + - eq-env + restart: always + ports: + - "5003:5003" + + cir: + image: onsdigital/eq-runner-mock-cir:latest + networks: + - eq-env + restart: always + ports: + - "5004:5004" networks: - eq-env: - driver: bridge + eq-env: + driver: bridge diff --git a/eq_questionnaire_runner/__init__.py b/eq_questionnaire_runner/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/mypy.ini b/mypy.ini index 5271cedcfb..dc4f23c263 100644 --- a/mypy.ini +++ b/mypy.ini @@ -3,60 +3,36 @@ [mypy] ignore_missing_imports = True warn_no_return = False +warn_unused_ignores = True # Per-module options: -[mypy-app.data_models.*] -disallow_untyped_defs = True -warn_return_any = True -no_implicit_optional = True - -[mypy-app.helpers.*] -disallow_untyped_defs = True -warn_return_any = True -no_implicit_optional = True - -[mypy-app.questionnaire.placeholder_parser] -disallow_untyped_defs = True -warn_return_any = True -no_implicit_optional = True - -[mypy-app.questionnaire.placeholder_renderer] -disallow_untyped_defs = True -warn_return_any = True -no_implicit_optional = True - -[mypy-app.questionnaire.placeholder_transforms] -disallow_untyped_defs = True -warn_return_any = True -no_implicit_optional = True - -[mypy-app.questionnaire.questionnaire_schema] +[mypy-app.views.handlers.*] disallow_untyped_defs = True warn_return_any = True no_implicit_optional = True -[mypy-app.views.handlers.submit] +[mypy-app.data_models.*] disallow_untyped_defs = True warn_return_any = True no_implicit_optional = True -[mypy-app.views.handlers.view_submitted_response_context] +[mypy-app.helpers.*] disallow_untyped_defs = True warn_return_any = True no_implicit_optional = True -[mypy-app.views.contexts.submit_context] +[mypy-app.questionnaire.*] disallow_untyped_defs = True warn_return_any = True no_implicit_optional = True -[mypy-app.views.contexts.view_submitted_response_context] +[mypy-app.views.contexts.*] disallow_untyped_defs = True warn_return_any = True no_implicit_optional = True -[mypy-app.views.contexts.view_summary_context] +[mypy-app.views.contexts.summary.*] disallow_untyped_defs = True warn_return_any = True no_implicit_optional = True @@ -66,22 +42,12 @@ disallow_untyped_defs = True warn_return_any = True no_implicit_optional = True -[mypy-app.questionnaire.value_source_resolver] -disallow_untyped_defs = True -warn_return_any = True -no_implicit_optional = True - [mypy-app.libs.utils] disallow_untyped_defs = True warn_return_any = True no_implicit_optional = True -[mypy-app.questionnaire.routing.*] -disallow_untyped_defs = True -warn_return_any = True -no_implicit_optional = True - -[mypy-app.forms.field_handlers.*] +[mypy-app.forms.*] disallow_untyped_defs = True warn_return_any = True no_implicit_optional = True @@ -111,37 +77,42 @@ disallow_untyped_defs = True warn_return_any = True no_implicit_optional = True -[mypy-app.views.handlers.view_submitted_response] +[mypy-app.submitter.*] disallow_untyped_defs = True warn_return_any = True no_implicit_optional = True -[mypy-app.views.handlers.pdf] +[mypy-app.storage.*] disallow_untyped_defs = True warn_return_any = True no_implicit_optional = True -[mypy-app.views.handlers.feedback] +[mypy-app.secrets] disallow_untyped_defs = True warn_return_any = True no_implicit_optional = True -[mypy-app.submitter.*] +[mypy-app.jinja_filters] disallow_untyped_defs = True warn_return_any = True no_implicit_optional = True -[mypy-app.storage.*] +[mypy-app.routes.*] +disallow_untyped_defs = True +warn_return_any = True +no_implicit_optional = True + +[mypy-app.utilities.*] disallow_untyped_defs = True warn_return_any = True no_implicit_optional = True -[mypy-app.questionnaire.router] +[mypy-app.publisher.*] disallow_untyped_defs = True warn_return_any = True no_implicit_optional = True -[mypy-app.questionnaire.dynamic_answer_options] +[mypy-scripts.generate_integration_test] disallow_untyped_defs = True warn_return_any = True no_implicit_optional = True diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000000..80b026618f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,13035 @@ +{ + "name": "eq-questionnaire-runner", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "eq-questionnaire-runner", + "version": "1.0.0", + "devDependencies": { + "@babel/core": "^7.25.8", + "@babel/plugin-transform-runtime": "^7.25.7", + "@babel/preset-env": "^7.25.8", + "@babel/register": "^7.25.7", + "@babel/runtime": "^7.25.7", + "@wdio/cli": "^9.2.1", + "@wdio/local-runner": "^8.14.3", + "@wdio/mocha-framework": "^9.1.3", + "@wdio/spec-reporter": "^9.1.3", + "eslint": "^v8.57.1", + "eslint-cli": "^1.1.1", + "eslint-config-standard": "^17.1.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-json": "^4.0.1", + "eslint-plugin-n": "^16.6.2", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^6.6.0", + "json-web-key": "^0.4.0", + "jsrsasign": "^11.0.0", + "livereload": "^0.9.3", + "node-forge": "^1.3.1", + "node-jose": "^2.2.0", + "prettier": "^3.3.3", + "typescript": "^5.6.3", + "uuid": "^11.0.2", + "webdriverio": "^9.2.1" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", + "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.10", + "@babel/types": "^7.26.10", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", + "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", + "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/traverse": "^7.25.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.9.tgz", + "integrity": "sha512-ORPNZ3h6ZRkOyAa/SaHU+XsLZr0UQzRwuDQ0cczIA17nAzZ+85G5cVkOJIj7QavLZGSe8QXUmNFxSZzjcZF9bw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "regexpu-core": "^6.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.4.tgz", + "integrity": "sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", + "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", + "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", + "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-wrap-function": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz", + "integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==", + "dev": true, + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", + "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", + "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", + "dev": true, + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz", + "integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz", + "integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", + "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz", + "integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", + "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", + "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.26.8.tgz", + "integrity": "sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/helper-remap-async-to-generator": "^7.25.9", + "@babel/traverse": "^7.26.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", + "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-remap-async-to-generator": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz", + "integrity": "sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz", + "integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz", + "integrity": "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz", + "integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", + "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/traverse": "^7.25.9", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", + "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/template": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", + "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz", + "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz", + "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz", + "integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz", + "integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz", + "integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.26.9.tgz", + "integrity": "sha512-Hry8AusVm8LW5BVFgiyUReuoGzPUpdHQQqJY5bZnbbf+ngOHWuCuYFKw/BqaaWlvEUrF91HMhDtEaI1hZzNbLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", + "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz", + "integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", + "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz", + "integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", + "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz", + "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz", + "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", + "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", + "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz", + "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.26.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.26.6.tgz", + "integrity": "sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz", + "integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz", + "integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/plugin-transform-parameters": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", + "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz", + "integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", + "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz", + "integrity": "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz", + "integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", + "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz", + "integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz", + "integrity": "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", + "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.26.10.tgz", + "integrity": "sha512-NWaL2qG6HRpONTnj4JvDU6th4jYeZOJgu3QhmFTCihib0ermtOJqktA5BduGm3suhhVe9EMP9c9+mfJ/I9slqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.26.5", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.11.0", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", + "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", + "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz", + "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.26.8.tgz", + "integrity": "sha512-OmGDL5/J0CJPJZTHZbi2XpO0tyT2Ia7fzpW5GURwdtp2X3fMmN8au/ej6peC/T33/+CRiIpA8Krse8hFGVmT5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.26.7.tgz", + "integrity": "sha512-jfoTXXZTgGg36BmhqT3cAYK5qkmqvJpvNrPhaK/52Vgjhw4Rq29s9UqpWWV0D6yuRmgiFH/BUVlkl96zJWqnaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz", + "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz", + "integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz", + "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz", + "integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.9.tgz", + "integrity": "sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.26.8", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.26.0", + "@babel/plugin-syntax-import-attributes": "^7.26.0", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.25.9", + "@babel/plugin-transform-async-generator-functions": "^7.26.8", + "@babel/plugin-transform-async-to-generator": "^7.25.9", + "@babel/plugin-transform-block-scoped-functions": "^7.26.5", + "@babel/plugin-transform-block-scoping": "^7.25.9", + "@babel/plugin-transform-class-properties": "^7.25.9", + "@babel/plugin-transform-class-static-block": "^7.26.0", + "@babel/plugin-transform-classes": "^7.25.9", + "@babel/plugin-transform-computed-properties": "^7.25.9", + "@babel/plugin-transform-destructuring": "^7.25.9", + "@babel/plugin-transform-dotall-regex": "^7.25.9", + "@babel/plugin-transform-duplicate-keys": "^7.25.9", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-dynamic-import": "^7.25.9", + "@babel/plugin-transform-exponentiation-operator": "^7.26.3", + "@babel/plugin-transform-export-namespace-from": "^7.25.9", + "@babel/plugin-transform-for-of": "^7.26.9", + "@babel/plugin-transform-function-name": "^7.25.9", + "@babel/plugin-transform-json-strings": "^7.25.9", + "@babel/plugin-transform-literals": "^7.25.9", + "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", + "@babel/plugin-transform-member-expression-literals": "^7.25.9", + "@babel/plugin-transform-modules-amd": "^7.25.9", + "@babel/plugin-transform-modules-commonjs": "^7.26.3", + "@babel/plugin-transform-modules-systemjs": "^7.25.9", + "@babel/plugin-transform-modules-umd": "^7.25.9", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-new-target": "^7.25.9", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.26.6", + "@babel/plugin-transform-numeric-separator": "^7.25.9", + "@babel/plugin-transform-object-rest-spread": "^7.25.9", + "@babel/plugin-transform-object-super": "^7.25.9", + "@babel/plugin-transform-optional-catch-binding": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9", + "@babel/plugin-transform-parameters": "^7.25.9", + "@babel/plugin-transform-private-methods": "^7.25.9", + "@babel/plugin-transform-private-property-in-object": "^7.25.9", + "@babel/plugin-transform-property-literals": "^7.25.9", + "@babel/plugin-transform-regenerator": "^7.25.9", + "@babel/plugin-transform-regexp-modifiers": "^7.26.0", + "@babel/plugin-transform-reserved-words": "^7.25.9", + "@babel/plugin-transform-shorthand-properties": "^7.25.9", + "@babel/plugin-transform-spread": "^7.25.9", + "@babel/plugin-transform-sticky-regex": "^7.25.9", + "@babel/plugin-transform-template-literals": "^7.26.8", + "@babel/plugin-transform-typeof-symbol": "^7.26.7", + "@babel/plugin-transform-unicode-escapes": "^7.25.9", + "@babel/plugin-transform-unicode-property-regex": "^7.25.9", + "@babel/plugin-transform-unicode-regex": "^7.25.9", + "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.11.0", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.40.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/register": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.25.9.tgz", + "integrity": "sha512-8D43jXtGsYmEeDvm4MWHYUpWf8iiXgWYx3fW7E7Wb7Oe6FWqJPl5K6TuFW0dOwNZzEE5rjlaSJYH9JjrUKJszA==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "find-cache-dir": "^2.0.0", + "make-dir": "^2.1.0", + "pirates": "^4.0.6", + "source-map-support": "^0.5.16" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", + "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.27.0", + "@babel/parser": "^7.27.0", + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", + "integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz", + "integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz", + "integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz", + "integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz", + "integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz", + "integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz", + "integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz", + "integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz", + "integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz", + "integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz", + "integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz", + "integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz", + "integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz", + "integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz", + "integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz", + "integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz", + "integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz", + "integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz", + "integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz", + "integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz", + "integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz", + "integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz", + "integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz", + "integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz", + "integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true + }, + "node_modules/@inquirer/checkbox": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-3.0.1.tgz", + "integrity": "sha512-0hm2nrToWUdD6/UHnel/UKGdk1//ke5zGUpHIvk5ZWmaKezlGxZkOJXNSWsdxO/rEqTkbB3lNC2J6nBElV2aAQ==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/confirm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-4.0.1.tgz", + "integrity": "sha512-46yL28o2NJ9doViqOy0VDcoTzng7rAb6yPQKU7VDLqkmbCaH4JqK4yk4XqlzNWy9PVC5pG1ZUXPBQv+VqnYs2w==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.2.1.tgz", + "integrity": "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==", + "dev": true, + "dependencies": { + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "@types/mute-stream": "^0.0.4", + "@types/node": "^22.5.5", + "@types/wrap-ansi": "^3.0.0", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^1.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core/node_modules/@types/node": { + "version": "22.7.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.6.tgz", + "integrity": "sha512-/d7Rnj0/ExXDMcioS78/kf1lMzYk4BZV8MZGTBKzTGZ6/406ukkbYlIsZmMPhcR5KlkunDHQLrtAVmSq7r+mSw==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@inquirer/core/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/editor": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-3.0.1.tgz", + "integrity": "sha512-VA96GPFaSOVudjKFraokEEmUQg/Lub6OXvbIEZU1SDCmBzRkHGhxoFAVaF30nyiB4m5cEbDgiI2QRacXZ2hw9Q==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/expand": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-3.0.1.tgz", + "integrity": "sha512-ToG8d6RIbnVpbdPdiN7BCxZGiHOTomOX94C2FaT5KOHupV40tKEDozp12res6cMIfRKrXLJyexAZhWVHgbALSQ==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.7.tgz", + "integrity": "sha512-m+Trk77mp54Zma6xLkLuY+mvanPxlE4A7yNKs2HBiyZ4UkVs28Mv5c/pgWrHeInx+USHeX/WEPzjrWrcJiQgjw==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-3.0.1.tgz", + "integrity": "sha512-BDuPBmpvi8eMCxqC5iacloWqv+5tQSJlUafYWUe31ow1BVXjW2a5qe3dh4X/Z25Wp22RwvcaLCc2siHobEOfzg==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/number": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-2.0.1.tgz", + "integrity": "sha512-QpR8jPhRjSmlr/mD2cw3IR8HRO7lSVOnqUvQa8scv1Lsr3xoAMMworcYW3J13z3ppjBFBD2ef1Ci6AE5Qn8goQ==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/password": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-3.0.1.tgz", + "integrity": "sha512-haoeEPUisD1NeE2IanLOiFr4wcTXGWrBOyAyPZi1FfLJuXOzNmxCJPgUrGYKVh+Y8hfGJenIfz5Wb/DkE9KkMQ==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/prompts": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-6.0.1.tgz", + "integrity": "sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A==", + "dev": true, + "dependencies": { + "@inquirer/checkbox": "^3.0.1", + "@inquirer/confirm": "^4.0.1", + "@inquirer/editor": "^3.0.1", + "@inquirer/expand": "^3.0.1", + "@inquirer/input": "^3.0.1", + "@inquirer/number": "^2.0.1", + "@inquirer/password": "^3.0.1", + "@inquirer/rawlist": "^3.0.1", + "@inquirer/search": "^2.0.1", + "@inquirer/select": "^3.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/rawlist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-3.0.1.tgz", + "integrity": "sha512-VgRtFIwZInUzTiPLSfDXK5jLrnpkuSOh1ctfaoygKAdPqjcjKYmGh6sCY1pb0aGnCGsmhUxoqLDUAU0ud+lGXQ==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/type": "^2.0.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/search": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-2.0.1.tgz", + "integrity": "sha512-r5hBKZk3g5MkIzLVoSgE4evypGqtOannnB3PKTG9NRZxyFRKcfzrdxXXPcoJQsxJPzvdSU2Rn7pB7lw0GCmGAg==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/select": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-3.0.1.tgz", + "integrity": "sha512-lUDGUxPhdWMkN/fHy1Lk7pF3nK1fh/gqeyWXmctefhxLYxlDsc7vsPBEpxrfVGDsVdyYJsiJoD4bJ1b623cV1Q==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-2.0.0.tgz", + "integrity": "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==", + "dev": true, + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/types/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@jest/types/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@promptbook/utils": { + "version": "0.70.0-1", + "resolved": "https://registry.npmjs.org/@promptbook/utils/-/utils-0.70.0-1.tgz", + "integrity": "sha512-qd2lLRRN+sE6UuNMi2tEeUUeb4zmXnxY5EMdfHVXNE+bqBDpUC7/aEfXgA3jnUXEr+xFjQ8PTFQgWvBMaKvw0g==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://buymeacoffee.com/hejny" + }, + { + "type": "github", + "url": "https://github.com/webgptorg/promptbook/blob/main/README.md#%EF%B8%8F-contributing" + } + ], + "dependencies": { + "spacetrim": "0.11.39" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.4.0.tgz", + "integrity": "sha512-x8J1csfIygOwf6D6qUAZ0ASk3z63zPb7wkNeHRerCMh82qWKUrOgkuP005AJC8lDL6/evtXETGEJVcwykKT4/g==", + "dev": true, + "dependencies": { + "debug": "^4.3.6", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@sindresorhus/is": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", + "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", + "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.1" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@types/mocha": { + "version": "10.0.9", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.9.tgz", + "integrity": "sha512-sicdRoWtYevwxjOHNMPTl3vSfJM6oyW8o1wXeI7uww6b6xHg8eBznQDNSGBCDJmsE8UMxP05JgZRtsKbTqt//Q==", + "dev": true + }, + "node_modules/@types/mute-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", + "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "20.16.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.12.tgz", + "integrity": "sha512-LfPFB0zOeCeCNQV3i+67rcoVvoN5n0NVuR2vLG0O5ySQMgchuZlC4lgz546ZOJyDtj5KIgOxy+lacOimfqZAIA==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "node_modules/@types/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/which/-/which-2.0.2.tgz", + "integrity": "sha512-113D3mDkZDjo+EeUEHCFy0qniNc1ZpecGiAU7WSo7YDoSzolZIQKpYFHrPpjkB2nuyahcKfrmLXeQlh7gqJYdw==", + "dev": true + }, + "node_modules/@types/wrap-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", + "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", + "dev": true + }, + "node_modules/@types/ws": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", + "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.3.tgz", + "integrity": "sha512-XH1XdtoLZCpqV59KRbPrIhFCOO0hErxrQCMcvnQete3Vibb9UeIOX02uFPfVn3Z9ZXsq78etlfyhnkmIZSzIwQ==", + "dev": true, + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.3.tgz", + "integrity": "sha512-qWC2mWc7VAXmjAkEKxrScWHWFyCQx/cmiZtuGqMi+WwqQJ2iURsVY4ZfAK6dVo6K2smKRU6l3BPwqEBvhnpQGg==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "2.1.3", + "magic-string": "^0.30.11", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@wdio/cli": { + "version": "9.12.1", + "resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-9.12.1.tgz", + "integrity": "sha512-NsY/c27SwXFm80fgkMfMVginrJ032vN9HtOSZpcbM3RXf3dzsvlhPYFTXJhmB8Iu6h4Yy9ejKLgZgZ/VS1fC+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.1", + "@vitest/snapshot": "^2.1.1", + "@wdio/config": "9.12.1", + "@wdio/globals": "9.12.1", + "@wdio/logger": "9.4.4", + "@wdio/protocols": "9.7.0", + "@wdio/types": "9.10.1", + "@wdio/utils": "9.12.1", + "async-exit-hook": "^2.0.1", + "chalk": "^5.2.0", + "chokidar": "^4.0.0", + "dotenv": "^16.3.1", + "ejs": "^3.1.9", + "execa": "^9.2.0", + "import-meta-resolve": "^4.0.0", + "inquirer": "^11.0.1", + "lodash.flattendeep": "^4.4.0", + "lodash.pickby": "^4.6.0", + "lodash.union": "^4.6.0", + "read-pkg-up": "^10.0.0", + "recursive-readdir": "^2.2.3", + "tsx": "^4.7.2", + "webdriverio": "9.12.1", + "yargs": "^17.7.2" + }, + "bin": { + "wdio": "bin/wdio.js" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/cli/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@wdio/config": { + "version": "9.12.1", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-9.12.1.tgz", + "integrity": "sha512-eYyF9HBQg2PyX6ScieZ5akDG4BaJmNBdYFJmwhUAGcJlxLgoI02vSqIuoWaQd5shbvtCdDzsFI0Jt8+S/xqINQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@wdio/logger": "9.4.4", + "@wdio/types": "9.10.1", + "@wdio/utils": "9.12.1", + "deepmerge-ts": "^7.0.3", + "glob": "^10.2.2", + "import-meta-resolve": "^4.0.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/globals": { + "version": "9.12.1", + "resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-9.12.1.tgz", + "integrity": "sha512-vZ48HnN3MpXcqxmmMtDUExBiHWhqQdRDm7m/j3mDycK/9GUVc+AuSEyFn6P6utsxoaVEqeRO2HZLPbAe54GaTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.20.0" + }, + "optionalDependencies": { + "expect-webdriverio": "^5.1.0", + "webdriverio": "9.12.1" + } + }, + "node_modules/@wdio/local-runner": { + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@wdio/local-runner/-/local-runner-8.43.0.tgz", + "integrity": "sha512-FEIl0wvKkn2rfTE+HT0t1O/aOUrIMHjf8zgz4N0m8PFwWEyi/WmM+jkAzruXDM9RJOT57Awv1gTdjQipbVw1Qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^22.2.0", + "@wdio/logger": "8.38.0", + "@wdio/repl": "8.40.3", + "@wdio/runner": "8.43.0", + "@wdio/types": "8.41.0", + "async-exit-hook": "^2.0.1", + "split2": "^4.1.0", + "stream-buffers": "^3.0.2" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/local-runner/node_modules/@types/node": { + "version": "22.10.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.10.tgz", + "integrity": "sha512-X47y/mPNzxviAGY5TcYPtYL8JsY3kAq2n8fMmKoRCxq/c4v4pyGNCzM2R6+M5/umG4ZfHuT+sgqDYqWc9rJ6ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@wdio/local-runner/node_modules/@wdio/logger": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-8.38.0.tgz", + "integrity": "sha512-kcHL86RmNbcQP+Gq/vQUGlArfU6IIcbbnNp32rRIraitomZow+iEoc519rdQmSVusDozMS5DZthkgDdxK+vz6Q==", + "dev": true, + "dependencies": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/local-runner/node_modules/@wdio/types": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.41.0.tgz", + "integrity": "sha512-t4NaNTvJZci3Xv/yUZPH4eTL0hxrVTf5wdwNnYIBrzMnlRDbNefjQ0P7FM7ZjQCLaH92AEH6t/XanUId7Webug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^22.2.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/local-runner/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@wdio/local-runner/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@wdio/logger": { + "version": "9.4.4", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.4.4.tgz", + "integrity": "sha512-BXx8RXFUW2M4dcO6t5Le95Hi2ZkTQBRsvBQqLekT2rZ6Xmw8ZKZBPf0FptnoftFGg6dYmwnDidYv/0+4PiHjpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/logger/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@wdio/mocha-framework": { + "version": "9.12.1", + "resolved": "https://registry.npmjs.org/@wdio/mocha-framework/-/mocha-framework-9.12.1.tgz", + "integrity": "sha512-gWRpSizXyhFFE5MaHtyf/O51nVL08bEoAqrAtZ8ZDRMhmjnVY6KA64Shxw+ULoK1ta1dpxpIyrsiloyLaBTPuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mocha": "^10.0.6", + "@types/node": "^20.11.28", + "@wdio/logger": "9.4.4", + "@wdio/types": "9.10.1", + "@wdio/utils": "9.12.1", + "mocha": "^10.3.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/protocols": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-9.7.0.tgz", + "integrity": "sha512-5DI8cqJqT9K6oQn8UpaSTmcGAl4ufkUWC5FoPT3oXdLjILfxvweZDf/2XNBCbGMk4+VOMKqB2ofOqKhDIB2nAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@wdio/repl": { + "version": "8.40.3", + "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-8.40.3.tgz", + "integrity": "sha512-mWEiBbaC7CgxvSd2/ozpbZWebnRIc8KRu/J81Hlw/txUWio27S7IpXBlZGVvhEsNzq0+cuxB/8gDkkXvMPbesw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^22.2.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/repl/node_modules/@types/node": { + "version": "22.13.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.13.tgz", + "integrity": "sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@wdio/repl/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@wdio/reporter": { + "version": "9.11.0", + "resolved": "https://registry.npmjs.org/@wdio/reporter/-/reporter-9.11.0.tgz", + "integrity": "sha512-YTUlrTsJU2XT5olFOk+tsYm6DcUwmowk0us1gt5Me2zFI3UdUQpuUMfPEqFtayRsfcHwSaPGLKR7Eng+r3uXGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0", + "@wdio/logger": "9.4.4", + "@wdio/types": "9.10.1", + "diff": "^7.0.0", + "object-inspect": "^1.12.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/runner": { + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@wdio/runner/-/runner-8.43.0.tgz", + "integrity": "sha512-tL6v7hFBQNUy23douH1kvtg+kNdhtgh6qGX2PxTpr2d1ucu4p92umEkkJaFnsRjy2SQmDiV5oAvk/kPphWOHqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^22.2.0", + "@wdio/config": "8.43.0", + "@wdio/globals": "8.43.0", + "@wdio/logger": "8.38.0", + "@wdio/types": "8.41.0", + "@wdio/utils": "8.41.0", + "deepmerge-ts": "^5.1.0", + "expect-webdriverio": "^4.12.0", + "gaze": "^1.1.3", + "webdriver": "8.43.0", + "webdriverio": "8.43.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/runner/node_modules/@puppeteer/browsers": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.9.1.tgz", + "integrity": "sha512-PuvK6xZzGhKPvlx3fpfdM2kYY3P/hB1URtK8wA7XUJ6prn6pp22zvJHu48th0SGcHL9SutbPHrFuQgfXTFobWA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "4.3.4", + "extract-zip": "2.0.1", + "progress": "2.0.3", + "proxy-agent": "6.3.1", + "tar-fs": "3.0.4", + "unbzip2-stream": "1.4.3", + "yargs": "17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=16.3.0" + } + }, + "node_modules/@wdio/runner/node_modules/@types/node": { + "version": "22.13.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.13.tgz", + "integrity": "sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@wdio/runner/node_modules/@wdio/config": { + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-8.43.0.tgz", + "integrity": "sha512-mptlO5Lt8hV/8T9vRkp24G4o/kpI4qa9k4bRlOsI777MgeOlCEu2xLdRvVNGcg7RigDkx4jdYXX1pIHFg7gpSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@wdio/logger": "8.38.0", + "@wdio/types": "8.41.0", + "@wdio/utils": "8.41.0", + "decamelize": "^6.0.0", + "deepmerge-ts": "^5.0.0", + "glob": "^10.2.2", + "import-meta-resolve": "^4.0.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/runner/node_modules/@wdio/globals": { + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-8.43.0.tgz", + "integrity": "sha512-RCkldSApdoCAMsQhU7Qy9uXlFoPy8OFWOxbiE8O2WcRpTpSwAwWksN9tOLmevCTn1Ovj+onnxkiZQ0spnAUDbw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.13 || >=18" + }, + "optionalDependencies": { + "expect-webdriverio": "^4.11.2", + "webdriverio": "8.43.0" + } + }, + "node_modules/@wdio/runner/node_modules/@wdio/logger": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-8.38.0.tgz", + "integrity": "sha512-kcHL86RmNbcQP+Gq/vQUGlArfU6IIcbbnNp32rRIraitomZow+iEoc519rdQmSVusDozMS5DZthkgDdxK+vz6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/runner/node_modules/@wdio/protocols": { + "version": "8.40.3", + "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-8.40.3.tgz", + "integrity": "sha512-wK7+eyrB3TAei8RwbdkcyoNk2dPu+mduMBOdPJjp8jf/mavd15nIUXLID1zA+w5m1Qt1DsT1NbvaeO9+aJQ33A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@wdio/runner/node_modules/@wdio/types": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.41.0.tgz", + "integrity": "sha512-t4NaNTvJZci3Xv/yUZPH4eTL0hxrVTf5wdwNnYIBrzMnlRDbNefjQ0P7FM7ZjQCLaH92AEH6t/XanUId7Webug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^22.2.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/runner/node_modules/@wdio/utils": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.41.0.tgz", + "integrity": "sha512-0TcTjBiax1VxtJQ/iQA0ZyYOSHjjX2ARVmEI0AMo9+AuIq+xBfnY561+v8k9GqOMPKsiH/HrK3xwjx8xCVS03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@puppeteer/browsers": "^1.6.0", + "@wdio/logger": "8.38.0", + "@wdio/types": "8.41.0", + "decamelize": "^6.0.0", + "deepmerge-ts": "^5.1.0", + "edgedriver": "^5.5.0", + "geckodriver": "~4.2.0", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.1.0", + "safaridriver": "^0.1.0", + "split2": "^4.2.0", + "wait-port": "^1.0.4" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/@wdio/runner/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@wdio/runner/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@wdio/runner/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@wdio/runner/node_modules/deepmerge-ts": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-5.1.0.tgz", + "integrity": "sha512-eS8dRJOckyo9maw9Tu5O5RUi/4inFLrnoLkBe3cPfDMx3WZioXtmOew4TXQaxq7Rhl4xjDtR7c6x8nNTxOvbFw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@wdio/runner/node_modules/expect-webdriverio": { + "version": "4.15.4", + "resolved": "https://registry.npmjs.org/expect-webdriverio/-/expect-webdriverio-4.15.4.tgz", + "integrity": "sha512-Op1xZoevlv1pohCq7g2Og5Gr3xP2NhY7MQueOApmopVxgweoJ/BqJxyvMNP0A//QsMg8v0WsN/1j81Sx2er9Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/snapshot": "^2.0.3", + "expect": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "lodash.isequal": "^4.5.0" + }, + "engines": { + "node": ">=16 || >=18 || >=20" + }, + "optionalDependencies": { + "@wdio/globals": "^8.29.3", + "@wdio/logger": "^8.28.0", + "webdriverio": "^8.29.3" + } + }, + "node_modules/@wdio/runner/node_modules/geckodriver": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-4.2.1.tgz", + "integrity": "sha512-4m/CRk0OI8MaANRuFIahvOxYTSjlNAO2p9JmE14zxueknq6cdtB5M9UGRQ8R9aMV0bLGNVHHDnDXmoXdOwJfWg==", + "dev": true, + "hasInstallScript": true, + "license": "MPL-2.0", + "dependencies": { + "@wdio/logger": "^8.11.0", + "decamelize": "^6.0.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.1", + "tar-fs": "^3.0.4", + "unzipper": "^0.10.14", + "which": "^4.0.0" + }, + "bin": { + "geckodriver": "bin/geckodriver.js" + }, + "engines": { + "node": "^16.13 || >=18 || >=20" + } + }, + "node_modules/@wdio/runner/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@wdio/runner/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@wdio/runner/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@wdio/runner/node_modules/proxy-agent": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.1.tgz", + "integrity": "sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.2", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@wdio/runner/node_modules/puppeteer-core": { + "version": "21.11.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-21.11.0.tgz", + "integrity": "sha512-ArbnyA3U5SGHokEvkfWjW+O8hOxV1RSJxOgriX/3A4xZRqixt9ZFHD0yPgZQF05Qj0oAqi8H/7stDorjoHY90Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "1.9.1", + "chromium-bidi": "0.5.8", + "cross-fetch": "4.0.0", + "debug": "4.3.4", + "devtools-protocol": "0.0.1232444", + "ws": "8.16.0" + }, + "engines": { + "node": ">=16.13.2" + } + }, + "node_modules/@wdio/runner/node_modules/puppeteer-core/node_modules/chromium-bidi": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.5.8.tgz", + "integrity": "sha512-blqh+1cEQbHBKmok3rVJkBlBxt9beKBgOsxbFgs7UJcoVbbeZ+K7+6liAsjgpc8l1Xd55cQUy14fXZdGSb4zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mitt": "3.0.1", + "urlpattern-polyfill": "10.0.0" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/@wdio/runner/node_modules/puppeteer-core/node_modules/devtools-protocol": { + "version": "0.0.1232444", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1232444.tgz", + "integrity": "sha512-pM27vqEfxSxRkTMnF+XCmxSEb6duO5R+t8A9DEEJgy4Wz2RVanje2mmj99B6A3zv2r/qGfYlOvYznUhuokizmg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@wdio/runner/node_modules/tar-fs": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", + "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + } + }, + "node_modules/@wdio/runner/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@wdio/runner/node_modules/webdriverio": { + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.43.0.tgz", + "integrity": "sha512-aBwK25CmBQZ98MMlLteMiDiqpuQUsd1RcK6OU2eS921SM1JX4WJGCo27FAgcweb1ehX7rECL1qai1WFCrGfWIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^22.2.0", + "@wdio/config": "8.43.0", + "@wdio/logger": "8.38.0", + "@wdio/protocols": "8.40.3", + "@wdio/repl": "8.40.3", + "@wdio/types": "8.41.0", + "@wdio/utils": "8.41.0", + "archiver": "^7.0.0", + "aria-query": "^5.0.0", + "css-shorthand-properties": "^1.1.1", + "css-value": "^0.0.1", + "devtools-protocol": "^0.0.1400418", + "grapheme-splitter": "^1.0.2", + "import-meta-resolve": "^4.0.0", + "is-plain-obj": "^4.1.0", + "jszip": "^3.10.1", + "lodash.clonedeep": "^4.5.0", + "lodash.zip": "^4.2.0", + "minimatch": "^9.0.0", + "puppeteer-core": "^21.11.0", + "query-selector-shadow-dom": "^1.0.0", + "resq": "^1.9.1", + "rgb2hex": "0.2.5", + "serialize-error": "^11.0.1", + "webdriver": "8.43.0" + }, + "engines": { + "node": "^16.13 || >=18" + }, + "peerDependencies": { + "devtools": "^8.14.0" + }, + "peerDependenciesMeta": { + "devtools": { + "optional": true + } + } + }, + "node_modules/@wdio/runner/node_modules/ws": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@wdio/spec-reporter": { + "version": "9.11.0", + "resolved": "https://registry.npmjs.org/@wdio/spec-reporter/-/spec-reporter-9.11.0.tgz", + "integrity": "sha512-S8pd2hm8zWbE2yiAZqpDSgZ0gRnZRwV4xWAKO/dhHgAYWzHamQ1Hk7P0Sp1sQA/Pu+gyRId3uJ+WDiWVos8ftA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@wdio/reporter": "9.11.0", + "@wdio/types": "9.10.1", + "chalk": "^5.1.2", + "easy-table": "^1.2.0", + "pretty-ms": "^9.0.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/spec-reporter/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@wdio/types": { + "version": "9.10.1", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-9.10.1.tgz", + "integrity": "sha512-/t1VXPU5Ad1FQjRUP0WlK7IR0dCTX5hSkul8SpCuUpWbeyI4Iol/Wx2b1YU6nS+Ydh78rJCyHxtV0eE5TM1rbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/utils": { + "version": "9.12.1", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-9.12.1.tgz", + "integrity": "sha512-WrkBdglOwKMpwvCZbOatlLUCghxNWyVfKRDyl92RBX3DuRqqq+uZK8fSHHAJMvXfax5TxcTRzHZUKrQO3ASSXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@puppeteer/browsers": "^2.2.0", + "@wdio/logger": "9.4.4", + "@wdio/types": "9.10.1", + "decamelize": "^6.0.0", + "deepmerge-ts": "^7.0.3", + "edgedriver": "^6.1.1", + "geckodriver": "^5.0.0", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.2.24", + "safaridriver": "^1.0.0", + "split2": "^4.2.0", + "wait-port": "^1.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/@wdio/utils/node_modules/edgedriver": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/edgedriver/-/edgedriver-6.1.1.tgz", + "integrity": "sha512-/dM/PoBf22Xg3yypMWkmRQrBKEnSyNaZ7wHGCT9+qqT14izwtFT+QvdR89rjNkMfXwW+bSFoqOfbcvM+2Cyc7w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@wdio/logger": "^9.1.3", + "@zip.js/zip.js": "^2.7.53", + "decamelize": "^6.0.0", + "edge-paths": "^3.0.5", + "fast-xml-parser": "^4.5.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "node-fetch": "^3.3.2", + "which": "^5.0.0" + }, + "bin": { + "edgedriver": "bin/edgedriver.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@wdio/utils/node_modules/safaridriver": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safaridriver/-/safaridriver-1.0.0.tgz", + "integrity": "sha512-J92IFbskyo7OYB3Dt4aTdyhag1GlInrfbPCmMteb7aBK7PwlnGz1HI0+oyNN97j7pV9DqUAVoVgkNRMrfY47mQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@wdio/utils/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@zip.js/zip.js": { + "version": "2.7.54", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.54.tgz", + "integrity": "sha512-qMrJVg2hoEsZJjMJez9yI2+nZlBUxgYzGV3mqcb2B/6T1ihXp0fWBDYlVHlHquuorgNUQP5a8qSmX6HF5rFJNg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "bun": ">=0.7.0", + "deno": ">=1.0.0", + "node": ">=16.5.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/acorn": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz", + "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dev": true, + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "dev": true, + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "dev": true, + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "dev": true + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", + "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.6.2", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", + "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.3", + "core-js-compat": "^3.40.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz", + "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/bare-events": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.0.tgz", + "integrity": "sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==", + "dev": true, + "optional": true + }, + "node_modules/bare-fs": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.5.tgz", + "integrity": "sha512-SlE9eTxifPDJrT6YgemQ1WGFleevzwY+XAP1Xqgl56HtcrisC2CHCZ2tq6dBpcH2TnNxwUEUGhweo+lrQtYuiw==", + "dev": true, + "optional": true, + "dependencies": { + "bare-events": "^2.0.0", + "bare-path": "^2.0.0", + "bare-stream": "^2.0.0" + } + }, + "node_modules/bare-os": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.4.tgz", + "integrity": "sha512-z3UiI2yi1mK0sXeRdc4O1Kk8aOa/e+FNWZcTiPB/dfTWyLypuE99LibgRaQki914Jq//yAWylcAt+mknKdixRQ==", + "dev": true, + "optional": true + }, + "node_modules/bare-path": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz", + "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==", + "dev": true, + "optional": true, + "dependencies": { + "bare-os": "^2.1.0" + } + }, + "node_modules/bare-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.3.1.tgz", + "integrity": "sha512-Vm8kAeOcfzHPTH8sq0tHBnUqYrkXdroaBVVylqFT4cF5wnMfKEIxxy2jIGu2zKVNl9P8MAP9XBWwXJ9N2+jfEw==", + "dev": true, + "optional": true, + "dependencies": { + "streamx": "^2.20.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "dev": true, + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", + "dev": true + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "dev": true, + "engines": { + "node": ">=0.2.0" + } + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/builtins": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.1.0.tgz", + "integrity": "sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==", + "dev": true, + "dependencies": { + "semver": "^7.0.0" + } + }, + "node_modules/builtins/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacheable-lookup": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/cacheable-request": { + "version": "10.2.14", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", + "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "^4.0.2", + "get-stream": "^6.0.1", + "http-cache-semantics": "^4.1.1", + "keyv": "^4.5.3", + "mimic-response": "^4.0.0", + "normalize-url": "^8.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001707", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", + "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "dev": true, + "license": "MIT/X11", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "node_modules/cheerio": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", + "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", + "dev": true, + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "encoding-sniffer": "^0.2.0", + "htmlparser2": "^9.1.0", + "parse5": "^7.1.2", + "parse5-htmlparser2-tree-adapter": "^7.0.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^6.19.5", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=18.17" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chokidar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "dev": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chromium-bidi": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.3.tgz", + "integrity": "sha512-qXlsCmpCZJAnoTYI83Iu6EdYQpMYdVkCfq08KDh2pmlVqK5t5IA9mGs4/LwCwp4fqisSOMXZxP3HIh8w8aRn0A==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "mitt": "3.0.1", + "urlpattern-polyfill": "10.0.0", + "zod": "3.23.8" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cliui/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "dev": true, + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/compress-commons/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/core-js-compat": { + "version": "3.41.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz", + "integrity": "sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "dev": true, + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/cross-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, + "node_modules/cross-fetch/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-shorthand-properties": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/css-shorthand-properties/-/css-shorthand-properties-1.1.2.tgz", + "integrity": "sha512-C2AugXIpRGQTxaCW0N7n5jD/p5irUmCrwl03TrnMFBHDbdq44CFWR2zO7rK9xPN4Eo3pUxC4vQzQgbIpzrD1PQ==", + "dev": true + }, + "node_modules/css-value": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/css-value/-/css-value-0.0.1.tgz", + "integrity": "sha512-FUV3xaJ63buRLgHrLQVlVgQnQdR4yqdLGaDu7g8CQcWjInDfM9plBTPI9FRfpahju1UBSaMckeb2/46ApS/V1Q==", + "dev": true + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz", + "integrity": "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge-ts": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.3.tgz", + "integrity": "sha512-qCSH6I0INPxd9Y1VtAiLpnYvz5O//6rCfJXKk0z66Up9/VOSr+1yS8XSKA5IWRxjocFGlzPyaZYe+jxq7OOLtQ==", + "dev": true, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "optional": true, + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1400418", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1400418.tgz", + "integrity": "sha512-U8j75zDOXF8IP3o0Cgb7K4tFA9uUHEOru2Wx64+EUqL4LNOh9dRe1i8WKR1k3mSpjcCe3aIkTDvEwq0YkI4hfw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/easy-table": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/easy-table/-/easy-table-1.2.0.tgz", + "integrity": "sha512-OFzVOv03YpvtcWGe5AayU5G2hgybsg3iqA6drU8UaoZyB9jLGMTrz9+asnLp/E+6qPh88yEI1gvyZFZ41dmgww==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "optionalDependencies": { + "wcwidth": "^1.0.1" + } + }, + "node_modules/edge-paths": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/edge-paths/-/edge-paths-3.0.5.tgz", + "integrity": "sha512-sB7vSrDnFa4ezWQk9nZ/n0FdpdUuC6R1EOrlU3DL+bovcNFK28rqu2emmAUjujYEJTWIgQGqgVVWUZXMnc8iWg==", + "dev": true, + "dependencies": { + "@types/which": "^2.0.1", + "which": "^2.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/shirshak55" + } + }, + "node_modules/edge-paths/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/edge-paths/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/edgedriver": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/edgedriver/-/edgedriver-5.6.1.tgz", + "integrity": "sha512-3Ve9cd5ziLByUdigw6zovVeWJjVs8QHVmqOB0sJ0WNeVPcwf4p18GnxMmVvlFmYRloUwf5suNuorea4QzwBIOA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@wdio/logger": "^8.38.0", + "@zip.js/zip.js": "^2.7.48", + "decamelize": "^6.0.0", + "edge-paths": "^3.0.5", + "fast-xml-parser": "^4.4.1", + "node-fetch": "^3.3.2", + "which": "^4.0.0" + }, + "bin": { + "edgedriver": "bin/edgedriver.js" + } + }, + "node_modules/edgedriver/node_modules/@wdio/logger": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-8.38.0.tgz", + "integrity": "sha512-kcHL86RmNbcQP+Gq/vQUGlArfU6IIcbbnNp32rRIraitomZow+iEoc519rdQmSVusDozMS5DZthkgDdxK+vz6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/edgedriver/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.123", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.123.tgz", + "integrity": "sha512-refir3NlutEZqlKaBLK0tzlVLe5P2wDKS7UQt/3SpibizgsRAPOsqQC3ffw1nlv3ze5gjRQZYHoPymgVZkplFA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/encoding-sniffer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", + "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==", + "dev": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/encoding-sniffer/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz", + "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.1", + "@esbuild/android-arm": "0.25.1", + "@esbuild/android-arm64": "0.25.1", + "@esbuild/android-x64": "0.25.1", + "@esbuild/darwin-arm64": "0.25.1", + "@esbuild/darwin-x64": "0.25.1", + "@esbuild/freebsd-arm64": "0.25.1", + "@esbuild/freebsd-x64": "0.25.1", + "@esbuild/linux-arm": "0.25.1", + "@esbuild/linux-arm64": "0.25.1", + "@esbuild/linux-ia32": "0.25.1", + "@esbuild/linux-loong64": "0.25.1", + "@esbuild/linux-mips64el": "0.25.1", + "@esbuild/linux-ppc64": "0.25.1", + "@esbuild/linux-riscv64": "0.25.1", + "@esbuild/linux-s390x": "0.25.1", + "@esbuild/linux-x64": "0.25.1", + "@esbuild/netbsd-arm64": "0.25.1", + "@esbuild/netbsd-x64": "0.25.1", + "@esbuild/openbsd-arm64": "0.25.1", + "@esbuild/openbsd-x64": "0.25.1", + "@esbuild/sunos-x64": "0.25.1", + "@esbuild/win32-arm64": "0.25.1", + "@esbuild/win32-ia32": "0.25.1", + "@esbuild/win32-x64": "0.25.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-cli": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/eslint-cli/-/eslint-cli-1.1.1.tgz", + "integrity": "sha512-Gu+fYzt7M+jIb5szUHLl5Ex0vFY7zErbi78D7ZaaLunvVTxHRvbOlfzmJlIUWsV5WDM4qyu9TD7WnGgDaDgaMA==", + "dev": true, + "dependencies": { + "chalk": "^2.0.1", + "debug": "^2.6.8", + "resolve": "^1.3.3" + }, + "bin": { + "eslint": "bin/eslint.js", + "eslint-cli": "bin/eslint.js" + } + }, + "node_modules/eslint-cli/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/eslint-cli/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/eslint-compat-utils": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", + "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", + "dev": true, + "dependencies": { + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-compat-utils/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-config-standard": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.1.0.tgz", + "integrity": "sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "eslint": "^8.0.1", + "eslint-plugin-import": "^2.25.2", + "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", + "eslint-plugin-promise": "^6.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-es": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", + "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", + "dev": true, + "dependencies": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=4.19.1" + } + }, + "node_modules/eslint-plugin-es-x": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.8.0.tgz", + "integrity": "sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/ota-meshi", + "https://opencollective.com/eslint" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.1.2", + "@eslint-community/regexpp": "^4.11.0", + "eslint-compat-utils": "^0.5.1" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": ">=8" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "dev": true, + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-json": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-json/-/eslint-plugin-json-4.0.1.tgz", + "integrity": "sha512-3An5ISV5dq/kHfXdNyY5TUe2ONC3yXFSkLX2gu+W8xAhKhfvrRvkSAeKXCxZqZ0KJLX15ojBuLPyj+UikQMkOA==", + "dev": true, + "dependencies": { + "lodash": "^4.17.21", + "vscode-json-languageservice": "^4.1.6" + }, + "engines": { + "node": ">=18.0" + } + }, + "node_modules/eslint-plugin-n": { + "version": "16.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-16.6.2.tgz", + "integrity": "sha512-6TyDmZ1HXoFQXnhCTUjVFULReoBPOAjpuiKELMkeP40yffI/1ZRO+d9ug/VC6fqISo2WkuIBk3cvuRPALaWlOQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "builtins": "^5.0.1", + "eslint-plugin-es-x": "^7.5.0", + "get-tsconfig": "^4.7.0", + "globals": "^13.24.0", + "ignore": "^5.2.4", + "is-builtin-module": "^3.2.1", + "is-core-module": "^2.12.1", + "minimatch": "^3.1.2", + "resolve": "^1.22.2", + "semver": "^7.5.3" + }, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-n/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-n/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-n/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-node": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", + "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", + "dev": true, + "dependencies": { + "eslint-plugin-es": "^3.0.0", + "eslint-utils": "^2.0.0", + "ignore": "^5.1.1", + "minimatch": "^3.0.4", + "resolve": "^1.10.1", + "semver": "^6.1.0" + }, + "engines": { + "node": ">=8.10.0" + }, + "peerDependencies": { + "eslint": ">=5.16.0" + } + }, + "node_modules/eslint-plugin-promise": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.6.0.tgz", + "integrity": "sha512-57Zzfw8G6+Gq7axm2Pdo3gW/Rx3h9Yywgn61uE/3elTCOePEHVrn2i5CdfBwA1BLK0Q0WqctICIUSqXZW/VprQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.4.1.tgz", + "integrity": "sha512-5eo/BRqZm3GYce+1jqX/tJ7duA2AnE39i88fuedNFUV8XxGxUpF3aWkBRfbUcjV49gCkvS/pzc0YrCPhaIewdg==", + "dev": true, + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.3", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.0", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/expect-webdriverio": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/expect-webdriverio/-/expect-webdriverio-5.1.0.tgz", + "integrity": "sha512-4u3q+Dqx/lXNgvCx1gKia4CfS28z1UxGGfVUkoMNbrsBlTBB2fYqXG+4+YtYoerxvp/XPwIb/+89IGEdyPbDXQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@vitest/snapshot": "^2.0.5", + "expect": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "lodash.isequal": "^4.5.0" + }, + "engines": { + "node": ">=18 || >=20 || >=22" + }, + "peerDependencies": { + "@wdio/globals": "^9.0.0", + "@wdio/logger": "^9.0.0", + "webdriverio": "^9.0.0" + }, + "peerDependenciesMeta": { + "@wdio/globals": { + "optional": false + }, + "@wdio/logger": { + "optional": false + }, + "webdriverio": { + "optional": false + } + } + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fast-xml-parser": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz", + "integrity": "sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data-encoder": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", + "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.17" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/fstream/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fstream/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaze": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "globule": "^1.0.0" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/geckodriver": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-5.0.0.tgz", + "integrity": "sha512-vn7TtQ3b9VMJtVXsyWtQQl1fyBVFhQy7UvJF96kPuuJ0or5THH496AD3eUyaDD11+EqCxH9t6V+EP9soZQk4YQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@wdio/logger": "^9.1.3", + "@zip.js/zip.js": "^2.7.53", + "decamelize": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "node-fetch": "^3.3.2", + "tar-fs": "^3.0.6", + "which": "^5.0.0" + }, + "bin": { + "geckodriver": "bin/geckodriver.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/geckodriver/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-port": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.1.0.tgz", + "integrity": "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz", + "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/get-uri": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz", + "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==", + "dev": true, + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4", + "fs-extra": "^11.2.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/get-uri/node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globule": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.4.tgz", + "integrity": "sha512-OPTIfhMBh7JbBYDpa5b+Q5ptmMWKwcNcFSR/0c6t8V4f3ZAVBEsKNY37QdVqmLRYSMhOUGYrY0QhSoEpzGr/Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "~7.1.1", + "lodash": "^4.17.21", + "minimatch": "~3.0.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/globule/node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globule/node_modules/minimatch": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", + "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "12.6.1", + "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", + "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^5.2.0", + "@szmarczak/http-timer": "^5.0.1", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^10.2.8", + "decompress-response": "^6.0.0", + "form-data-encoder": "^2.1.2", + "get-stream": "^6.0.1", + "http2-wrapper": "^2.1.10", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^3.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/got/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dev": true, + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/htmlfy": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/htmlfy/-/htmlfy-0.6.2.tgz", + "integrity": "sha512-dWRE+TW3QSB5mXsnYCUPLoPmaCu2O7kp6/3xh5fayiGuaNtRL/64SdjhoTBwJ2XvuSkLoMgQDLunrAqwxJj40Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http2-wrapper": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.0.tgz", + "integrity": "sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", + "integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/inquirer": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-11.1.0.tgz", + "integrity": "sha512-CmLAZT65GG/v30c+D2Fk8+ceP6pxD6RL+hIUOWAltCmeyEqWYwqu9v76q03OvjyZ3AB0C1Ala2stn1z/rMqGEw==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.2.1", + "@inquirer/prompts": "^6.0.1", + "@inquirer/type": "^2.0.0", + "@types/mute-stream": "^0.0.4", + "ansi-escapes": "^4.3.2", + "mute-stream": "^1.0.0", + "run-async": "^3.0.0", + "rxjs": "^7.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jake/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jake/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jake/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jake/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jake/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-diff/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-diff/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-matcher-utils/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-matcher-utils/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-message-util/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-message-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-util/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true + }, + "node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json-web-key": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-web-key/-/json-web-key-0.4.0.tgz", + "integrity": "sha512-4MwQAsadU3y7ZTfhwup/2lYJUEB7mAOMOjfNvuHGJkgkvXaBaiWlFsuWNFpWkTG23ZlMm56TBB3BI9cVQTxjzA==", + "dev": true, + "dependencies": { + "asn1.js": "^5.0.1" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsrsasign": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-11.1.0.tgz", + "integrity": "sha512-Ov74K9GihaK9/9WncTe1mPmvrO7Py665TUfUKvraXBpu+xcTWitrtuOwcjf4KMU9maPaYn0OuaWy0HOzy/GBXg==", + "dev": true, + "funding": { + "url": "https://github.com/kjur/jsrsasign#donations" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/jszip/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ky": { + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/ky/-/ky-0.33.3.tgz", + "integrity": "sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/ky?sponsor=1" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lines-and-columns": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.4.tgz", + "integrity": "sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/livereload": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/livereload/-/livereload-0.9.3.tgz", + "integrity": "sha512-q7Z71n3i4X0R9xthAryBdNGVGAO2R5X+/xXpmKeuPMrteg+W2U8VusTKV3YiJbXZwKsOlFlHe+go6uSNjfxrZw==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.0", + "livereload-js": "^3.3.1", + "opts": ">= 1.2.0", + "ws": "^7.4.3" + }, + "bin": { + "livereload": "bin/livereload.js" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/livereload-js": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-3.4.1.tgz", + "integrity": "sha512-5MP0uUeVCec89ZbNOT/i97Mc+q3SxXmiUGhRFOTmhrGPn//uWVQdCvcLJDy64MSBR5MidFdOR7B9viumoavy6g==", + "dev": true + }, + "node_modules/livereload/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/livereload/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/livereload/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/locate-app": { + "version": "2.4.43", + "resolved": "https://registry.npmjs.org/locate-app/-/locate-app-2.4.43.tgz", + "integrity": "sha512-BX6NEdECUGcDQw8aqqg02qLyF9rF8V+dAfyAnBzL2AofIlIvf4Q6EGXnzVWpWot9uBE+x/o8CjXHo7Zlegu91Q==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://buymeacoffee.com/hejny" + }, + { + "type": "github", + "url": "https://github.com/hejny/locate-app/blob/main/README.md#%EF%B8%8F-contributing" + } + ], + "dependencies": { + "@promptbook/utils": "0.70.0-1", + "type-fest": "2.13.0", + "userhome": "1.0.0" + } + }, + "node_modules/locate-app/node_modules/type-fest": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.13.0.tgz", + "integrity": "sha512-lPfAm42MxE4/456+QyIaaVBAwgpJb6xZ8PRu09utnhPdWwcyj9vgy6Sq0Z5yNbJ21EdxB5dRU/Qg8bsyAMtlcw==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "dev": true + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "dev": true + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.pickby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz", + "integrity": "sha512-AZV+GsS/6ckvPOVQPXSiFFacKvKB4kOQu6ynt9wz0F3LO4R9Ij4K1ddYsIytDpSgLz88JHd9P+oaLeej5/Sl7Q==", + "dev": true + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", + "dev": true + }, + "node_modules/lodash.zip": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz", + "integrity": "sha512-C7IOaBBK/0gMORRBd8OETNx3kmOkgIWIPvyDpZSCTwUrpYmgZwJkjZeOD8ww4xbOUOs4/attY+pciKvadNfFbg==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/loglevel-plugin-prefix": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz", + "integrity": "sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==", + "dev": true + }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", + "dev": true + }, + "node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.12", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", + "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/mocha": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.7.3.tgz", + "integrity": "sha512-uQWxAu44wwiACGqjbPYmjo7Lg8sFrS3dQe7PP2FQI+woptP4vZXSMcfMyFL/e1yFEeEpV4RtyTpZROOKmxis+A==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/mocha/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/mocha/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/mocha/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/mocha/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/mocha/node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/mocha/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/mocha/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/mocha/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/mocha/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/mocha/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/mocha/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-jose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-jose/-/node-jose-2.2.0.tgz", + "integrity": "sha512-XPCvJRr94SjLrSIm4pbYHKLEaOsDvJCpyFw/6V/KK/IXmyZ6SFBzAUDO9HQf4DB/nTEFcRGH87mNciOP23kFjw==", + "dev": true, + "dependencies": { + "base64url": "^3.0.1", + "buffer": "^6.0.3", + "es6-promise": "^4.2.8", + "lodash": "^4.17.21", + "long": "^5.2.0", + "node-forge": "^1.2.1", + "pako": "^2.0.4", + "process": "^0.11.10", + "uuid": "^9.0.0" + } + }, + "node_modules/node-jose/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "dev": true, + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", + "integrity": "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/opts": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/opts/-/opts-2.0.2.tgz", + "integrity": "sha512-k41FwbcLnlgnFh69f4qdUfvDQ+5vaSDnVPFI/y5XuhKRq97EnVVneO9F1ESVCdiVu4fCS2L8usX3mU331hB7pg==", + "dev": true + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-cancelable": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", + "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz", + "integrity": "sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==", + "dev": true, + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.5", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "dev": true + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-7.1.1.tgz", + "integrity": "sha512-SgOTCX/EZXtZxBE5eJ97P4yGM5n37BwRU+YMsH4vNzFqJV/oWFXXCmwFlgWUM4PrakybVOueJJ6pwHqSVhTFDw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.21.4", + "error-ex": "^1.3.2", + "json-parse-even-better-errors": "^3.0.0", + "lines-and-columns": "^2.0.3", + "type-fest": "^3.8.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json/node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.0.tgz", + "integrity": "sha512-ZkDsAOcxsUMZ4Lz5fVciOehNcJ+Gb8gTzcA4yl3wnc273BAybYWrQ+Ks/OjCjSEpjvQkDSeZbybK9qj2VHHdGA==", + "dev": true, + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-ms": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.1.0.tgz", + "integrity": "sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw==", + "dev": true, + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/puppeteer-core": { + "version": "22.15.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.15.0.tgz", + "integrity": "sha512-cHArnywCiAAVXa3t4GGL2vttNxh7GqXtIYGym99egkNJ3oG//wL9LkvO4WE8W1TJe95t1F1ocu9X4xWaGsOKOA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@puppeteer/browsers": "2.3.0", + "chromium-bidi": "0.6.3", + "debug": "^4.3.6", + "devtools-protocol": "0.0.1312386", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core/node_modules/@puppeteer/browsers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.0.tgz", + "integrity": "sha512-ioXoq9gPxkss4MYhD+SFaU9p1IHFUX0ILAWFPyjGaBdjLsYAlZw6j1iLA0N/m12uVHLFDfSYNF7EQccjinIMDA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "debug": "^4.3.5", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core/node_modules/devtools-protocol": { + "version": "0.0.1312386", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz", + "integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/puppeteer-core/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "optional": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/puppeteer-core/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/query-selector-shadow-dom": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz", + "integrity": "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==", + "dev": true + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/read-pkg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-8.1.0.tgz", + "integrity": "sha512-PORM8AgzXeskHO/WEv312k9U03B8K9JSiWF/8N9sUuFjBa+9SF2u6K7VClzXwDXab51jCd8Nd36CNM+zR97ScQ==", + "dev": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.1", + "normalize-package-data": "^6.0.0", + "parse-json": "^7.0.0", + "type-fest": "^4.2.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-10.1.0.tgz", + "integrity": "sha512-aNtBq4jR8NawpKJQldrQcSW9y/d+KWH4v24HWkHljOZ7H0av+YTGANBzRh9A5pw7v/bLVsLVPpOhJ7gHNVy8lA==", + "dev": true, + "dependencies": { + "find-up": "^6.3.0", + "read-pkg": "^8.1.0", + "type-fest": "^4.2.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz", + "integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/yocto-queue": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", + "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz", + "integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dev": true, + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "dev": true, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/recursive-readdir": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", + "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", + "dev": true, + "dependencies": { + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", + "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/regexpu-core": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.1.1.tgz", + "integrity": "sha512-k67Nb9jvwJcJmVpw0jPttR1/zVfnKf8Km0IPatrU/zJ5XeG3+Slx0xLXs9HByJSzXzrlz5EDvN6yLNMDc2qdnw==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.11.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true + }, + "node_modules/regjsparser": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.11.1.tgz", + "integrity": "sha512-1DHODs4B8p/mQHU9kr+jv8+wIC9mtG4eBHxWxIq5mhjE3D5oORhCc6deRKzTjs9DcfRFmj9BHSDguZklqCGFWQ==", + "dev": true, + "dependencies": { + "jsesc": "~3.0.2" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/responselike": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", + "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/resq": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/resq/-/resq-1.11.0.tgz", + "integrity": "sha512-G10EBz+zAAy3zUd/CDoBbXRL6ia9kOo3xRHrMDsHljI0GDkhYlyjwoCx5+3eCC4swi1uCoZQhskuJkj7Gp57Bw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^2.0.1" + } + }, + "node_modules/resq/node_modules/fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", + "dev": true + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rgb2hex": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/rgb2hex/-/rgb2hex-0.2.5.tgz", + "integrity": "sha512-22MOP1Rh7sAo1BZpDG6R5RFYzR2lYEgwq7HEmyW2qcsOqR2lQKmn+O//xV3YG/0rrhMC6KVX2hU+ZXuaw9a5bw==", + "dev": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safaridriver": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/safaridriver/-/safaridriver-0.1.2.tgz", + "integrity": "sha512-4R309+gWflJktzPXBQCobbWEHlzC4aK3a+Ov3tz2Ib2aBxiwd11phkdIBH1l0EO22x24CJMUQkpKFumRriCSRg==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/serialize-error": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-11.0.3.tgz", + "integrity": "sha512-2G2y++21dhj2R7iHAdd0FIzjGwuKZld+7Pl/bTU6YIkrC2ZMbVUjm+luj6A6V34Rv9XfKJDKpTWu9W4Gse1D9g==", + "dev": true, + "dependencies": { + "type-fest": "^2.12.2" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-error/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "dev": true, + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", + "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.1", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spacetrim": { + "version": "0.11.39", + "resolved": "https://registry.npmjs.org/spacetrim/-/spacetrim-0.11.39.tgz", + "integrity": "sha512-S/baW29azJ7py5ausQRE2S6uEDQnlxgMHOEEq4V770ooBDD1/9kZnxRcco/tjZYuDuqYXblCk/r3N13ZmvHZ2g==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://buymeacoffee.com/hejny" + }, + { + "type": "github", + "url": "https://github.com/hejny/spacetrim/blob/main/README.md#%EF%B8%8F-contributing" + } + ] + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.20", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", + "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", + "dev": true + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stream-buffers": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-3.0.3.tgz", + "integrity": "sha512-pqMqwQCso0PBJt2PQmDO0cFj0lyqmiwOMiMSkVtRokl7e+ZTRYgDHKnuZNbqjiJXgsg4nuqtD/zxuo9KqTp0Yw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/streamx": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.20.1.tgz", + "integrity": "sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA==", + "dev": true, + "dependencies": { + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "dev": true + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "dev": true, + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/text-decoder": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.1.tgz", + "integrity": "sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ==", + "dev": true + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-regex-range": { + "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==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "dev": true, + "license": "MIT/X11", + "engines": { + "node": "*" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", + "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==", + "dev": true + }, + "node_modules/tsx": { + "version": "4.19.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz", + "integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dev": true, + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/unbzip2-stream/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/undici": { + "version": "6.21.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz", + "integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/urlpattern-polyfill": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", + "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==", + "dev": true + }, + "node_modules/userhome": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/userhome/-/userhome-1.0.0.tgz", + "integrity": "sha512-ayFKY3H+Pwfy4W98yPdtH1VqH4psDeyW8lYYFzfecR9d6hqLpqhecktvYR3SEEXt7vG0S1JEpciI3g94pMErig==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/vscode-json-languageservice": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-4.2.1.tgz", + "integrity": "sha512-xGmv9QIWs2H8obGbWg+sIPI/3/pFgj/5OWBhNzs00BkYQ9UaB2F6JJaGB/2/YOZJ3BvLXQTC4Q7muqU25QgAhA==", + "dev": true, + "dependencies": { + "jsonc-parser": "^3.0.0", + "vscode-languageserver-textdocument": "^1.0.3", + "vscode-languageserver-types": "^3.16.0", + "vscode-nls": "^5.0.0", + "vscode-uri": "^3.0.3" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "dev": true + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "dev": true + }, + "node_modules/vscode-nls": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-5.2.0.tgz", + "integrity": "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==", + "dev": true + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "dev": true + }, + "node_modules/wait-port": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-1.1.0.tgz", + "integrity": "sha512-3e04qkoN3LxTMLakdqeWth8nih8usyg+sf1Bgdf9wwUkp05iuK1eSY/QpLvscT/+F/gA89+LpUmmgBtesbqI2Q==", + "dev": true, + "dependencies": { + "chalk": "^4.1.2", + "commander": "^9.3.0", + "debug": "^4.3.4" + }, + "bin": { + "wait-port": "bin/wait-port.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/wait-port/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wait-port/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/wait-port/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wait-port/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wait-port/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/wait-port/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wait-port/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "optional": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/webdriver": { + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-8.43.0.tgz", + "integrity": "sha512-cfyqymgFae4gtxuvp/nz7Yat3qqlEyxopQL7H6MSSOey+mXq3kYZZ0auyuhGaaPvDidOY3Oh597cEB2ft8pW3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^22.2.0", + "@types/ws": "^8.5.3", + "@wdio/config": "8.43.0", + "@wdio/logger": "8.38.0", + "@wdio/protocols": "8.40.3", + "@wdio/types": "8.41.0", + "@wdio/utils": "8.41.0", + "deepmerge-ts": "^5.1.0", + "got": "^12.6.1", + "ky": "^0.33.0", + "ws": "^8.8.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/webdriver/node_modules/@puppeteer/browsers": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.9.1.tgz", + "integrity": "sha512-PuvK6xZzGhKPvlx3fpfdM2kYY3P/hB1URtK8wA7XUJ6prn6pp22zvJHu48th0SGcHL9SutbPHrFuQgfXTFobWA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "4.3.4", + "extract-zip": "2.0.1", + "progress": "2.0.3", + "proxy-agent": "6.3.1", + "tar-fs": "3.0.4", + "unbzip2-stream": "1.4.3", + "yargs": "17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=16.3.0" + } + }, + "node_modules/webdriver/node_modules/@types/node": { + "version": "22.13.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.13.tgz", + "integrity": "sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/webdriver/node_modules/@wdio/config": { + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@wdio/config/-/config-8.43.0.tgz", + "integrity": "sha512-mptlO5Lt8hV/8T9vRkp24G4o/kpI4qa9k4bRlOsI777MgeOlCEu2xLdRvVNGcg7RigDkx4jdYXX1pIHFg7gpSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@wdio/logger": "8.38.0", + "@wdio/types": "8.41.0", + "@wdio/utils": "8.41.0", + "decamelize": "^6.0.0", + "deepmerge-ts": "^5.0.0", + "glob": "^10.2.2", + "import-meta-resolve": "^4.0.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/webdriver/node_modules/@wdio/logger": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-8.38.0.tgz", + "integrity": "sha512-kcHL86RmNbcQP+Gq/vQUGlArfU6IIcbbnNp32rRIraitomZow+iEoc519rdQmSVusDozMS5DZthkgDdxK+vz6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.1.2", + "loglevel": "^1.6.0", + "loglevel-plugin-prefix": "^0.8.4", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/webdriver/node_modules/@wdio/protocols": { + "version": "8.40.3", + "resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-8.40.3.tgz", + "integrity": "sha512-wK7+eyrB3TAei8RwbdkcyoNk2dPu+mduMBOdPJjp8jf/mavd15nIUXLID1zA+w5m1Qt1DsT1NbvaeO9+aJQ33A==", + "dev": true, + "license": "MIT" + }, + "node_modules/webdriver/node_modules/@wdio/types": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.41.0.tgz", + "integrity": "sha512-t4NaNTvJZci3Xv/yUZPH4eTL0hxrVTf5wdwNnYIBrzMnlRDbNefjQ0P7FM7ZjQCLaH92AEH6t/XanUId7Webug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^22.2.0" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/webdriver/node_modules/@wdio/utils": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.41.0.tgz", + "integrity": "sha512-0TcTjBiax1VxtJQ/iQA0ZyYOSHjjX2ARVmEI0AMo9+AuIq+xBfnY561+v8k9GqOMPKsiH/HrK3xwjx8xCVS03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@puppeteer/browsers": "^1.6.0", + "@wdio/logger": "8.38.0", + "@wdio/types": "8.41.0", + "decamelize": "^6.0.0", + "deepmerge-ts": "^5.1.0", + "edgedriver": "^5.5.0", + "geckodriver": "~4.2.0", + "get-port": "^7.0.0", + "import-meta-resolve": "^4.0.0", + "locate-app": "^2.1.0", + "safaridriver": "^0.1.0", + "split2": "^4.2.0", + "wait-port": "^1.0.4" + }, + "engines": { + "node": "^16.13 || >=18" + } + }, + "node_modules/webdriver/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/webdriver/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/webdriver/node_modules/deepmerge-ts": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-5.1.0.tgz", + "integrity": "sha512-eS8dRJOckyo9maw9Tu5O5RUi/4inFLrnoLkBe3cPfDMx3WZioXtmOew4TXQaxq7Rhl4xjDtR7c6x8nNTxOvbFw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/webdriver/node_modules/geckodriver": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-4.2.1.tgz", + "integrity": "sha512-4m/CRk0OI8MaANRuFIahvOxYTSjlNAO2p9JmE14zxueknq6cdtB5M9UGRQ8R9aMV0bLGNVHHDnDXmoXdOwJfWg==", + "dev": true, + "hasInstallScript": true, + "license": "MPL-2.0", + "dependencies": { + "@wdio/logger": "^8.11.0", + "decamelize": "^6.0.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.1", + "tar-fs": "^3.0.4", + "unzipper": "^0.10.14", + "which": "^4.0.0" + }, + "bin": { + "geckodriver": "bin/geckodriver.js" + }, + "engines": { + "node": "^16.13 || >=18 || >=20" + } + }, + "node_modules/webdriver/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/webdriver/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/webdriver/node_modules/proxy-agent": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.1.tgz", + "integrity": "sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.2", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/webdriver/node_modules/tar-fs": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", + "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + } + }, + "node_modules/webdriver/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/webdriver/node_modules/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webdriverio": { + "version": "9.12.1", + "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-9.12.1.tgz", + "integrity": "sha512-xKasUD3DRey9lEcTAbrI29XCxLX45cxVHOIN5EJK44/thch4zhzfskdCKY3BQ9589TCkSGY1CGf8q9LFx2Ve5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.11.30", + "@types/sinonjs__fake-timers": "^8.1.5", + "@wdio/config": "9.12.1", + "@wdio/logger": "9.4.4", + "@wdio/protocols": "9.7.0", + "@wdio/repl": "9.4.4", + "@wdio/types": "9.10.1", + "@wdio/utils": "9.12.1", + "archiver": "^7.0.1", + "aria-query": "^5.3.0", + "cheerio": "^1.0.0-rc.12", + "css-shorthand-properties": "^1.1.1", + "css-value": "^0.0.1", + "grapheme-splitter": "^1.0.4", + "htmlfy": "^0.6.0", + "is-plain-obj": "^4.1.0", + "jszip": "^3.10.1", + "lodash.clonedeep": "^4.5.0", + "lodash.zip": "^4.2.0", + "query-selector-shadow-dom": "^1.0.1", + "resq": "^1.11.0", + "rgb2hex": "0.2.5", + "serialize-error": "^11.0.3", + "urlpattern-polyfill": "^10.0.0", + "webdriver": "9.12.1" + }, + "engines": { + "node": ">=18.20.0" + }, + "peerDependencies": { + "puppeteer-core": "^22.3.0" + }, + "peerDependenciesMeta": { + "puppeteer-core": { + "optional": true + } + } + }, + "node_modules/webdriverio/node_modules/@wdio/repl": { + "version": "9.4.4", + "resolved": "https://registry.npmjs.org/@wdio/repl/-/repl-9.4.4.tgz", + "integrity": "sha512-kchPRhoG/pCn4KhHGiL/ocNhdpR8OkD2e6sANlSUZ4TGBVi86YSIEjc2yXUwLacHknC/EnQk/SFnqd4MsNjGGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/webdriverio/node_modules/webdriver": { + "version": "9.12.1", + "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-9.12.1.tgz", + "integrity": "sha512-gtdsfoYAVgPVlN1x3kXhPmOOzQXx6vtw9KB0LCeFQH2zUfNMB7RB1OJN475cFgSgJ7PpyvXk3CKdT1lGCHRTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.1.0", + "@types/ws": "^8.5.3", + "@wdio/config": "9.12.1", + "@wdio/logger": "9.4.4", + "@wdio/protocols": "9.7.0", + "@wdio/types": "9.10.1", + "@wdio/utils": "9.12.1", + "deepmerge-ts": "^7.0.3", + "undici": "^6.20.1", + "ws": "^8.8.0" + }, + "engines": { + "node": ">=18.20.0" + } + }, + "node_modules/webdriverio/node_modules/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/workerpool": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dev": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser/node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs-unparser/node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yauzl/node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz", + "integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dev": true, + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "dev": true, + "optional": true, + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json index d6d16dbb67..9f22f533a9 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "eq-questionnaire-runner", "version": "1.0.0", "description": "ONS Digital eQ Questionnaire Runner App", + "type": "module", "author": { "name": "ONS Digital", "url": "http://onsdigital.github.io/" @@ -12,47 +13,43 @@ }, "scripts": { "start": "make dev-compose-up && concurrently \"make run\" \"livereload . -e 'html,njk'\"", - "lint": "yarn generate_pages && yarn lint:tests && yarn lint:test-schemas", + "lint": "npm run generate_pages && npm run lint:tests && npm run lint:test-schemas", "lint:tests": "prettier --check \"tests/functional/**/*.js\" && eslint \"tests/functional/**/*.js\"", "lint:test-schemas": "prettier --check \"schemas/test/*/*.json\" && eslint \"schemas/test/**/*.json\"", - "test_functional": "./node_modules/.bin/wdio tests/functional/wdio.conf.js --suite $1", - "generate_pages": "rm -rf ./tests/functional/generated_pages && pipenv run python -m tests.functional.generate_pages schemas/test/en/ ./tests/functional/generated_pages -r '../../base_pages'", - "format": "yarn format:tests && yarn format:test-schemas", + "test_functional": "./node_modules/.bin/wdio tests/functional/wdio.conf.js", + "generate_pages": "rm -rf ./tests/functional/generated_pages && poetry run python -m tests.functional.generate_pages schemas/test/en/ ./tests/functional/generated_pages -r '../../base_pages'", + "format": "npm run format:tests && npm run format:test-schemas", "format:tests": "prettier \"tests/functional/**/*.js\" --write && eslint --fix \"tests/functional/**/*.js\"", - "format:test-schemas": "prettier \"schemas/test/*/*.json\" --write && eslint --fix \"schemas/test/*/*.json\"" + "format:test-schemas": "prettier \"schemas/test/*/*.json\" --write && eslint --fix \"schemas/test/*/*.json\"", + "wdio": "wdio run ./tests/functional/wdio.conf.js" }, "devDependencies": { - "@babel/core": "^7.17.5", - "@babel/plugin-transform-runtime": "^7.17.0", - "@babel/preset-env": "^7.16.11", - "@babel/register": "^7.17.0", - "@babel/runtime": "^7.17.2", - "@wdio/cli": "^7.16.16", - "@wdio/local-runner": "^7.16.16", - "@wdio/mocha-framework": "^7.16.15", - "@wdio/spec-reporter": "^7.16.14", - "@wdio/sync": "^7.16.16", - "chai": "^4.3.6", - "chromedriver": "^99.0.0", - "eslint": "^8.10.0", + "@babel/core": "^7.25.8", + "@babel/plugin-transform-runtime": "^7.25.7", + "@babel/preset-env": "^7.25.8", + "@babel/register": "^7.25.7", + "@babel/runtime": "^7.25.7", + "@wdio/cli": "^9.2.1", + "@wdio/local-runner": "^8.14.3", + "@wdio/mocha-framework": "^9.1.3", + "@wdio/spec-reporter": "^9.1.3", + "eslint": "^v8.57.1", "eslint-cli": "^1.1.1", - "eslint-config-standard": "^14.1.1", - "eslint-plugin-chai-friendly": "^0.7.2", - "eslint-plugin-import": "^2.25.4", - "eslint-plugin-json": "^3.1.0", + "eslint-config-standard": "^17.1.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-json": "^4.0.1", + "eslint-plugin-n": "^16.6.2", "eslint-plugin-node": "^11.1.0", - "eslint-plugin-promise": "^6.0.0", - "eslint-plugin-standard": "^4.0.1", + "eslint-plugin-promise": "^6.6.0", "json-web-key": "^0.4.0", - "jsrsasign": "^10.5.10", - "lint-staged": "^12.3.5", + "jsrsasign": "^11.0.0", "livereload": "^0.9.3", - "node-forge": "^1.2.1", - "node-jose": "^2.1.0", - "prettier": "^2.5.1", - "uuid": "^8.3.2", - "wdio-chromedriver-service": "^7.2.8", - "webdriverio": "^7.17.4" + "node-forge": "^1.3.1", + "node-jose": "^2.2.0", + "prettier": "^3.3.3", + "typescript": "^5.6.3", + "uuid": "^11.0.2", + "webdriverio": "^9.2.1" }, "prettier": {} } diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000000..e138b3cdb5 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,3936 @@ +# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. + +[[package]] +name = "astroid" +version = "3.3.9" +description = "An abstract syntax tree for Python with inference support." +optional = false +python-versions = ">=3.9.0" +groups = ["dev"] +files = [ + {file = "astroid-3.3.9-py3-none-any.whl", hash = "sha256:d05bfd0acba96a7bd43e222828b7d9bc1e138aaeb0649707908d3702a9831248"}, + {file = "astroid-3.3.9.tar.gz", hash = "sha256:622cc8e3048684aa42c820d9d218978021c3c3d174fb03a9f0d615921744f550"}, +] + +[[package]] +name = "attrs" +version = "25.3.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, + {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, +] + +[package.extras] +benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] +tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] + +[[package]] +name = "babel" +version = "2.14.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "Babel-2.14.0-py3-none-any.whl", hash = "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287"}, + {file = "Babel-2.14.0.tar.gz", hash = "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363"}, +] + +[package.extras] +dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] + +[[package]] +name = "beautifulsoup4" +version = "4.13.4" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.7.0" +groups = ["dev"] +files = [ + {file = "beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b"}, + {file = "beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195"}, +] + +[package.dependencies] +soupsieve = ">1.2" +typing-extensions = ">=4.0.0" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +name = "black" +version = "25.1.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32"}, + {file = "black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da"}, + {file = "black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7"}, + {file = "black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9"}, + {file = "black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0"}, + {file = "black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299"}, + {file = "black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096"}, + {file = "black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2"}, + {file = "black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b"}, + {file = "black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc"}, + {file = "black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f"}, + {file = "black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba"}, + {file = "black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f"}, + {file = "black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3"}, + {file = "black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171"}, + {file = "black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18"}, + {file = "black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0"}, + {file = "black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f"}, + {file = "black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e"}, + {file = "black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355"}, + {file = "black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717"}, + {file = "black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "blinker" +version = "1.9.0" +description = "Fast, simple object-to-object and broadcast signaling" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, + {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, +] + +[[package]] +name = "boto3" +version = "1.37.23" +description = "The AWS SDK for Python" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "boto3-1.37.23-py3-none-any.whl", hash = "sha256:fc462b9fd738bd8a1c121d94d237c6b6a05a2c1cc709d16f5223acb752f7310b"}, + {file = "boto3-1.37.23.tar.gz", hash = "sha256:82f4599a34f5eb66e916b9ac8547394f6e5899c19580e74b60237db04cf66d1e"}, +] + +[package.dependencies] +botocore = ">=1.37.23,<1.38.0" +jmespath = ">=0.7.1,<2.0.0" +s3transfer = ">=0.11.0,<0.12.0" + +[package.extras] +crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] + +[[package]] +name = "botocore" +version = "1.37.23" +description = "Low-level, data-driven core of boto 3." +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "botocore-1.37.23-py3-none-any.whl", hash = "sha256:ffbe1f5958adb1c50d72d3ad1018cb265fe349248c08782d334601c0814f0e38"}, + {file = "botocore-1.37.23.tar.gz", hash = "sha256:3a249c950cef9ee9ed7b2278500ad83a4ad6456bc433a43abd1864d1b61b2acb"}, +] + +[package.dependencies] +jmespath = ">=0.7.1,<2.0.0" +python-dateutil = ">=2.1,<3.0.0" +urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} + +[package.extras] +crt = ["awscrt (==0.23.8)"] + +[[package]] +name = "brotli" +version = "1.1.0" +description = "Python bindings for the Brotli compression library" +optional = false +python-versions = "*" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752"}, + {file = "Brotli-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e"}, + {file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2"}, + {file = "Brotli-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec"}, + {file = "Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2"}, + {file = "Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128"}, + {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc"}, + {file = "Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61"}, + {file = "Brotli-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0"}, + {file = "Brotli-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b"}, + {file = "Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50"}, + {file = "Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409"}, + {file = "Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408"}, + {file = "Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111"}, + {file = "Brotli-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839"}, + {file = "Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0"}, + {file = "Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951"}, + {file = "Brotli-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5"}, + {file = "Brotli-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8"}, + {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f"}, + {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648"}, + {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0"}, + {file = "Brotli-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089"}, + {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368"}, + {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c"}, + {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284"}, + {file = "Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7"}, + {file = "Brotli-1.1.0-cp313-cp313-win32.whl", hash = "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0"}, + {file = "Brotli-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b"}, + {file = "Brotli-1.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112"}, + {file = "Brotli-1.1.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:aea440a510e14e818e67bfc4027880e2fb500c2ccb20ab21c7a7c8b5b4703d75"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:6974f52a02321b36847cd19d1b8e381bf39939c21efd6ee2fc13a28b0d99348c"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:a7e53012d2853a07a4a79c00643832161a910674a893d296c9f1259859a289d2"}, + {file = "Brotli-1.1.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:d7702622a8b40c49bffb46e1e3ba2e81268d5c04a34f460978c6b5517a34dd52"}, + {file = "Brotli-1.1.0-cp36-cp36m-win32.whl", hash = "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460"}, + {file = "Brotli-1.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579"}, + {file = "Brotli-1.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a"}, + {file = "Brotli-1.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:cb1dac1770878ade83f2ccdf7d25e494f05c9165f5246b46a621cc849341dc01"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:3ee8a80d67a4334482d9712b8e83ca6b1d9bc7e351931252ebef5d8f7335a547"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:5e55da2c8724191e5b557f8e18943b1b4839b8efc3ef60d65985bcf6f587dd38"}, + {file = "Brotli-1.1.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:d342778ef319e1026af243ed0a07c97acf3bad33b9f29e7ae6a1f68fd083e90c"}, + {file = "Brotli-1.1.0-cp37-cp37m-win32.whl", hash = "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95"}, + {file = "Brotli-1.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68"}, + {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3"}, + {file = "Brotli-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48"}, + {file = "Brotli-1.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943"}, + {file = "Brotli-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a"}, + {file = "Brotli-1.1.0-cp38-cp38-win32.whl", hash = "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b"}, + {file = "Brotli-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0"}, + {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a"}, + {file = "Brotli-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac"}, + {file = "Brotli-1.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f"}, + {file = "Brotli-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb"}, + {file = "Brotli-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64"}, + {file = "Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467"}, + {file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"}, +] + +[[package]] +name = "brotlicffi" +version = "1.1.0.0" +description = "Python CFFI bindings to the Brotli library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +markers = "platform_python_implementation == \"PyPy\"" +files = [ + {file = "brotlicffi-1.1.0.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9b7ae6bd1a3f0df532b6d67ff674099a96d22bc0948955cb338488c31bfb8851"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19ffc919fa4fc6ace69286e0a23b3789b4219058313cf9b45625016bf7ff996b"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9feb210d932ffe7798ee62e6145d3a757eb6233aa9a4e7db78dd3690d7755814"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84763dbdef5dd5c24b75597a77e1b30c66604725707565188ba54bab4f114820"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-win32.whl", hash = "sha256:1b12b50e07c3911e1efa3a8971543e7648100713d4e0971b13631cce22c587eb"}, + {file = "brotlicffi-1.1.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:994a4f0681bb6c6c3b0925530a1926b7a189d878e6e5e38fae8efa47c5d9c613"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2e4aeb0bd2540cb91b069dbdd54d458da8c4334ceaf2d25df2f4af576d6766ca"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b7b0033b0d37bb33009fb2fef73310e432e76f688af76c156b3594389d81391"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54a07bb2374a1eba8ebb52b6fafffa2afd3c4df85ddd38fcc0511f2bb387c2a8"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7901a7dc4b88f1c1475de59ae9be59799db1007b7d059817948d8e4f12e24e35"}, + {file = "brotlicffi-1.1.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce01c7316aebc7fce59da734286148b1d1b9455f89cf2c8a4dfce7d41db55c2d"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:246f1d1a90279bb6069de3de8d75a8856e073b8ff0b09dcca18ccc14cec85979"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc4bc5d82bc56ebd8b514fb8350cfac4627d6b0743382e46d033976a5f80fab6"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37c26ecb14386a44b118ce36e546ce307f4810bc9598a6e6cb4f7fca725ae7e6"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca72968ae4eaf6470498d5c2887073f7efe3b1e7d7ec8be11a06a79cc810e990"}, + {file = "brotlicffi-1.1.0.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:add0de5b9ad9e9aa293c3aa4e9deb2b61e99ad6c1634e01d01d98c03e6a354cc"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9b6068e0f3769992d6b622a1cd2e7835eae3cf8d9da123d7f51ca9c1e9c333e5"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8557a8559509b61e65083f8782329188a250102372576093c88930c875a69838"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a7ae37e5d79c5bdfb5b4b99f2715a6035e6c5bf538c3746abc8e26694f92f33"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391151ec86bb1c683835980f4816272a87eaddc46bb91cbf44f62228b84d8cca"}, + {file = "brotlicffi-1.1.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:2f3711be9290f0453de8eed5275d93d286abe26b08ab4a35d7452caa1fef532f"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1a807d760763e398bbf2c6394ae9da5815901aa93ee0a37bca5efe78d4ee3171"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa8ca0623b26c94fccc3a1fdd895be1743b838f3917300506d04aa3346fd2a14"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3de0cf28a53a3238b252aca9fed1593e9d36c1d116748013339f0949bfc84112"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6be5ec0e88a4925c91f3dea2bb0013b3a2accda6f77238f76a34a1ea532a1cb0"}, + {file = "brotlicffi-1.1.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d9eb71bb1085d996244439154387266fd23d6ad37161f6f52f1cd41dd95a3808"}, + {file = "brotlicffi-1.1.0.0.tar.gz", hash = "sha256:b77827a689905143f87915310b93b273ab17888fd43ef350d4832c4a71083c13"}, +] + +[package.dependencies] +cffi = ">=1.0.0" + +[[package]] +name = "cachetools" +version = "5.5.2" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a"}, + {file = "cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4"}, +] + +[[package]] +name = "certifi" +version = "2025.1.31" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +groups = ["main", "dev"] +files = [ + {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, + {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, +] + +[[package]] +name = "cffi" +version = "1.17.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] +markers = {dev = "platform_python_implementation != \"PyPy\""} + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, + {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, + {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, +] + +[[package]] +name = "click" +version = "8.1.8" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev"] +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coloredlogs" +version = "15.0.1" +description = "Colored terminal output for Python's logging module" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["main"] +files = [ + {file = "coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934"}, + {file = "coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0"}, +] + +[package.dependencies] +humanfriendly = ">=9.1" + +[package.extras] +cron = ["capturer (>=2.4)"] + +[[package]] +name = "coverage" +version = "7.8.0" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe"}, + {file = "coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28"}, + {file = "coverage-7.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c8a5c139aae4c35cbd7cadca1df02ea8cf28a911534fc1b0456acb0b14234f3"}, + {file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a26c0c795c3e0b63ec7da6efded5f0bc856d7c0b24b2ac84b4d1d7bc578d676"}, + {file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821f7bcbaa84318287115d54becb1915eece6918136c6f91045bb84e2f88739d"}, + {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a321c61477ff8ee705b8a5fed370b5710c56b3a52d17b983d9215861e37b642a"}, + {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ed2144b8a78f9d94d9515963ed273d620e07846acd5d4b0a642d4849e8d91a0c"}, + {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:042e7841a26498fff7a37d6fda770d17519982f5b7d8bf5278d140b67b61095f"}, + {file = "coverage-7.8.0-cp310-cp310-win32.whl", hash = "sha256:f9983d01d7705b2d1f7a95e10bbe4091fabc03a46881a256c2787637b087003f"}, + {file = "coverage-7.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a570cd9bd20b85d1a0d7b009aaf6c110b52b5755c17be6962f8ccd65d1dbd23"}, + {file = "coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27"}, + {file = "coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea"}, + {file = "coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7"}, + {file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040"}, + {file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543"}, + {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2"}, + {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318"}, + {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9"}, + {file = "coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c"}, + {file = "coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78"}, + {file = "coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc"}, + {file = "coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6"}, + {file = "coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d"}, + {file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05"}, + {file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a"}, + {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6"}, + {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47"}, + {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe"}, + {file = "coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545"}, + {file = "coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b"}, + {file = "coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd"}, + {file = "coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00"}, + {file = "coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64"}, + {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067"}, + {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008"}, + {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733"}, + {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323"}, + {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3"}, + {file = "coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d"}, + {file = "coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487"}, + {file = "coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25"}, + {file = "coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42"}, + {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502"}, + {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1"}, + {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4"}, + {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73"}, + {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a"}, + {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883"}, + {file = "coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada"}, + {file = "coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257"}, + {file = "coverage-7.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa260de59dfb143af06dcf30c2be0b200bed2a73737a8a59248fcb9fa601ef0f"}, + {file = "coverage-7.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:96121edfa4c2dfdda409877ea8608dd01de816a4dc4a0523356067b305e4e17a"}, + {file = "coverage-7.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8af63b9afa1031c0ef05b217faa598f3069148eeee6bb24b79da9012423b82"}, + {file = "coverage-7.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89b1f4af0d4afe495cd4787a68e00f30f1d15939f550e869de90a86efa7e0814"}, + {file = "coverage-7.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94ec0be97723ae72d63d3aa41961a0b9a6f5a53ff599813c324548d18e3b9e8c"}, + {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8a1d96e780bdb2d0cbb297325711701f7c0b6f89199a57f2049e90064c29f6bd"}, + {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f1d8a2a57b47142b10374902777e798784abf400a004b14f1b0b9eaf1e528ba4"}, + {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cf60dd2696b457b710dd40bf17ad269d5f5457b96442f7f85722bdb16fa6c899"}, + {file = "coverage-7.8.0-cp39-cp39-win32.whl", hash = "sha256:be945402e03de47ba1872cd5236395e0f4ad635526185a930735f66710e1bd3f"}, + {file = "coverage-7.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:90e7fbc6216ecaffa5a880cdc9c77b7418c1dcb166166b78dbc630d07f278cc3"}, + {file = "coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd"}, + {file = "coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7"}, + {file = "coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501"}, +] + +[package.extras] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] + +[[package]] +name = "cramjam" +version = "2.9.1" +description = "Thin Python bindings to de/compression algorithms in Rust" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "cramjam-2.9.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:8e82464d1e00fbbb12958999b8471ba5e9f3d9711954505a0a7b378762332e6f"}, + {file = "cramjam-2.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d2df8a6511cc08ef1fccd2e0c65e2ebc9f57574ec8376052a76851af5398810"}, + {file = "cramjam-2.9.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:21ea784e6c3f1843d3523ae0f03651dd06058b39eeb64beb82ee3b100fa83662"}, + {file = "cramjam-2.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e0c5d98a4e791f0bbd0ffcb7dae879baeb2dcc357348a8dc2be0a8c10403a2a"}, + {file = "cramjam-2.9.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e076fd87089197cb61117c63dbe7712ad5eccb93968860eb3bae09b767bac813"}, + {file = "cramjam-2.9.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d86b44933aea0151e4a2e1e6935448499849045c38167d288ca4c59d5b8cd4e"}, + {file = "cramjam-2.9.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7eb032549dec897b942ddcf80c1cdccbcb40629f15fc902731dbe6362da49326"}, + {file = "cramjam-2.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf29b4def86ec503e329fe138842a9b79a997e3beb6c7809b05665a0d291edff"}, + {file = "cramjam-2.9.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a36adf7d13b7accfa206e1c917f08924eb905b45aa8e62176509afa7b14db71e"}, + {file = "cramjam-2.9.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:cf4ea758d98b6fad1b4b2d808d0de690d3162ac56c26968aea0af6524e3eb736"}, + {file = "cramjam-2.9.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4826d6d81ea490fa7a3ae7a4b9729866a945ffac1f77fe57b71e49d6e1b21efd"}, + {file = "cramjam-2.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:335103317475bf992953c58838152a4761fc3c87354000edbfc4d7e57cf05909"}, + {file = "cramjam-2.9.1-cp310-cp310-win32.whl", hash = "sha256:258120cb1e3afc3443f756f9de161ed63eed56a2c31f6093e81c571c0f2dc9f6"}, + {file = "cramjam-2.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:c60e5996aa02547d12bc2740d44e90e006b0f93100f53206f7abe6732ad56e69"}, + {file = "cramjam-2.9.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b9db1debe48060e41a5b91af9193c524e473c57f6105462c5524a41f5aabdb88"}, + {file = "cramjam-2.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f6f18f0242212d3409d26ce3874937b5b979cebd61f08b633a6ea893c32fc7b6"}, + {file = "cramjam-2.9.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b5b1cd7d39242b2b903cf09cd4696b3a6e04dc537ffa9f3ac8668edae76eecb6"}, + {file = "cramjam-2.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47de0a68f5f4d9951250ef5af31f2a7228132caa9ed60994234f7eb98090d33"}, + {file = "cramjam-2.9.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e13c9a697881e5e38148958612dc6856967f5ff8cd7bba5ff751f2d6ac020aa4"}, + {file = "cramjam-2.9.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba560244bc1335b420b74e91e35f9d4e7f307a3be3a4603ce0f0d7e15a0acdf0"}, + {file = "cramjam-2.9.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d47fd41ce260cf4f0ff0e788de961fab9e9c6844a05ce55d06ce31e06107bdc"}, + {file = "cramjam-2.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84d154fbadece82935396eb6bcb502085d944d2fd13b07a94348364344370c2c"}, + {file = "cramjam-2.9.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:038df668ffb94d64d67b6ecc59cbd206745a425ffc0402897dde12d89fa6a870"}, + {file = "cramjam-2.9.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:4125d8cd86fa08495d310e80926c2f0563f157b76862e7479f9b2cf94823ea0c"}, + {file = "cramjam-2.9.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4206ebdd1d1ef0f3f86c8c2f7c426aa4af6094f4f41e274601fd4c4569f37454"}, + {file = "cramjam-2.9.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ab687bef5c493732b9a4ab870542ee43f5eae0025f9c684c7cb399c3a85cb380"}, + {file = "cramjam-2.9.1-cp311-cp311-win32.whl", hash = "sha256:dda7698b6d7caeae1047adafebc4b43b2a82478234f6c2b45bc3edad854e0600"}, + {file = "cramjam-2.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:872b00ff83e84bcbdc7e951af291ebe65eed20b09c47e7c4af21c312f90b796f"}, + {file = "cramjam-2.9.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:79417957972553502b217a0093532e48893c8b4ca30ccc941cefe9c72379df7c"}, + {file = "cramjam-2.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce2b94117f373defc876f88e74e44049a9969223dbca3240415b71752d0422fb"}, + {file = "cramjam-2.9.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:67040e0fd84404885ec716a806bee6110f9960c3647e0ef1670aab3b7375a70a"}, + {file = "cramjam-2.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bedb84e068b53c944bd08dcb501fd00d67daa8a917922356dd559b484ce7eab"}, + {file = "cramjam-2.9.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:06e3f97a379386d97debf08638a78b3d3850fdf6124755eb270b54905a169930"}, + {file = "cramjam-2.9.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11118675e9c7952ececabc62f023290ee4f8ecf0bee0d2c7eb8d1c402ee9769d"}, + {file = "cramjam-2.9.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b7de6b61b11545570e4d6033713f3599525efc615ee353a822be8f6b0c65b77"}, + {file = "cramjam-2.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57ca8f3775324a9de3ee6f05ca172687ba258c0dea79f7e3a6b4112834982f2a"}, + {file = "cramjam-2.9.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9847dd6f288f1c56359f52acb48ff2df848ff3e3bff34d23855bbcf7016427cc"}, + {file = "cramjam-2.9.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d1248dfa7f151e893ce819670f00879e4b7650b8d4c01279ce4f12140d68dd2"}, + {file = "cramjam-2.9.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9da6d970281083bae91b914362de325414aa03c01fc806f6bb2cc006322ec834"}, + {file = "cramjam-2.9.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1c33bc095db5733c841a102b8693062be5db8cdac17b9782ebc00577c6a94480"}, + {file = "cramjam-2.9.1-cp312-cp312-win32.whl", hash = "sha256:9e9193cd4bb57e7acd3af24891526299244bfed88168945efdaa09af4e50720f"}, + {file = "cramjam-2.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:15955dd75e80f66c1ea271167a5347661d9bdc365f894a57698c383c9b7d465c"}, + {file = "cramjam-2.9.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5a7797a2fff994fc5e323f7a967a35a3e37e3006ed21d64dcded086502f482af"}, + {file = "cramjam-2.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d51b9b140b1df39a44bff7896d98a10da345b7d5f5ce92368d328c1c2c829167"}, + {file = "cramjam-2.9.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:07ac76b7f992556e7aa910244be11ece578cdf84f4d5d5297461f9a895e18312"}, + {file = "cramjam-2.9.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d90a72608c7550cd7eba914668f6277bfb0b24f074d1f1bd9d061fcb6f2adbd6"}, + {file = "cramjam-2.9.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56495975401b1821dbe1f29cf222e23556232209a2fdb809fe8156d120ca9c7f"}, + {file = "cramjam-2.9.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b695259e71fde6d5be66b77a4474523ced9ffe9fe8a34cb9b520ec1241a14d3"}, + {file = "cramjam-2.9.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab1e69dc4831bbb79b6d547077aae89074c83e8ad94eba1a3d80e94d2424fd02"}, + {file = "cramjam-2.9.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:440b489902bfb7a26d3fec1ca888007615336ff763d2a32a2fc40586548a0dbf"}, + {file = "cramjam-2.9.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:217fe22b41f8c3dce03852f828b059abfad11d1344a1df2f43d3eb8634b18d75"}, + {file = "cramjam-2.9.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:95f3646ddc98af25af25d5692ae65966488a283813336ea9cf41b22e542e7c0d"}, + {file = "cramjam-2.9.1-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:6b19fc60ead1cae9795a5b359599da3a1c95d38f869bdfb51c441fd76b04e926"}, + {file = "cramjam-2.9.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:8dc5207567459d049696f62a1fdfb220f3fe6aa0d722285d44753e12504dac6c"}, + {file = "cramjam-2.9.1-cp313-cp313-win32.whl", hash = "sha256:fbfe35929a61b914de9e5dbacde0cfbba86cbf5122f9285a24c14ed0b645490b"}, + {file = "cramjam-2.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:06068bd191a82ad4fc1ac23d6f8627fb5e37ec4be0431711b9a2dbacaccfeddb"}, + {file = "cramjam-2.9.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:6a2ca4d3c683d28d3217821029eb08d3487d5043d7eb455df11ff3cacfd4c916"}, + {file = "cramjam-2.9.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:008b49b455b396acc5459dfb06fb9d56049c4097ee8e590892a4d3da9a711da3"}, + {file = "cramjam-2.9.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:45c18cc13156e8697a8d3f9e57e49a69b00e14a103196efab0893fae1a5257f8"}, + {file = "cramjam-2.9.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d14a0efb21e0fec0631bcd66040b06e6a0fe10825f3aacffded38c1c978bdff9"}, + {file = "cramjam-2.9.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3f815fb0eba625af45139af4f90f5fc2ddda61b171c2cc3ab63d44b40c5c7768"}, + {file = "cramjam-2.9.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04828cbfad7384f06a4a7d0d927c3e85ef11dc5a40b9cf5f3e29ac4e23ecd678"}, + {file = "cramjam-2.9.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0944a7c3a78f940c06d1b29bdce91a17798d80593dd01ebfeb842761e48a8b5"}, + {file = "cramjam-2.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec769e5b16251704502277a1163dcf2611551452d7590ff4cc422b7b0367fc96"}, + {file = "cramjam-2.9.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3ba79c7d2cc5adb897b690c05dd9b67c4d401736d207314b99315f7be3cd94fd"}, + {file = "cramjam-2.9.1-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d35923fb5411bde30b53c0696dff8e24c8a38b010b89544834c53f4462fd71df"}, + {file = "cramjam-2.9.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:da0cc0efdbfb8ee2361f89f38ded03d11678f37e392afff7a97b09c55dadfc83"}, + {file = "cramjam-2.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f89924858712b8b936f04f3d690e72825a3e5127a140b434c79030c1c5a887ce"}, + {file = "cramjam-2.9.1-cp38-cp38-win32.whl", hash = "sha256:5925a738b8478f223ab9756fc794e3cabd5917fd7846f66adcf1d5fc2bf9864c"}, + {file = "cramjam-2.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:b7ac273498a2c6772d67707e101b74014c0d9413bb4711c51d8ec311de59b4b1"}, + {file = "cramjam-2.9.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:af39006faddfc6253beb93ca821d544931cfee7f0177b99ff106dfd8fd6a2cd8"}, + {file = "cramjam-2.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b3291be0d3f73d5774d69013be4ab33978c777363b5312d14f62f77817c2f75a"}, + {file = "cramjam-2.9.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1539fd758f0e57fad7913cebff8baaee871bb561ddf6fa710a427b74da6b6778"}, + {file = "cramjam-2.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff362f68bd68ac0eccb445209238d589bba728fb6d7f2e9dc199e0ec3a61d6e0"}, + {file = "cramjam-2.9.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:23b9786d1d17686fb8d600ade2a19374c7188d4b8867efa9af0d8274a220aec7"}, + {file = "cramjam-2.9.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8bc9c2c748aaf91863d89c4583f529c1c709485c94f8dfeb3ee48662d88e3258"}, + {file = "cramjam-2.9.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd0fa9a0e7f18224b6d2d1d69dbdc3aecec80ef1393c59244159b131604a4395"}, + {file = "cramjam-2.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ceef6e09ee22457997370882aa3c69de01e6dd0aaa2f953e1e87ad11641d042"}, + {file = "cramjam-2.9.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1376f6fdbf0b30712413a0b4e51663a4938ae2f6b449f8e4635dbb3694db83cf"}, + {file = "cramjam-2.9.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:342fb946f8d3e9e35b837288b03ab23cfbe0bb5a30e582ed805ef79706823a96"}, + {file = "cramjam-2.9.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a237064a6e2c2256c9a1cf2beb7c971382190c0f1eb2e810e02e971881756132"}, + {file = "cramjam-2.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53145fc9f2319c1245d4329e1da8cfacd6e35e27090c07c0b9d453ae2bbdac3e"}, + {file = "cramjam-2.9.1-cp39-cp39-win32.whl", hash = "sha256:8a9f52c27292c21457f43c4ce124939302a9acfb62295e7cda8667310563a5a3"}, + {file = "cramjam-2.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:8097ee39b61c86848a443c0b25b2df1de6b331fd512b20836a4f5cfde51ab255"}, + {file = "cramjam-2.9.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:86824c695688fcd06c5ac9bbd3fea9bdfb4cca194b1e706fbf11a629df48d2b4"}, + {file = "cramjam-2.9.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:27571bfa5a5d618604696747d0dc1d2a99b5906c967c8dee53c13a7107edfde6"}, + {file = "cramjam-2.9.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb01f6e38719818778144d3165a89ea1ad9dc58c6342b7f20aa194c70f34cbd1"}, + {file = "cramjam-2.9.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b5cef5cf40725fe64592af9ec163e7389855077700678a1d94bec549403a74d"}, + {file = "cramjam-2.9.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ac48b978aa0675f62b642750e798c394a64d25ce852e4e541f69bef9a564c2f0"}, + {file = "cramjam-2.9.1.tar.gz", hash = "sha256:336cc591d86cbd225d256813779f46624f857bc9c779db126271eff9ddc524ae"}, +] + +[package.extras] +dev = ["black (==22.3.0)", "hypothesis", "numpy", "pytest (>=5.30)", "pytest-benchmark", "pytest-xdist"] + +[[package]] +name = "cryptography" +version = "44.0.2" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = "!=3.9.0,!=3.9.1,>=3.7" +groups = ["main", "dev"] +files = [ + {file = "cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7"}, + {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1"}, + {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb"}, + {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843"}, + {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5"}, + {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c"}, + {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a"}, + {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308"}, + {file = "cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688"}, + {file = "cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7"}, + {file = "cryptography-44.0.2-cp37-abi3-win32.whl", hash = "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79"}, + {file = "cryptography-44.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa"}, + {file = "cryptography-44.0.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3"}, + {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639"}, + {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd"}, + {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181"}, + {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea"}, + {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699"}, + {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9"}, + {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23"}, + {file = "cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922"}, + {file = "cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4"}, + {file = "cryptography-44.0.2-cp39-abi3-win32.whl", hash = "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5"}, + {file = "cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6"}, + {file = "cryptography-44.0.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:af4ff3e388f2fa7bff9f7f2b31b87d5651c45731d3e8cfa0944be43dff5cfbdb"}, + {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0529b1d5a0105dd3731fa65680b45ce49da4d8115ea76e9da77a875396727b41"}, + {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7ca25849404be2f8e4b3c59483d9d3c51298a22c1c61a0e84415104dacaf5562"}, + {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:268e4e9b177c76d569e8a145a6939eca9a5fec658c932348598818acf31ae9a5"}, + {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9eb9d22b0a5d8fd9925a7764a054dca914000607dff201a24c791ff5c799e1fa"}, + {file = "cryptography-44.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2bf7bf75f7df9715f810d1b038870309342bff3069c5bd8c6b96128cb158668d"}, + {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:909c97ab43a9c0c0b0ada7a1281430e4e5ec0458e6d9244c0e821bbf152f061d"}, + {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96e7a5e9d6e71f9f4fca8eebfd603f8e86c5225bb18eb621b2c1e50b290a9471"}, + {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d1b3031093a366ac767b3feb8bcddb596671b3aaff82d4050f984da0c248b615"}, + {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:04abd71114848aa25edb28e225ab5f268096f44cf0127f3d36975bdf1bdf3390"}, + {file = "cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0) ; python_version >= \"3.8\""] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_version >= \"3.8\""] +pep8test = ["check-sdist ; python_version >= \"3.8\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] +sdist = ["build (>=1.0.0)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi (>=2024)", "cryptography-vectors (==44.0.2)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "cssbeautifier" +version = "1.15.4" +description = "CSS unobfuscator and beautifier." +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "cssbeautifier-1.15.4-py3-none-any.whl", hash = "sha256:78c84d5e5378df7d08622bbd0477a1abdbd209680e95480bf22f12d5701efc98"}, + {file = "cssbeautifier-1.15.4.tar.gz", hash = "sha256:9bb08dc3f64c101a01677f128acf01905914cf406baf87434dcde05b74c0acf5"}, +] + +[package.dependencies] +editorconfig = ">=0.12.2" +jsbeautifier = "*" +six = ">=1.13.0" + +[[package]] +name = "deprecated" +version = "1.2.18" +description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +groups = ["main"] +files = [ + {file = "Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec"}, + {file = "deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d"}, +] + +[package.dependencies] +wrapt = ">=1.10,<2" + +[package.extras] +dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "setuptools ; python_version >= \"3.12\"", "tox"] + +[[package]] +name = "dill" +version = "0.3.9" +description = "serialize all of Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "dill-0.3.9-py3-none-any.whl", hash = "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a"}, + {file = "dill-0.3.9.tar.gz", hash = "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c"}, +] + +[package.extras] +graph = ["objgraph (>=1.7.2)"] +profile = ["gprof2dot (>=2022.7.29)"] + +[[package]] +name = "djlint" +version = "1.36.4" +description = "HTML Template Linter and Formatter" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "djlint-1.36.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a2dfb60883ceb92465201bfd392291a7597c6752baede6fbb6f1980cac8d6c5c"}, + {file = "djlint-1.36.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4bc6a1320c0030244b530ac200642f883d3daa451a115920ef3d56d08b644292"}, + {file = "djlint-1.36.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3164a048c7bb0baf042387b1e33f9bbbf99d90d1337bb4c3d66eb0f96f5400a1"}, + {file = "djlint-1.36.4-cp310-cp310-win_amd64.whl", hash = "sha256:3196d5277da5934962d67ad6c33a948ba77a7b6eadf064648bef6ee5f216b03c"}, + {file = "djlint-1.36.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d68da0ed10ee9ca1e32e225cbb8e9b98bf7e6f8b48a8e4836117b6605b88cc7"}, + {file = "djlint-1.36.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c0478d5392247f1e6ee29220bbdbf7fb4e1bc0e7e83d291fda6fb926c1787ba7"}, + {file = "djlint-1.36.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:962f7b83aee166e499eff916d631c6dde7f1447d7610785a60ed2a75a5763483"}, + {file = "djlint-1.36.4-cp311-cp311-win_amd64.whl", hash = "sha256:53cbc450aa425c832f09bc453b8a94a039d147b096740df54a3547fada77ed08"}, + {file = "djlint-1.36.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff9faffd7d43ac20467493fa71d5355b5b330a00ade1c4d1e859022f4195223b"}, + {file = "djlint-1.36.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:79489e262b5ac23a8dfb7ca37f1eea979674cfc2d2644f7061d95bea12c38f7e"}, + {file = "djlint-1.36.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e58c5fa8c6477144a0be0a87273706a059e6dd0d6efae01146ae8c29cdfca675"}, + {file = "djlint-1.36.4-cp312-cp312-win_amd64.whl", hash = "sha256:bb6903777bf3124f5efedcddf1f4716aef097a7ec4223fc0fa54b865829a6e08"}, + {file = "djlint-1.36.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ead475013bcac46095b1bbc8cf97ed2f06e83422335734363f8a76b4ba7e47c2"}, + {file = "djlint-1.36.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6c601dfa68ea253311deb4a29a7362b7a64933bdfcfb5a06618f3e70ad1fa835"}, + {file = "djlint-1.36.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bda5014f295002363381969864addeb2db13955f1b26e772657c3b273ed7809f"}, + {file = "djlint-1.36.4-cp313-cp313-win_amd64.whl", hash = "sha256:16ce37e085afe5a30953b2bd87cbe34c37843d94c701fc68a2dda06c1e428ff4"}, + {file = "djlint-1.36.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:89678661888c03d7bc6cadd75af69db29962b5ecbf93a81518262f5c48329f04"}, + {file = "djlint-1.36.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b01a98df3e1ab89a552793590875bc6e954cad661a9304057db75363d519fa0"}, + {file = "djlint-1.36.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dabbb4f7b93223d471d09ae34ed515fef98b2233cbca2449ad117416c44b1351"}, + {file = "djlint-1.36.4-cp39-cp39-win_amd64.whl", hash = "sha256:7a483390d17e44df5bc23dcea29bdf6b63f3ed8b4731d844773a4829af4f5e0b"}, + {file = "djlint-1.36.4-py3-none-any.whl", hash = "sha256:e9699b8ac3057a6ed04fb90835b89bee954ed1959c01541ce4f8f729c938afdd"}, + {file = "djlint-1.36.4.tar.gz", hash = "sha256:17254f218b46fe5a714b224c85074c099bcb74e3b2e1f15c2ddc2cf415a408a1"}, +] + +[package.dependencies] +click = ">=8.0.1" +colorama = ">=0.4.4" +cssbeautifier = ">=1.14.4" +jsbeautifier = ">=1.14.4" +json5 = ">=0.9.11" +pathspec = ">=0.12" +pyyaml = ">=6" +regex = ">=2023" +tqdm = ">=4.62.2" + +[[package]] +name = "dnspython" +version = "2.7.0" +description = "DNS toolkit" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86"}, + {file = "dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1"}, +] + +[package.extras] +dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "hypercorn (>=0.16.0)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "quart-trio (>=0.11.0)", "sphinx (>=7.2.0)", "sphinx-rtd-theme (>=2.0.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] +dnssec = ["cryptography (>=43)"] +doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] +doq = ["aioquic (>=1.0.0)"] +idna = ["idna (>=3.7)"] +trio = ["trio (>=0.23)"] +wmi = ["wmi (>=1.5.1)"] + +[[package]] +name = "editorconfig" +version = "0.17.0" +description = "EditorConfig File Locator and Interpreter for Python" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "EditorConfig-0.17.0-py3-none-any.whl", hash = "sha256:fe491719c5f65959ec00b167d07740e7ffec9a3f362038c72b289330b9991dfc"}, + {file = "editorconfig-0.17.0.tar.gz", hash = "sha256:8739052279699840065d3a9f5c125d7d5a98daeefe53b0e5274261d77cb49aa2"}, +] + +[[package]] +name = "email-validator" +version = "2.2.0" +description = "A robust email address syntax and deliverability validation library." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"}, + {file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"}, +] + +[package.dependencies] +dnspython = ">=2.0.0" +idna = ">=2.0.0" + +[[package]] +name = "execnet" +version = "2.1.1" +description = "execnet: rapid multi-Python deployment" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, + {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, +] + +[package.extras] +testing = ["hatch", "pre-commit", "pytest", "tox"] + +[[package]] +name = "fakeredis" +version = "2.29.0" +description = "Python implementation of redis API, can be used for testing purposes." +optional = false +python-versions = "<4.0,>=3.7" +groups = ["dev"] +files = [ + {file = "fakeredis-2.29.0-py3-none-any.whl", hash = "sha256:f644c0a69dc088455d75a9b259d101e28a1c5659381aa6d9ee6c2b31eb5a909f"}, + {file = "fakeredis-2.29.0.tar.gz", hash = "sha256:159cebf2c53e2c2bd7d18220fa93aa5f1d7152f6b6dd7896c46234d674342398"}, +] + +[package.dependencies] +redis = {version = ">=4.3", markers = "python_full_version > \"3.8.0\""} +sortedcontainers = ">=2,<3" + +[package.extras] +bf = ["pyprobables (>=0.6,<0.7)"] +cf = ["pyprobables (>=0.6,<0.7)"] +json = ["jsonpath-ng (>=1.6,<2.0)"] +lua = ["lupa (>=2.1,<3.0)"] +probabilistic = ["pyprobables (>=0.6,<0.7)"] + +[[package]] +name = "flask" +version = "3.1.0" +description = "A simple framework for building complex web applications." +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "flask-3.1.0-py3-none-any.whl", hash = "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136"}, + {file = "flask-3.1.0.tar.gz", hash = "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac"}, +] + +[package.dependencies] +blinker = ">=1.9" +click = ">=8.1.3" +itsdangerous = ">=2.2" +Jinja2 = ">=3.1.2" +Werkzeug = ">=3.1" + +[package.extras] +async = ["asgiref (>=3.2)"] +dotenv = ["python-dotenv"] + +[[package]] +name = "flask-babel" +version = "4.0.0" +description = "Adds i18n/l10n support for Flask applications." +optional = false +python-versions = ">=3.8,<4.0" +groups = ["main"] +files = [ + {file = "flask_babel-4.0.0-py3-none-any.whl", hash = "sha256:638194cf91f8b301380f36d70e2034c77ee25b98cb5d80a1626820df9a6d4625"}, + {file = "flask_babel-4.0.0.tar.gz", hash = "sha256:dbeab4027a3f4a87678a11686496e98e1492eb793cbdd77ab50f4e9a2602a593"}, +] + +[package.dependencies] +Babel = ">=2.12" +Flask = ">=2.0" +Jinja2 = ">=3.1" +pytz = ">=2022.7" + +[[package]] +name = "flask-compress" +version = "1.17" +description = "Compress responses in your Flask app with gzip, deflate, brotli or zstandard." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "Flask_Compress-1.17-py3-none-any.whl", hash = "sha256:415131f197c41109f08e8fdfc3a6628d83d81680fb5ecd0b3a97410e02397b20"}, + {file = "flask_compress-1.17.tar.gz", hash = "sha256:1ebb112b129ea7c9e7d6ee6d5cc0d64f226cbc50c4daddf1a58b9bd02253fbd8"}, +] + +[package.dependencies] +brotli = {version = "*", markers = "platform_python_implementation != \"PyPy\""} +brotlicffi = {version = "*", markers = "platform_python_implementation == \"PyPy\""} +flask = "*" +zstandard = [ + {version = "*", markers = "platform_python_implementation != \"PyPy\""}, + {version = "*", extras = ["cffi"], markers = "platform_python_implementation == \"PyPy\""}, +] + +[[package]] +name = "flask-login" +version = "0.6.3" +description = "User authentication and session management for Flask." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "Flask-Login-0.6.3.tar.gz", hash = "sha256:5e23d14a607ef12806c699590b89d0f0e0d67baeec599d75947bf9c147330333"}, + {file = "Flask_Login-0.6.3-py3-none-any.whl", hash = "sha256:849b25b82a436bf830a054e74214074af59097171562ab10bfa999e6b78aae5d"}, +] + +[package.dependencies] +Flask = ">=1.0.4" +Werkzeug = ">=1.0.1" + +[[package]] +name = "flask-talisman" +version = "1.1.0" +description = "HTTP security headers for Flask." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "flask-talisman-1.1.0.tar.gz", hash = "sha256:c5f486f5f54420729f84b3c3850cd63f96e8b033a9629bee66c524ea363797ff"}, + {file = "flask_talisman-1.1.0-py2.py3-none-any.whl", hash = "sha256:3c42b610ebe49b0e35ca150e179bf51aa1da01e4635b49a674868ea681046208"}, +] + +[[package]] +name = "flask-wtf" +version = "1.2.2" +description = "Form rendering, validation, and CSRF protection for Flask with WTForms." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "flask_wtf-1.2.2-py3-none-any.whl", hash = "sha256:e93160c5c5b6b571cf99300b6e01b72f9a101027cab1579901f8b10c5daf0b70"}, + {file = "flask_wtf-1.2.2.tar.gz", hash = "sha256:79d2ee1e436cf570bccb7d916533fa18757a2f18c290accffab1b9a0b684666b"}, +] + +[package.dependencies] +flask = "*" +itsdangerous = "*" +wtforms = "*" + +[package.extras] +email = ["email-validator"] + +[[package]] +name = "freezegun" +version = "1.5.2" +description = "Let your Python tests travel through time" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "freezegun-1.5.2-py3-none-any.whl", hash = "sha256:5aaf3ba229cda57afab5bd311f0108d86b6fb119ae89d2cd9c43ec8c1733c85b"}, + {file = "freezegun-1.5.2.tar.gz", hash = "sha256:a54ae1d2f9c02dbf42e02c18a3ab95ab4295818b549a34dac55592d72a905181"}, +] + +[package.dependencies] +python-dateutil = ">=2.7" + +[[package]] +name = "gevent" +version = "24.11.1" +description = "Coroutine-based network library" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "gevent-24.11.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:92fe5dfee4e671c74ffaa431fd7ffd0ebb4b339363d24d0d944de532409b935e"}, + {file = "gevent-24.11.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7bfcfe08d038e1fa6de458891bca65c1ada6d145474274285822896a858c870"}, + {file = "gevent-24.11.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7398c629d43b1b6fd785db8ebd46c0a353880a6fab03d1cf9b6788e7240ee32e"}, + {file = "gevent-24.11.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7886b63ebfb865178ab28784accd32f287d5349b3ed71094c86e4d3ca738af5"}, + {file = "gevent-24.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9ca80711e6553880974898d99357fb649e062f9058418a92120ca06c18c3c59"}, + {file = "gevent-24.11.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e24181d172f50097ac8fc272c8c5b030149b630df02d1c639ee9f878a470ba2b"}, + {file = "gevent-24.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1d4fadc319b13ef0a3c44d2792f7918cf1bca27cacd4d41431c22e6b46668026"}, + {file = "gevent-24.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:3d882faa24f347f761f934786dde6c73aa6c9187ee710189f12dcc3a63ed4a50"}, + {file = "gevent-24.11.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:351d1c0e4ef2b618ace74c91b9b28b3eaa0dd45141878a964e03c7873af09f62"}, + {file = "gevent-24.11.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5efe72e99b7243e222ba0c2c2ce9618d7d36644c166d63373af239da1036bab"}, + {file = "gevent-24.11.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d3b249e4e1f40c598ab8393fc01ae6a3b4d51fc1adae56d9ba5b315f6b2d758"}, + {file = "gevent-24.11.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81d918e952954675f93fb39001da02113ec4d5f4921bf5a0cc29719af6824e5d"}, + {file = "gevent-24.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9c935b83d40c748b6421625465b7308d87c7b3717275acd587eef2bd1c39546"}, + {file = "gevent-24.11.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff96c5739834c9a594db0e12bf59cb3fa0e5102fc7b893972118a3166733d61c"}, + {file = "gevent-24.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d6c0a065e31ef04658f799215dddae8752d636de2bed61365c358f9c91e7af61"}, + {file = "gevent-24.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:97e2f3999a5c0656f42065d02939d64fffaf55861f7d62b0107a08f52c984897"}, + {file = "gevent-24.11.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:a3d75fa387b69c751a3d7c5c3ce7092a171555126e136c1d21ecd8b50c7a6e46"}, + {file = "gevent-24.11.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:beede1d1cff0c6fafae3ab58a0c470d7526196ef4cd6cc18e7769f207f2ea4eb"}, + {file = "gevent-24.11.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:85329d556aaedced90a993226d7d1186a539c843100d393f2349b28c55131c85"}, + {file = "gevent-24.11.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:816b3883fa6842c1cf9d2786722014a0fd31b6312cca1f749890b9803000bad6"}, + {file = "gevent-24.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b24d800328c39456534e3bc3e1684a28747729082684634789c2f5a8febe7671"}, + {file = "gevent-24.11.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a5f1701ce0f7832f333dd2faf624484cbac99e60656bfbb72504decd42970f0f"}, + {file = "gevent-24.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d740206e69dfdfdcd34510c20adcb9777ce2cc18973b3441ab9767cd8948ca8a"}, + {file = "gevent-24.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:68bee86b6e1c041a187347ef84cf03a792f0b6c7238378bf6ba4118af11feaae"}, + {file = "gevent-24.11.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:d618e118fdb7af1d6c1a96597a5cd6ac84a9f3732b5be8515c6a66e098d498b6"}, + {file = "gevent-24.11.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2142704c2adce9cd92f6600f371afb2860a446bfd0be5bd86cca5b3e12130766"}, + {file = "gevent-24.11.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92e0d7759de2450a501effd99374256b26359e801b2d8bf3eedd3751973e87f5"}, + {file = "gevent-24.11.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca845138965c8c56d1550499d6b923eb1a2331acfa9e13b817ad8305dde83d11"}, + {file = "gevent-24.11.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:356b73d52a227d3313f8f828025b665deada57a43d02b1cf54e5d39028dbcf8d"}, + {file = "gevent-24.11.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:58851f23c4bdb70390f10fc020c973ffcf409eb1664086792c8b1e20f25eef43"}, + {file = "gevent-24.11.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1ea50009ecb7f1327347c37e9eb6561bdbc7de290769ee1404107b9a9cba7cf1"}, + {file = "gevent-24.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:ec68e270543ecd532c4c1d70fca020f90aa5486ad49c4f3b8b2e64a66f5c9274"}, + {file = "gevent-24.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9347690f4e53de2c4af74e62d6fabc940b6d4a6cad555b5a379f61e7d3f2a8e"}, + {file = "gevent-24.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8619d5c888cb7aebf9aec6703e410620ef5ad48cdc2d813dd606f8aa7ace675f"}, + {file = "gevent-24.11.1-cp39-cp39-win32.whl", hash = "sha256:c6b775381f805ff5faf250e3a07c0819529571d19bb2a9d474bee8c3f90d66af"}, + {file = "gevent-24.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:1c3443b0ed23dcb7c36a748d42587168672953d368f2956b17fad36d43b58836"}, + {file = "gevent-24.11.1-pp310-pypy310_pp73-macosx_11_0_universal2.whl", hash = "sha256:f43f47e702d0c8e1b8b997c00f1601486f9f976f84ab704f8f11536e3fa144c9"}, + {file = "gevent-24.11.1.tar.gz", hash = "sha256:8bd1419114e9e4a3ed33a5bad766afff9a3cf765cb440a582a1b3a9bc80c1aca"}, +] + +[package.dependencies] +cffi = {version = ">=1.17.1", markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""} +greenlet = {version = ">=3.1.1", markers = "platform_python_implementation == \"CPython\""} +"zope.event" = "*" +"zope.interface" = "*" + +[package.extras] +dnspython = ["dnspython (>=1.16.0,<2.0) ; python_version < \"3.10\"", "idna ; python_version < \"3.10\""] +docs = ["furo", "repoze.sphinx.autointerface", "sphinx", "sphinxcontrib-programoutput", "zope.schema"] +monitor = ["psutil (>=5.7.0) ; sys_platform != \"win32\" or platform_python_implementation == \"CPython\""] +recommended = ["cffi (>=1.17.1) ; platform_python_implementation == \"CPython\"", "dnspython (>=1.16.0,<2.0) ; python_version < \"3.10\"", "idna ; python_version < \"3.10\"", "psutil (>=5.7.0) ; sys_platform != \"win32\" or platform_python_implementation == \"CPython\""] +test = ["cffi (>=1.17.1) ; platform_python_implementation == \"CPython\"", "coverage (>=5.0) ; sys_platform != \"win32\"", "dnspython (>=1.16.0,<2.0) ; python_version < \"3.10\"", "idna ; python_version < \"3.10\"", "objgraph", "psutil (>=5.7.0) ; sys_platform != \"win32\" or platform_python_implementation == \"CPython\"", "requests"] + +[[package]] +name = "google-api-core" +version = "2.24.2" +description = "Google API client core library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "google_api_core-2.24.2-py3-none-any.whl", hash = "sha256:810a63ac95f3c441b7c0e43d344e372887f62ce9071ba972eacf32672e072de9"}, + {file = "google_api_core-2.24.2.tar.gz", hash = "sha256:81718493daf06d96d6bc76a91c23874dbf2fac0adbbf542831b805ee6e974696"}, +] + +[package.dependencies] +google-auth = ">=2.14.1,<3.0.0" +googleapis-common-protos = ">=1.56.2,<2.0.0" +grpcio = {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} +grpcio-status = {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} +proto-plus = [ + {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, + {version = ">=1.22.3,<2.0.0"}, +] +protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" +requests = ">=2.18.0,<3.0.0" + +[package.extras] +async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.dev0)"] +grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev) ; python_version >= \"3.11\"", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0) ; python_version >= \"3.11\""] +grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] +grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] + +[[package]] +name = "google-auth" +version = "2.38.0" +description = "Google Authentication Library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "google_auth-2.38.0-py2.py3-none-any.whl", hash = "sha256:e7dae6694313f434a2727bf2906f27ad259bae090d7aa896590d86feec3d9d4a"}, + {file = "google_auth-2.38.0.tar.gz", hash = "sha256:8285113607d3b80a3f1543b75962447ba8a09fe85783432a784fdeef6ac094c4"}, +] + +[package.dependencies] +cachetools = ">=2.0.0,<6.0" +pyasn1-modules = ">=0.2.1" +rsa = ">=3.1.4,<5" + +[package.extras] +aiohttp = ["aiohttp (>=3.6.2,<4.0.0.dev0)", "requests (>=2.20.0,<3.0.0.dev0)"] +enterprise-cert = ["cryptography", "pyopenssl"] +pyjwt = ["cryptography (>=38.0.3)", "pyjwt (>=2.0)"] +pyopenssl = ["cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] +reauth = ["pyu2f (>=0.1.5)"] +requests = ["requests (>=2.20.0,<3.0.0.dev0)"] + +[[package]] +name = "google-cloud-core" +version = "2.4.3" +description = "Google Cloud API client core library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "google_cloud_core-2.4.3-py2.py3-none-any.whl", hash = "sha256:5130f9f4c14b4fafdff75c79448f9495cfade0d8775facf1b09c3bf67e027f6e"}, + {file = "google_cloud_core-2.4.3.tar.gz", hash = "sha256:1fab62d7102844b278fe6dead3af32408b1df3eb06f5c7e8634cbd40edc4da53"}, +] + +[package.dependencies] +google-api-core = ">=1.31.6,<2.0.dev0 || >2.3.0,<3.0.0dev" +google-auth = ">=1.25.0,<3.0dev" + +[package.extras] +grpc = ["grpcio (>=1.38.0,<2.0dev)", "grpcio-status (>=1.38.0,<2.0.dev0)"] + +[[package]] +name = "google-cloud-datastore" +version = "2.20.2" +description = "Google Cloud Datastore API client library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "google_cloud_datastore-2.20.2-py2.py3-none-any.whl", hash = "sha256:d2190180343b807d4aa3b0b3bb837606349b71e5e74e29aa9009c0ae38c0b6a0"}, + {file = "google_cloud_datastore-2.20.2.tar.gz", hash = "sha256:9665d009729d9551329d9476f4d5bda9c11d3469243ea8a2c0d9490b65aa899f"}, +] + +[package.dependencies] +google-api-core = {version = ">=1.34.0,<2.0.dev0 || >=2.11.dev0,<3.0.0dev", extras = ["grpc"]} +google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0dev" +google-cloud-core = ">=1.4.0,<3.0.0dev" +proto-plus = {version = ">=1.22.2,<2.0.0dev", markers = "python_version >= \"3.11\""} +protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev" + +[package.extras] +libcst = ["libcst (>=0.2.5)"] + +[[package]] +name = "google-cloud-pubsub" +version = "2.29.0" +description = "Google Cloud Pub/Sub API client library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "google_cloud_pubsub-2.29.0-py2.py3-none-any.whl", hash = "sha256:3ccc76ae623e408c7a80f2f81bfd3ab9dca1d61231cc2a063d569d021449481a"}, + {file = "google_cloud_pubsub-2.29.0.tar.gz", hash = "sha256:b820f8d410c96ad87b8da79c696b979e1a182a170d0c0602626f5b9d8cbf21ee"}, +] + +[package.dependencies] +google-api-core = {version = ">=1.34.0,<2.0.dev0 || >=2.11.dev0,<3.0.0", extras = ["grpc"]} +google-auth = ">=2.14.1,<3.0.0" +grpc-google-iam-v1 = ">=0.12.4,<1.0.0" +grpcio = ">=1.51.3,<2.0.0" +grpcio-status = ">=1.33.2" +opentelemetry-api = {version = ">=1.27.0", markers = "python_version >= \"3.8\""} +opentelemetry-sdk = {version = ">=1.27.0", markers = "python_version >= \"3.8\""} +proto-plus = [ + {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, + {version = ">=1.22.2,<2.0.0", markers = "python_version >= \"3.11\""}, +] +protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" + +[package.extras] +libcst = ["libcst (>=0.3.10)"] + +[[package]] +name = "google-cloud-storage" +version = "3.1.0" +description = "Google Cloud Storage API client library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "google_cloud_storage-3.1.0-py2.py3-none-any.whl", hash = "sha256:eaf36966b68660a9633f03b067e4a10ce09f1377cae3ff9f2c699f69a81c66c6"}, + {file = "google_cloud_storage-3.1.0.tar.gz", hash = "sha256:944273179897c7c8a07ee15f2e6466a02da0c7c4b9ecceac2a26017cb2972049"}, +] + +[package.dependencies] +google-api-core = ">=2.15.0,<3.0.0dev" +google-auth = ">=2.26.1,<3.0dev" +google-cloud-core = ">=2.4.2,<3.0dev" +google-crc32c = ">=1.0,<2.0dev" +google-resumable-media = ">=2.7.2" +requests = ">=2.18.0,<3.0.0dev" + +[package.extras] +protobuf = ["protobuf (<6.0.0dev)"] +tracing = ["opentelemetry-api (>=1.1.0)"] + +[[package]] +name = "google-cloud-tasks" +version = "2.19.2" +description = "Google Cloud Tasks API client library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "google_cloud_tasks-2.19.2-py3-none-any.whl", hash = "sha256:898bf75020ead4dfb836a43d2ad666389ceeec1a4beb3cb65cc25b9accc289bb"}, + {file = "google_cloud_tasks-2.19.2.tar.gz", hash = "sha256:276b47e85f4825923a778d543fc0735e4b24be45f73fa7d964ad1655402d07dc"}, +] + +[package.dependencies] +google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0", extras = ["grpc"]} +google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0" +grpc-google-iam-v1 = ">=0.14.0,<1.0.0" +proto-plus = [ + {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, + {version = ">=1.22.3,<2.0.0", markers = "python_version < \"3.13\""}, +] +protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" + +[[package]] +name = "google-crc32c" +version = "1.7.1" +description = "A python wrapper of the C library 'Google CRC32C'" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "google_crc32c-1.7.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:b07d48faf8292b4db7c3d64ab86f950c2e94e93a11fd47271c28ba458e4a0d76"}, + {file = "google_crc32c-1.7.1-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:7cc81b3a2fbd932a4313eb53cc7d9dde424088ca3a0337160f35d91826880c1d"}, + {file = "google_crc32c-1.7.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1c67ca0a1f5b56162951a9dae987988679a7db682d6f97ce0f6381ebf0fbea4c"}, + {file = "google_crc32c-1.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc5319db92daa516b653600794d5b9f9439a9a121f3e162f94b0e1891c7933cb"}, + {file = "google_crc32c-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcdf5a64adb747610140572ed18d011896e3b9ae5195f2514b7ff678c80f1603"}, + {file = "google_crc32c-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:754561c6c66e89d55754106739e22fdaa93fafa8da7221b29c8b8e8270c6ec8a"}, + {file = "google_crc32c-1.7.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:6fbab4b935989e2c3610371963ba1b86afb09537fd0c633049be82afe153ac06"}, + {file = "google_crc32c-1.7.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:ed66cbe1ed9cbaaad9392b5259b3eba4a9e565420d734e6238813c428c3336c9"}, + {file = "google_crc32c-1.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee6547b657621b6cbed3562ea7826c3e11cab01cd33b74e1f677690652883e77"}, + {file = "google_crc32c-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d68e17bad8f7dd9a49181a1f5a8f4b251c6dbc8cc96fb79f1d321dfd57d66f53"}, + {file = "google_crc32c-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:6335de12921f06e1f774d0dd1fbea6bf610abe0887a1638f64d694013138be5d"}, + {file = "google_crc32c-1.7.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2d73a68a653c57281401871dd4aeebbb6af3191dcac751a76ce430df4d403194"}, + {file = "google_crc32c-1.7.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:22beacf83baaf59f9d3ab2bbb4db0fb018da8e5aebdce07ef9f09fce8220285e"}, + {file = "google_crc32c-1.7.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19eafa0e4af11b0a4eb3974483d55d2d77ad1911e6cf6f832e1574f6781fd337"}, + {file = "google_crc32c-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6d86616faaea68101195c6bdc40c494e4d76f41e07a37ffdef270879c15fb65"}, + {file = "google_crc32c-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:b7491bdc0c7564fcf48c0179d2048ab2f7c7ba36b84ccd3a3e1c3f7a72d3bba6"}, + {file = "google_crc32c-1.7.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:df8b38bdaf1629d62d51be8bdd04888f37c451564c2042d36e5812da9eff3c35"}, + {file = "google_crc32c-1.7.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:e42e20a83a29aa2709a0cf271c7f8aefaa23b7ab52e53b322585297bb94d4638"}, + {file = "google_crc32c-1.7.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:905a385140bf492ac300026717af339790921f411c0dfd9aa5a9e69a08ed32eb"}, + {file = "google_crc32c-1.7.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b211ddaf20f7ebeec5c333448582c224a7c90a9d98826fbab82c0ddc11348e6"}, + {file = "google_crc32c-1.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:0f99eaa09a9a7e642a61e06742856eec8b19fc0037832e03f941fe7cf0c8e4db"}, + {file = "google_crc32c-1.7.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32d1da0d74ec5634a05f53ef7df18fc646666a25efaaca9fc7dcfd4caf1d98c3"}, + {file = "google_crc32c-1.7.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e10554d4abc5238823112c2ad7e4560f96c7bf3820b202660373d769d9e6e4c9"}, + {file = "google_crc32c-1.7.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:9fc196f0b8d8bd2789352c6a522db03f89e83a0ed6b64315923c396d7a932315"}, + {file = "google_crc32c-1.7.1-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:bb5e35dcd8552f76eed9461a23de1030920a3c953c1982f324be8f97946e7127"}, + {file = "google_crc32c-1.7.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f2226b6a8da04f1d9e61d3e357f2460b9551c5e6950071437e122c958a18ae14"}, + {file = "google_crc32c-1.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f2b3522222746fff0e04a9bd0a23ea003ba3cccc8cf21385c564deb1f223242"}, + {file = "google_crc32c-1.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3bda0fcb632d390e3ea8b6b07bf6b4f4a66c9d02dcd6fbf7ba00a197c143f582"}, + {file = "google_crc32c-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:713121af19f1a617054c41f952294764e0c5443d5a5d9034b2cd60f5dd7e0349"}, + {file = "google_crc32c-1.7.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8e9afc74168b0b2232fb32dd202c93e46b7d5e4bf03e66ba5dc273bb3559589"}, + {file = "google_crc32c-1.7.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa8136cc14dd27f34a3221c0f16fd42d8a40e4778273e61a3c19aedaa44daf6b"}, + {file = "google_crc32c-1.7.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85fef7fae11494e747c9fd1359a527e5970fc9603c90764843caabd3a16a0a48"}, + {file = "google_crc32c-1.7.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6efb97eb4369d52593ad6f75e7e10d053cf00c48983f7a973105bc70b0ac4d82"}, + {file = "google_crc32c-1.7.1.tar.gz", hash = "sha256:2bff2305f98846f3e825dbeec9ee406f89da7962accdb29356e4eadc251bd472"}, +] + +[package.extras] +testing = ["pytest"] + +[[package]] +name = "google-resumable-media" +version = "2.7.2" +description = "Utilities for Google Media Downloads and Resumable Uploads" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "google_resumable_media-2.7.2-py2.py3-none-any.whl", hash = "sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa"}, + {file = "google_resumable_media-2.7.2.tar.gz", hash = "sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0"}, +] + +[package.dependencies] +google-crc32c = ">=1.0,<2.0dev" + +[package.extras] +aiohttp = ["aiohttp (>=3.6.2,<4.0.0dev)", "google-auth (>=1.22.0,<2.0dev)"] +requests = ["requests (>=2.18.0,<3.0.0dev)"] + +[[package]] +name = "googleapis-common-protos" +version = "1.69.2" +description = "Common protobufs used in Google APIs" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "googleapis_common_protos-1.69.2-py3-none-any.whl", hash = "sha256:0b30452ff9c7a27d80bfc5718954063e8ab53dd3697093d3bc99581f5fd24212"}, + {file = "googleapis_common_protos-1.69.2.tar.gz", hash = "sha256:3e1b904a27a33c821b4b749fd31d334c0c9c30e6113023d495e48979a3dc9c5f"}, +] + +[package.dependencies] +grpcio = {version = ">=1.44.0,<2.0.0", optional = true, markers = "extra == \"grpc\""} +protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" + +[package.extras] +grpc = ["grpcio (>=1.44.0,<2.0.0)"] + +[[package]] +name = "greenlet" +version = "3.1.1" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617"}, + {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7"}, + {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6"}, + {file = "greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80"}, + {file = "greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a"}, + {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511"}, + {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395"}, + {file = "greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39"}, + {file = "greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9"}, + {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0"}, + {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942"}, + {file = "greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01"}, + {file = "greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e"}, + {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1"}, + {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c"}, + {file = "greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822"}, + {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01"}, + {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47da355d8687fd65240c364c90a31569a133b7b60de111c255ef5b606f2ae291"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98884ecf2ffb7d7fe6bd517e8eb99d31ff7855a840fa6d0d63cd07c037f6a981"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1d4aeb8891338e60d1ab6127af1fe45def5259def8094b9c7e34690c8858803"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db32b5348615a04b82240cc67983cb315309e88d444a288934ee6ceaebcad6cc"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcc62f31eae24de7f8dce72134c8651c58000d3b1868e01392baea7c32c247de"}, + {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1d3755bcb2e02de341c55b4fca7a745a24a9e7212ac953f6b3a48d117d7257aa"}, + {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b8da394b34370874b4572676f36acabac172602abf054cbc4ac910219f3340af"}, + {file = "greenlet-3.1.1-cp37-cp37m-win32.whl", hash = "sha256:a0dfc6c143b519113354e780a50381508139b07d2177cb6ad6a08278ec655798"}, + {file = "greenlet-3.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:54558ea205654b50c438029505def3834e80f0869a70fb15b871c29b4575ddef"}, + {file = "greenlet-3.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1"}, + {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd"}, + {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7"}, + {file = "greenlet-3.1.1-cp38-cp38-win32.whl", hash = "sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef"}, + {file = "greenlet-3.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d"}, + {file = "greenlet-3.1.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c"}, + {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e"}, + {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e"}, + {file = "greenlet-3.1.1-cp39-cp39-win32.whl", hash = "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c"}, + {file = "greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22"}, + {file = "greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467"}, +] +markers = {main = "platform_python_implementation == \"CPython\""} + +[package.extras] +docs = ["Sphinx", "furo"] +test = ["objgraph", "psutil"] + +[[package]] +name = "grpc-google-iam-v1" +version = "0.14.2" +description = "IAM API client library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "grpc_google_iam_v1-0.14.2-py3-none-any.whl", hash = "sha256:a3171468459770907926d56a440b2bb643eec1d7ba215f48f3ecece42b4d8351"}, + {file = "grpc_google_iam_v1-0.14.2.tar.gz", hash = "sha256:b3e1fc387a1a329e41672197d0ace9de22c78dd7d215048c4c78712073f7bd20"}, +] + +[package.dependencies] +googleapis-common-protos = {version = ">=1.56.0,<2.0.0", extras = ["grpc"]} +grpcio = ">=1.44.0,<2.0.0" +protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" + +[[package]] +name = "grpcio" +version = "1.71.0" +description = "HTTP/2-based RPC framework" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "grpcio-1.71.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:c200cb6f2393468142eb50ab19613229dcc7829b5ccee8b658a36005f6669fdd"}, + {file = "grpcio-1.71.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:b2266862c5ad664a380fbbcdbdb8289d71464c42a8c29053820ee78ba0119e5d"}, + {file = "grpcio-1.71.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:0ab8b2864396663a5b0b0d6d79495657ae85fa37dcb6498a2669d067c65c11ea"}, + {file = "grpcio-1.71.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c30f393f9d5ff00a71bb56de4aa75b8fe91b161aeb61d39528db6b768d7eac69"}, + {file = "grpcio-1.71.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f250ff44843d9a0615e350c77f890082102a0318d66a99540f54769c8766ab73"}, + {file = "grpcio-1.71.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6d8de076528f7c43a2f576bc311799f89d795aa6c9b637377cc2b1616473804"}, + {file = "grpcio-1.71.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9b91879d6da1605811ebc60d21ab6a7e4bae6c35f6b63a061d61eb818c8168f6"}, + {file = "grpcio-1.71.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f71574afdf944e6652203cd1badcda195b2a27d9c83e6d88dc1ce3cfb73b31a5"}, + {file = "grpcio-1.71.0-cp310-cp310-win32.whl", hash = "sha256:8997d6785e93308f277884ee6899ba63baafa0dfb4729748200fcc537858a509"}, + {file = "grpcio-1.71.0-cp310-cp310-win_amd64.whl", hash = "sha256:7d6ac9481d9d0d129224f6d5934d5832c4b1cddb96b59e7eba8416868909786a"}, + {file = "grpcio-1.71.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:d6aa986318c36508dc1d5001a3ff169a15b99b9f96ef5e98e13522c506b37eef"}, + {file = "grpcio-1.71.0-cp311-cp311-macosx_10_14_universal2.whl", hash = "sha256:d2c170247315f2d7e5798a22358e982ad6eeb68fa20cf7a820bb74c11f0736e7"}, + {file = "grpcio-1.71.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:e6f83a583ed0a5b08c5bc7a3fe860bb3c2eac1f03f1f63e0bc2091325605d2b7"}, + {file = "grpcio-1.71.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4be74ddeeb92cc87190e0e376dbc8fc7736dbb6d3d454f2fa1f5be1dee26b9d7"}, + {file = "grpcio-1.71.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd0dfbe4d5eb1fcfec9490ca13f82b089a309dc3678e2edabc144051270a66e"}, + {file = "grpcio-1.71.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a2242d6950dc892afdf9e951ed7ff89473aaf744b7d5727ad56bdaace363722b"}, + {file = "grpcio-1.71.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:0fa05ee31a20456b13ae49ad2e5d585265f71dd19fbd9ef983c28f926d45d0a7"}, + {file = "grpcio-1.71.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3d081e859fb1ebe176de33fc3adb26c7d46b8812f906042705346b314bde32c3"}, + {file = "grpcio-1.71.0-cp311-cp311-win32.whl", hash = "sha256:d6de81c9c00c8a23047136b11794b3584cdc1460ed7cbc10eada50614baa1444"}, + {file = "grpcio-1.71.0-cp311-cp311-win_amd64.whl", hash = "sha256:24e867651fc67717b6f896d5f0cac0ec863a8b5fb7d6441c2ab428f52c651c6b"}, + {file = "grpcio-1.71.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:0ff35c8d807c1c7531d3002be03221ff9ae15712b53ab46e2a0b4bb271f38537"}, + {file = "grpcio-1.71.0-cp312-cp312-macosx_10_14_universal2.whl", hash = "sha256:b78a99cd1ece4be92ab7c07765a0b038194ded2e0a26fd654591ee136088d8d7"}, + {file = "grpcio-1.71.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:dc1a1231ed23caac1de9f943d031f1bc38d0f69d2a3b243ea0d664fc1fbd7fec"}, + {file = "grpcio-1.71.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6beeea5566092c5e3c4896c6d1d307fb46b1d4bdf3e70c8340b190a69198594"}, + {file = "grpcio-1.71.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5170929109450a2c031cfe87d6716f2fae39695ad5335d9106ae88cc32dc84c"}, + {file = "grpcio-1.71.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5b08d03ace7aca7b2fadd4baf291139b4a5f058805a8327bfe9aece7253b6d67"}, + {file = "grpcio-1.71.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f903017db76bf9cc2b2d8bdd37bf04b505bbccad6be8a81e1542206875d0e9db"}, + {file = "grpcio-1.71.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:469f42a0b410883185eab4689060a20488a1a0a00f8bbb3cbc1061197b4c5a79"}, + {file = "grpcio-1.71.0-cp312-cp312-win32.whl", hash = "sha256:ad9f30838550695b5eb302add33f21f7301b882937460dd24f24b3cc5a95067a"}, + {file = "grpcio-1.71.0-cp312-cp312-win_amd64.whl", hash = "sha256:652350609332de6dac4ece254e5d7e1ff834e203d6afb769601f286886f6f3a8"}, + {file = "grpcio-1.71.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:cebc1b34ba40a312ab480ccdb396ff3c529377a2fce72c45a741f7215bfe8379"}, + {file = "grpcio-1.71.0-cp313-cp313-macosx_10_14_universal2.whl", hash = "sha256:85da336e3649a3d2171e82f696b5cad2c6231fdd5bad52616476235681bee5b3"}, + {file = "grpcio-1.71.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:f9a412f55bb6e8f3bb000e020dbc1e709627dcb3a56f6431fa7076b4c1aab0db"}, + {file = "grpcio-1.71.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47be9584729534660416f6d2a3108aaeac1122f6b5bdbf9fd823e11fe6fbaa29"}, + {file = "grpcio-1.71.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c9c80ac6091c916db81131d50926a93ab162a7e97e4428ffc186b6e80d6dda4"}, + {file = "grpcio-1.71.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:789d5e2a3a15419374b7b45cd680b1e83bbc1e52b9086e49308e2c0b5bbae6e3"}, + {file = "grpcio-1.71.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:1be857615e26a86d7363e8a163fade914595c81fec962b3d514a4b1e8760467b"}, + {file = "grpcio-1.71.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a76d39b5fafd79ed604c4be0a869ec3581a172a707e2a8d7a4858cb05a5a7637"}, + {file = "grpcio-1.71.0-cp313-cp313-win32.whl", hash = "sha256:74258dce215cb1995083daa17b379a1a5a87d275387b7ffe137f1d5131e2cfbb"}, + {file = "grpcio-1.71.0-cp313-cp313-win_amd64.whl", hash = "sha256:22c3bc8d488c039a199f7a003a38cb7635db6656fa96437a8accde8322ce2366"}, + {file = "grpcio-1.71.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:c6a0a28450c16809f94e0b5bfe52cabff63e7e4b97b44123ebf77f448534d07d"}, + {file = "grpcio-1.71.0-cp39-cp39-macosx_10_14_universal2.whl", hash = "sha256:a371e6b6a5379d3692cc4ea1cb92754d2a47bdddeee755d3203d1f84ae08e03e"}, + {file = "grpcio-1.71.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:39983a9245d37394fd59de71e88c4b295eb510a3555e0a847d9965088cdbd033"}, + {file = "grpcio-1.71.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9182e0063112e55e74ee7584769ec5a0b4f18252c35787f48738627e23a62b97"}, + {file = "grpcio-1.71.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693bc706c031aeb848849b9d1c6b63ae6bcc64057984bb91a542332b75aa4c3d"}, + {file = "grpcio-1.71.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:20e8f653abd5ec606be69540f57289274c9ca503ed38388481e98fa396ed0b41"}, + {file = "grpcio-1.71.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8700a2a57771cc43ea295296330daaddc0d93c088f0a35cc969292b6db959bf3"}, + {file = "grpcio-1.71.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d35a95f05a8a2cbe8e02be137740138b3b2ea5f80bd004444e4f9a1ffc511e32"}, + {file = "grpcio-1.71.0-cp39-cp39-win32.whl", hash = "sha256:f9c30c464cb2ddfbc2ddf9400287701270fdc0f14be5f08a1e3939f1e749b455"}, + {file = "grpcio-1.71.0-cp39-cp39-win_amd64.whl", hash = "sha256:63e41b91032f298b3e973b3fa4093cbbc620c875e2da7b93e249d4728b54559a"}, + {file = "grpcio-1.71.0.tar.gz", hash = "sha256:2b85f7820475ad3edec209d3d89a7909ada16caab05d3f2e08a7e8ae3200a55c"}, +] + +[package.extras] +protobuf = ["grpcio-tools (>=1.71.0)"] + +[[package]] +name = "grpcio-status" +version = "1.71.0" +description = "Status proto mapping for gRPC" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "grpcio_status-1.71.0-py3-none-any.whl", hash = "sha256:843934ef8c09e3e858952887467f8256aac3910c55f077a359a65b2b3cde3e68"}, + {file = "grpcio_status-1.71.0.tar.gz", hash = "sha256:11405fed67b68f406b3f3c7c5ae5104a79d2d309666d10d61b152e91d28fb968"}, +] + +[package.dependencies] +googleapis-common-protos = ">=1.5.5" +grpcio = ">=1.71.0" +protobuf = ">=5.26.1,<6.0dev" + +[[package]] +name = "gunicorn" +version = "23.0.0" +description = "WSGI HTTP Server for UNIX" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"}, + {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"}, +] + +[package.dependencies] +packaging = "*" + +[package.extras] +eventlet = ["eventlet (>=0.24.1,!=0.36.0)"] +gevent = ["gevent (>=1.4.0)"] +setproctitle = ["setproctitle"] +testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"] +tornado = ["tornado (>=0.2)"] + +[[package]] +name = "htmlmin" +version = "0.1.12" +description = "An HTML Minifier" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "htmlmin-0.1.12.tar.gz", hash = "sha256:50c1ef4630374a5d723900096a961cff426dff46b48f34d194a81bbe14eca178"}, +] + +[[package]] +name = "httmock" +version = "1.4.0" +description = "A mocking library for requests." +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "httmock-1.4.0-py3-none-any.whl", hash = "sha256:13e6c63f135a928e15d386af789a2890efb03e0e280f29bdc9961f3f0dc34cb9"}, + {file = "httmock-1.4.0.tar.gz", hash = "sha256:44eaf4bb59cc64cd6f5d8bf8700b46aa3097cc5651b9bc85c527dfbc71792f41"}, +] + +[package.dependencies] +requests = ">=1.0.0" + +[[package]] +name = "humanfriendly" +version = "10.0" +description = "Human friendly output for text interfaces using Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["main"] +files = [ + {file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"}, + {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, +] + +[package.dependencies] +pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_version >= \"3.8\""} + +[[package]] +name = "humanize" +version = "4.12.2" +description = "Python humanize utilities" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "humanize-4.12.2-py3-none-any.whl", hash = "sha256:e4e44dced598b7e03487f3b1c6fd5b1146c30ea55a110e71d5d4bca3e094259e"}, + {file = "humanize-4.12.2.tar.gz", hash = "sha256:ce0715740e9caacc982bb89098182cf8ded3552693a433311c6a4ce6f4e12a2c"}, +] + +[package.extras] +tests = ["freezegun", "pytest", "pytest-cov"] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +groups = ["main", "dev"] +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "importlib-metadata" +version = "8.6.1" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e"}, + {file = "importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580"}, +] + +[package.dependencies] +zipp = ">=3.20" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +perf = ["ipython"] +test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy"] + +[[package]] +name = "iniconfig" +version = "2.1.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, +] + +[[package]] +name = "isort" +version = "6.0.1" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.9.0" +groups = ["dev"] +files = [ + {file = "isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615"}, + {file = "isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450"}, +] + +[package.extras] +colors = ["colorama"] +plugins = ["setuptools"] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +description = "Safely pass data to untrusted environments and back." +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, + {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "jmespath" +version = "1.0.1" +description = "JSON Matching Expressions" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, + {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, +] + +[[package]] +name = "jsbeautifier" +version = "1.15.4" +description = "JavaScript unobfuscator and beautifier." +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "jsbeautifier-1.15.4-py3-none-any.whl", hash = "sha256:72f65de312a3f10900d7685557f84cb61a9733c50dcc27271a39f5b0051bf528"}, + {file = "jsbeautifier-1.15.4.tar.gz", hash = "sha256:5bb18d9efb9331d825735fbc5360ee8f1aac5e52780042803943aa7f854f7592"}, +] + +[package.dependencies] +editorconfig = ">=0.12.2" +six = ">=1.13.0" + +[[package]] +name = "json5" +version = "0.10.0" +description = "A Python implementation of the JSON5 data format." +optional = false +python-versions = ">=3.8.0" +groups = ["dev"] +files = [ + {file = "json5-0.10.0-py3-none-any.whl", hash = "sha256:19b23410220a7271e8377f81ba8aacba2fdd56947fbb137ee5977cbe1f5e8dfa"}, + {file = "json5-0.10.0.tar.gz", hash = "sha256:e66941c8f0a02026943c52c2eb34ebeb2a6f819a0be05920a6f5243cd30fd559"}, +] + +[package.extras] +dev = ["build (==1.2.2.post1)", "coverage (==7.5.3)", "mypy (==1.13.0)", "pip (==24.3.1)", "pylint (==3.2.3)", "ruff (==0.7.3)", "twine (==5.1.1)", "uv (==0.5.1)"] + +[[package]] +name = "jsonpointer" +version = "3.0.0" +description = "Identify specific nodes in a JSON document (RFC 6901)" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"}, + {file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"}, +] + +[[package]] +name = "jsonschema" +version = "4.24.0" +description = "An implementation of JSON Schema validation for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "jsonschema-4.24.0-py3-none-any.whl", hash = "sha256:a462455f19f5faf404a7902952b6f0e3ce868f3ee09a359b05eca6673bd8412d"}, + {file = "jsonschema-4.24.0.tar.gz", hash = "sha256:0b4e8069eb12aedfa881333004bccaec24ecef5a8a6a4b6df142b2cc9599d196"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +jsonschema-specifications = ">=2023.03.6" +referencing = ">=0.28.4" +rpds-py = ">=0.7.1" + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=24.6.0)"] + +[[package]] +name = "jsonschema-specifications" +version = "2024.10.1" +description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf"}, + {file = "jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272"}, +] + +[package.dependencies] +referencing = ">=0.31.0" + +[[package]] +name = "jwcrypto" +version = "1.5.6" +description = "Implementation of JOSE Web standards" +optional = false +python-versions = ">= 3.8" +groups = ["main"] +files = [ + {file = "jwcrypto-1.5.6-py3-none-any.whl", hash = "sha256:150d2b0ebbdb8f40b77f543fb44ffd2baeff48788be71f67f03566692fd55789"}, + {file = "jwcrypto-1.5.6.tar.gz", hash = "sha256:771a87762a0c081ae6166958a954f80848820b2ab066937dc8b8379d65b1b039"}, +] + +[package.dependencies] +cryptography = ">=3.4" +typing-extensions = ">=4.5.0" + +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "marshmallow" +version = "3.26.1" +description = "A lightweight library for converting complex datatypes to and from native Python datatypes." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c"}, + {file = "marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6"}, +] + +[package.dependencies] +packaging = ">=17.0" + +[package.extras] +dev = ["marshmallow[tests]", "pre-commit (>=3.5,<5.0)", "tox"] +docs = ["autodocsumm (==0.2.14)", "furo (==2024.8.6)", "sphinx (==8.1.3)", "sphinx-copybutton (==0.5.2)", "sphinx-issues (==5.0.0)", "sphinxext-opengraph (==0.9.1)"] +tests = ["pytest", "simplejson"] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mock" +version = "5.2.0" +description = "Rolling backport of unittest.mock for all Pythons" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "mock-5.2.0-py3-none-any.whl", hash = "sha256:7ba87f72ca0e915175596069dbbcc7c75af7b5e9b9bc107ad6349ede0819982f"}, + {file = "mock-5.2.0.tar.gz", hash = "sha256:4e460e818629b4b173f32d08bf30d3af8123afbb8e04bb5707a1fd4799e503f0"}, +] + +[package.extras] +build = ["blurb", "twine", "wheel"] +docs = ["sphinx"] +test = ["pytest", "pytest-cov"] + +[[package]] +name = "moto" +version = "5.1.6" +description = "A library that allows you to easily mock out tests based on AWS infrastructure" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "moto-5.1.6-py3-none-any.whl", hash = "sha256:e4a3092bc8fe9139caa77cd34cdcbad804de4d9671e2270ea3b4d53f5c645047"}, + {file = "moto-5.1.6.tar.gz", hash = "sha256:baf7afa9d4a92f07277b29cf466d0738f25db2ed2ee12afcb1dc3f2c540beebd"}, +] + +[package.dependencies] +boto3 = ">=1.9.201" +botocore = ">=1.20.88,<1.35.45 || >1.35.45,<1.35.46 || >1.35.46" +cryptography = ">=35.0.0" +Jinja2 = ">=2.10.1" +python-dateutil = ">=2.1,<3.0.0" +requests = ">=2.5" +responses = ">=0.15.0,<0.25.5 || >0.25.5" +werkzeug = ">=0.5,<2.2.0 || >2.2.0,<2.2.1 || >2.2.1" +xmltodict = "*" + +[package.extras] +all = ["PyYAML (>=5.1)", "antlr4-python3-runtime", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "graphql-core", "joserfc (>=0.9.0)", "jsonpath_ng", "jsonschema", "multipart", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.6.1)", "pyparsing (>=3.0.7)", "setuptools"] +apigateway = ["PyYAML (>=5.1)", "joserfc (>=0.9.0)", "openapi-spec-validator (>=0.5.0)"] +apigatewayv2 = ["PyYAML (>=5.1)", "openapi-spec-validator (>=0.5.0)"] +appsync = ["graphql-core"] +awslambda = ["docker (>=3.0.0)"] +batch = ["docker (>=3.0.0)"] +cloudformation = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "graphql-core", "joserfc (>=0.9.0)", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.6.1)", "pyparsing (>=3.0.7)", "setuptools"] +cognitoidp = ["joserfc (>=0.9.0)"] +dynamodb = ["docker (>=3.0.0)", "py-partiql-parser (==0.6.1)"] +dynamodbstreams = ["docker (>=3.0.0)", "py-partiql-parser (==0.6.1)"] +events = ["jsonpath_ng"] +glue = ["pyparsing (>=3.0.7)"] +proxy = ["PyYAML (>=5.1)", "antlr4-python3-runtime", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=2.5.1)", "graphql-core", "joserfc (>=0.9.0)", "jsonpath_ng", "multipart", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.6.1)", "pyparsing (>=3.0.7)", "setuptools"] +quicksight = ["jsonschema"] +resourcegroupstaggingapi = ["PyYAML (>=5.1)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "graphql-core", "joserfc (>=0.9.0)", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.6.1)", "pyparsing (>=3.0.7)"] +s3 = ["PyYAML (>=5.1)", "py-partiql-parser (==0.6.1)"] +s3crc32c = ["PyYAML (>=5.1)", "crc32c", "py-partiql-parser (==0.6.1)"] +server = ["PyYAML (>=5.1)", "antlr4-python3-runtime", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "flask (!=2.2.0,!=2.2.1)", "flask-cors", "graphql-core", "joserfc (>=0.9.0)", "jsonpath_ng", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.6.1)", "pyparsing (>=3.0.7)", "setuptools"] +ssm = ["PyYAML (>=5.1)"] +stepfunctions = ["antlr4-python3-runtime", "jsonpath_ng"] +xray = ["aws-xray-sdk (>=0.93,!=0.96)", "setuptools"] + +[[package]] +name = "mypy" +version = "1.16.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "mypy-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7909541fef256527e5ee9c0a7e2aeed78b6cda72ba44298d1334fe7881b05c5c"}, + {file = "mypy-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e71d6f0090c2256c713ed3d52711d01859c82608b5d68d4fa01a3fe30df95571"}, + {file = "mypy-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:936ccfdd749af4766be824268bfe22d1db9eb2f34a3ea1d00ffbe5b5265f5491"}, + {file = "mypy-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4086883a73166631307fdd330c4a9080ce24913d4f4c5ec596c601b3a4bdd777"}, + {file = "mypy-1.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:feec38097f71797da0231997e0de3a58108c51845399669ebc532c815f93866b"}, + {file = "mypy-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:09a8da6a0ee9a9770b8ff61b39c0bb07971cda90e7297f4213741b48a0cc8d93"}, + {file = "mypy-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9f826aaa7ff8443bac6a494cf743f591488ea940dd360e7dd330e30dd772a5ab"}, + {file = "mypy-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82d056e6faa508501af333a6af192c700b33e15865bda49611e3d7d8358ebea2"}, + {file = "mypy-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:089bedc02307c2548eb51f426e085546db1fa7dd87fbb7c9fa561575cf6eb1ff"}, + {file = "mypy-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a2322896003ba66bbd1318c10d3afdfe24e78ef12ea10e2acd985e9d684a666"}, + {file = "mypy-1.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:021a68568082c5b36e977d54e8f1de978baf401a33884ffcea09bd8e88a98f4c"}, + {file = "mypy-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:54066fed302d83bf5128632d05b4ec68412e1f03ef2c300434057d66866cea4b"}, + {file = "mypy-1.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5436d11e89a3ad16ce8afe752f0f373ae9620841c50883dc96f8b8805620b13"}, + {file = "mypy-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f2622af30bf01d8fc36466231bdd203d120d7a599a6d88fb22bdcb9dbff84090"}, + {file = "mypy-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d045d33c284e10a038f5e29faca055b90eee87da3fc63b8889085744ebabb5a1"}, + {file = "mypy-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4968f14f44c62e2ec4a038c8797a87315be8df7740dc3ee8d3bfe1c6bf5dba8"}, + {file = "mypy-1.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb14a4a871bb8efb1e4a50360d4e3c8d6c601e7a31028a2c79f9bb659b63d730"}, + {file = "mypy-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd4e1ebe126152a7bbaa4daedd781c90c8f9643c79b9748caa270ad542f12bec"}, + {file = "mypy-1.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e056237c89f1587a3be1a3a70a06a698d25e2479b9a2f57325ddaaffc3567b"}, + {file = "mypy-1.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b07e107affb9ee6ce1f342c07f51552d126c32cd62955f59a7db94a51ad12c0"}, + {file = "mypy-1.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6fb60cbd85dc65d4d63d37cb5c86f4e3a301ec605f606ae3a9173e5cf34997b"}, + {file = "mypy-1.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7e32297a437cc915599e0578fa6bc68ae6a8dc059c9e009c628e1c47f91495d"}, + {file = "mypy-1.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:afe420c9380ccec31e744e8baff0d406c846683681025db3531b32db56962d52"}, + {file = "mypy-1.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:55f9076c6ce55dd3f8cd0c6fff26a008ca8e5131b89d5ba6d86bd3f47e736eeb"}, + {file = "mypy-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f56236114c425620875c7cf71700e3d60004858da856c6fc78998ffe767b73d3"}, + {file = "mypy-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:15486beea80be24ff067d7d0ede673b001d0d684d0095803b3e6e17a886a2a92"}, + {file = "mypy-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2ed0e0847a80655afa2c121835b848ed101cc7b8d8d6ecc5205aedc732b1436"}, + {file = "mypy-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb5fbc8063cb4fde7787e4c0406aa63094a34a2daf4673f359a1fb64050e9cb2"}, + {file = "mypy-1.16.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a5fcfdb7318c6a8dd127b14b1052743b83e97a970f0edb6c913211507a255e20"}, + {file = "mypy-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:2e7e0ad35275e02797323a5aa1be0b14a4d03ffdb2e5f2b0489fa07b89c67b21"}, + {file = "mypy-1.16.0-py3-none-any.whl", hash = "sha256:29e1499864a3888bca5c1542f2d7232c6e586295183320caa95758fc84034031"}, + {file = "mypy-1.16.0.tar.gz", hash = "sha256:84b94283f817e2aa6350a14b4a8fb2a35a53c286f97c9d30f53b63620e7af8ab"}, +] + +[package.dependencies] +mypy_extensions = ">=1.0.0" +pathspec = ">=0.9.0" +typing_extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "opentelemetry-api" +version = "1.31.1" +description = "OpenTelemetry Python API" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "opentelemetry_api-1.31.1-py3-none-any.whl", hash = "sha256:1511a3f470c9c8a32eeea68d4ea37835880c0eed09dd1a0187acc8b1301da0a1"}, + {file = "opentelemetry_api-1.31.1.tar.gz", hash = "sha256:137ad4b64215f02b3000a0292e077641c8611aab636414632a9b9068593b7e91"}, +] + +[package.dependencies] +deprecated = ">=1.2.6" +importlib-metadata = ">=6.0,<8.7.0" + +[[package]] +name = "opentelemetry-sdk" +version = "1.31.1" +description = "OpenTelemetry Python SDK" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "opentelemetry_sdk-1.31.1-py3-none-any.whl", hash = "sha256:882d021321f223e37afaca7b4e06c1d8bbc013f9e17ff48a7aa017460a8e7dae"}, + {file = "opentelemetry_sdk-1.31.1.tar.gz", hash = "sha256:c95f61e74b60769f8ff01ec6ffd3d29684743404603df34b20aa16a49dc8d903"}, +] + +[package.dependencies] +opentelemetry-api = "1.31.1" +opentelemetry-semantic-conventions = "0.52b1" +typing-extensions = ">=3.7.4" + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.52b1" +description = "OpenTelemetry Semantic Conventions" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "opentelemetry_semantic_conventions-0.52b1-py3-none-any.whl", hash = "sha256:72b42db327e29ca8bb1b91e8082514ddf3bbf33f32ec088feb09526ade4bc77e"}, + {file = "opentelemetry_semantic_conventions-0.52b1.tar.gz", hash = "sha256:7b3d226ecf7523c27499758a58b542b48a0ac8d12be03c0488ff8ec60c5bae5d"}, +] + +[package.dependencies] +deprecated = ">=1.2.6" +opentelemetry-api = "1.31.1" + +[[package]] +name = "ordered-set" +version = "4.1.0" +description = "An OrderedSet is a custom MutableSet that remembers its order, so that every" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "ordered-set-4.1.0.tar.gz", hash = "sha256:694a8e44c87657c59292ede72891eb91d34131f6531463aab3009191c77364a8"}, + {file = "ordered_set-4.1.0-py3-none-any.whl", hash = "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562"}, +] + +[package.extras] +dev = ["black", "mypy", "pytest"] + +[[package]] +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "pdfkit" +version = "1.0.0" +description = "Wkhtmltopdf python wrapper to convert html to pdf using the webkit rendering engine and qt" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "pdfkit-1.0.0-py2-none-any.whl", hash = "sha256:cc122e5aed594198ff7aaa566f2950d2163763576ab891c161bb1f6c630f5a8e"}, + {file = "pdfkit-1.0.0-py3-none-any.whl", hash = "sha256:a7a4ca0d978e44fa8310c4909f087052430a6e8e0b1dd7ceef657f139789f96f"}, + {file = "pdfkit-1.0.0.tar.gz", hash = "sha256:992f821e1e18fc8a0e701ecae24b51a2d598296a180caee0a24c0af181da02a9"}, +] + +[[package]] +name = "pep8" +version = "1.7.1" +description = "Python style guide checker" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "pep8-1.7.1-py2.py3-none-any.whl", hash = "sha256:b22cfae5db09833bb9bd7c8463b53e1a9c9b39f12e304a8d0bba729c501827ee"}, + {file = "pep8-1.7.1.tar.gz", hash = "sha256:fe249b52e20498e59e0b5c5256aa52ee99fc295b26ec9eaa85776ffdb9fe6374"}, +] + +[[package]] +name = "pika" +version = "1.3.2" +description = "Pika Python AMQP Client Library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "pika-1.3.2-py3-none-any.whl", hash = "sha256:0779a7c1fafd805672796085560d290213a465e4f6f76a6fb19e378d8041a14f"}, + {file = "pika-1.3.2.tar.gz", hash = "sha256:b2a327ddddf8570b4965b3576ac77091b850262d34ce8c1d8cb4e4146aa4145f"}, +] + +[package.extras] +gevent = ["gevent"] +tornado = ["tornado"] +twisted = ["twisted"] + +[[package]] +name = "platformdirs" +version = "4.3.7" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94"}, + {file = "platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] + +[[package]] +name = "playwright" +version = "1.52.0" +description = "A high-level API to automate web browsers" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "playwright-1.52.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:19b2cb9d4794062008a635a99bd135b03ebb782d460f96534a91cb583f549512"}, + {file = "playwright-1.52.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0797c0479cbdc99607412a3c486a3a2ec9ddc77ac461259fd2878c975bcbb94a"}, + {file = "playwright-1.52.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:7223960b7dd7ddeec1ba378c302d1d09733b8dac438f492e9854c85d3ca7144f"}, + {file = "playwright-1.52.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:d010124d24a321e0489a8c0d38a3971a7ca7656becea7656c9376bfea7f916d4"}, + {file = "playwright-1.52.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4173e453c43180acc60fd77ffe1ebee8d0efbfd9986c03267007b9c3845415af"}, + {file = "playwright-1.52.0-py3-none-win32.whl", hash = "sha256:cd0bdf92df99db6237a99f828e80a6a50db6180ef8d5352fc9495df2c92f9971"}, + {file = "playwright-1.52.0-py3-none-win_amd64.whl", hash = "sha256:dcbf75101eba3066b7521c6519de58721ea44379eb17a0dafa94f9f1b17f59e4"}, + {file = "playwright-1.52.0-py3-none-win_arm64.whl", hash = "sha256:9d0085b8de513de5fb50669f8e6677f0252ef95a9a1d2d23ccee9638e71e65cb"}, +] + +[package.dependencies] +greenlet = ">=3.1.1,<4.0.0" +pyee = ">=13,<14" + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "proto-plus" +version = "1.26.1" +description = "Beautiful, Pythonic protocol buffers" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66"}, + {file = "proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012"}, +] + +[package.dependencies] +protobuf = ">=3.19.0,<7.0.0" + +[package.extras] +testing = ["google-api-core (>=1.31.5)"] + +[[package]] +name = "protobuf" +version = "5.29.4" +description = "" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "protobuf-5.29.4-cp310-abi3-win32.whl", hash = "sha256:13eb236f8eb9ec34e63fc8b1d6efd2777d062fa6aaa68268fb67cf77f6839ad7"}, + {file = "protobuf-5.29.4-cp310-abi3-win_amd64.whl", hash = "sha256:bcefcdf3976233f8a502d265eb65ea740c989bacc6c30a58290ed0e519eb4b8d"}, + {file = "protobuf-5.29.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:307ecba1d852ec237e9ba668e087326a67564ef83e45a0189a772ede9e854dd0"}, + {file = "protobuf-5.29.4-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:aec4962f9ea93c431d5714ed1be1c93f13e1a8618e70035ba2b0564d9e633f2e"}, + {file = "protobuf-5.29.4-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:d7d3f7d1d5a66ed4942d4fefb12ac4b14a29028b209d4bfb25c68ae172059922"}, + {file = "protobuf-5.29.4-cp38-cp38-win32.whl", hash = "sha256:1832f0515b62d12d8e6ffc078d7e9eb06969aa6dc13c13e1036e39d73bebc2de"}, + {file = "protobuf-5.29.4-cp38-cp38-win_amd64.whl", hash = "sha256:476cb7b14914c780605a8cf62e38c2a85f8caff2e28a6a0bad827ec7d6c85d68"}, + {file = "protobuf-5.29.4-cp39-cp39-win32.whl", hash = "sha256:fd32223020cb25a2cc100366f1dedc904e2d71d9322403224cdde5fdced0dabe"}, + {file = "protobuf-5.29.4-cp39-cp39-win_amd64.whl", hash = "sha256:678974e1e3a9b975b8bc2447fca458db5f93a2fb6b0c8db46b6675b5b5346812"}, + {file = "protobuf-5.29.4-py3-none-any.whl", hash = "sha256:3fde11b505e1597f71b875ef2fc52062b6a9740e5f7c8997ce878b6009145862"}, + {file = "protobuf-5.29.4.tar.gz", hash = "sha256:4f1dfcd7997b31ef8f53ec82781ff434a28bf71d9102ddde14d076adcfc78c99"}, +] + +[[package]] +name = "pyasn1" +version = "0.6.1" +description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, + {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +description = "A collection of ASN.1-based protocols modules" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a"}, + {file = "pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6"}, +] + +[package.dependencies] +pyasn1 = ">=0.6.1,<0.7.0" + +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] +markers = {dev = "platform_python_implementation != \"PyPy\""} + +[[package]] +name = "pyee" +version = "13.0.0" +description = "A rough port of Node.js's EventEmitter to Python with a few tricks of its own" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pyee-13.0.0-py3-none-any.whl", hash = "sha256:48195a3cddb3b1515ce0695ed76036b5ccc2ef3a9f963ff9f77aec0139845498"}, + {file = "pyee-13.0.0.tar.gz", hash = "sha256:b391e3c5a434d1f5118a25615001dbc8f669cf410ab67d04c4d4e07c55481c37"}, +] + +[package.dependencies] +typing-extensions = "*" + +[package.extras] +dev = ["black", "build", "flake8", "flake8-black", "isort", "jupyter-console", "mkdocs", "mkdocs-include-markdown-plugin", "mkdocstrings[python]", "mypy", "pytest", "pytest-asyncio ; python_version >= \"3.4\"", "pytest-trio ; python_version >= \"3.7\"", "sphinx", "toml", "tox", "trio", "trio ; python_version > \"3.6\"", "trio-typing ; python_version > \"3.6\"", "twine", "twisted", "validate-pyproject[all]"] + +[[package]] +name = "pygments" +version = "2.19.1" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, + {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pylint" +version = "3.3.7" +description = "python code static checker" +optional = false +python-versions = ">=3.9.0" +groups = ["dev"] +files = [ + {file = "pylint-3.3.7-py3-none-any.whl", hash = "sha256:43860aafefce92fca4cf6b61fe199cdc5ae54ea28f9bf4cd49de267b5195803d"}, + {file = "pylint-3.3.7.tar.gz", hash = "sha256:2b11de8bde49f9c5059452e0c310c079c746a0a8eeaa789e5aa966ecc23e4559"}, +] + +[package.dependencies] +astroid = ">=3.3.8,<=3.4.0.dev0" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} +dill = {version = ">=0.3.7", markers = "python_version >= \"3.12\""} +isort = ">=4.2.5,<5.13 || >5.13,<7" +mccabe = ">=0.6,<0.8" +platformdirs = ">=2.2" +tomlkit = ">=0.10.1" + +[package.extras] +spelling = ["pyenchant (>=3.2,<4.0)"] +testutils = ["gitpython (>3)"] + +[[package]] +name = "pylint-absolute-imports" +version = "1.1.0" +description = "Pylint plugin which adds linter error for relatives imports.Read more about imports at https://peps.python.org/pep-0008/#imports" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "pylint_absolute_imports-1.1.0-py3-none-any.whl", hash = "sha256:3fa7b73481c50cb7e7892d5eeaecf8108ad0ed06d9a545b5c620d20250a1f727"}, + {file = "pylint_absolute_imports-1.1.0.tar.gz", hash = "sha256:3ea2bb868626c4662b76cb84d541af4e4cc4a355872fa28def72ad4439b31717"}, +] + +[package.dependencies] +pylint = ">=2.5.0,<4" + +[[package]] +name = "pylint-mccabe" +version = "0.1.3" +description = "McCabe complexity checker as a PyLint plugin" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "pylint-mccabe-0.1.3.tar.gz", hash = "sha256:f3628affbc6064c08477243915f6752f3ef59fb82803b00be92f30d0ef7bbf29"}, +] + +[package.dependencies] +mccabe = ">=0.2" +pep8 = ">=1.4.6" +pylint = ">=0.28.0" + +[[package]] +name = "pyreadline3" +version = "3.5.4" +description = "A python implementation of GNU readline." +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6"}, + {file = "pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7"}, +] + +[package.extras] +dev = ["build", "flake8", "mypy", "pytest", "twine"] + +[[package]] +name = "pytest" +version = "8.4.0" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e"}, + {file = "pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +iniconfig = ">=1" +packaging = ">=20" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "6.2.1" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5"}, + {file = "pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2"}, +] + +[package.dependencies] +coverage = {version = ">=7.5", extras = ["toml"]} +pluggy = ">=1.2" +pytest = ">=6.2.5" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pytest-flask" +version = "1.3.0" +description = "A set of py.test fixtures to test Flask applications." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pytest-flask-1.3.0.tar.gz", hash = "sha256:58be1c97b21ba3c4d47e0a7691eb41007748506c36bf51004f78df10691fa95e"}, + {file = "pytest_flask-1.3.0-py3-none-any.whl", hash = "sha256:c0e36e6b0fddc3b91c4362661db83fa694d1feb91fa505475be6732b5bc8c253"}, +] + +[package.dependencies] +Flask = "*" +pytest = ">=5.2" +Werkzeug = "*" + +[package.extras] +docs = ["Sphinx", "sphinx-rtd-theme"] + +[[package]] +name = "pytest-mock" +version = "3.14.1" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0"}, + {file = "pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e"}, +] + +[package.dependencies] +pytest = ">=6.2.5" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + +[[package]] +name = "pytest-sugar" +version = "1.0.0" +description = "pytest-sugar is a plugin for pytest that changes the default look and feel of pytest (e.g. progressbar, show tests that fail instantly)." +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "pytest-sugar-1.0.0.tar.gz", hash = "sha256:6422e83258f5b0c04ce7c632176c7732cab5fdb909cb39cca5c9139f81276c0a"}, + {file = "pytest_sugar-1.0.0-py3-none-any.whl", hash = "sha256:70ebcd8fc5795dc457ff8b69d266a4e2e8a74ae0c3edc749381c64b5246c8dfd"}, +] + +[package.dependencies] +packaging = ">=21.3" +pytest = ">=6.2.0" +termcolor = ">=2.1.0" + +[package.extras] +dev = ["black", "flake8", "pre-commit"] + +[[package]] +name = "pytest-xdist" +version = "3.7.0" +description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest_xdist-3.7.0-py3-none-any.whl", hash = "sha256:7d3fbd255998265052435eb9daa4e99b62e6fb9cfb6efd1f858d4d8c0c7f0ca0"}, + {file = "pytest_xdist-3.7.0.tar.gz", hash = "sha256:f9248c99a7c15b7d2f90715df93610353a485827bc06eefb6566d23f6400f126"}, +] + +[package.dependencies] +execnet = ">=2.1" +pytest = ">=7.0.0" + +[package.extras] +psutil = ["psutil (>=3.0)"] +setproctitle = ["setproctitle"] +testing = ["filelock"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main", "dev"] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-snappy" +version = "0.7.3" +description = "Python library for the snappy compression library from Google" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "python_snappy-0.7.3-py3-none-any.whl", hash = "sha256:074c0636cfcd97e7251330f428064050ac81a52c62ed884fc2ddebbb60ed7f50"}, + {file = "python_snappy-0.7.3.tar.gz", hash = "sha256:40216c1badfb2d38ac781ecb162a1d0ec40f8ee9747e610bcfefdfa79486cee3"}, +] + +[package.dependencies] +cramjam = "*" + +[[package]] +name = "pytz" +version = "2025.2" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, + {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "redis" +version = "5.2.1" +description = "Python client for Redis database and key-value store" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4"}, + {file = "redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f"}, +] + +[package.extras] +hiredis = ["hiredis (>=3.0.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==23.2.1)", "requests (>=2.31.0)"] + +[[package]] +name = "referencing" +version = "0.36.2" +description = "JSON Referencing + Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0"}, + {file = "referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +rpds-py = ">=0.7.0" +typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} + +[[package]] +name = "regex" +version = "2024.11.6" +description = "Alternative regular expression module, to replace re." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"}, + {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"}, + {file = "regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c"}, + {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008"}, + {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62"}, + {file = "regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e"}, + {file = "regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7"}, + {file = "regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0"}, + {file = "regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d"}, + {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45"}, + {file = "regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9"}, + {file = "regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9"}, + {file = "regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e"}, + {file = "regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51"}, + {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad"}, + {file = "regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54"}, + {file = "regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4"}, + {file = "regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c"}, + {file = "regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4"}, + {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d"}, + {file = "regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff"}, + {file = "regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3a51ccc315653ba012774efca4f23d1d2a8a8f278a6072e29c7147eee7da446b"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ad182d02e40de7459b73155deb8996bbd8e96852267879396fb274e8700190e3"}, + {file = "regex-2024.11.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba9b72e5643641b7d41fa1f6d5abda2c9a263ae835b917348fc3c928182ad467"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40291b1b89ca6ad8d3f2b82782cc33807f1406cf68c8d440861da6304d8ffbbd"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdf58d0e516ee426a48f7b2c03a332a4114420716d55769ff7108c37a09951bf"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a36fdf2af13c2b14738f6e973aba563623cb77d753bbbd8d414d18bfaa3105dd"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1cee317bfc014c2419a76bcc87f071405e3966da434e03e13beb45f8aced1a6"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50153825ee016b91549962f970d6a4442fa106832e14c918acd1c8e479916c4f"}, + {file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea1bfda2f7162605f6e8178223576856b3d791109f15ea99a9f95c16a7636fb5"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:df951c5f4a1b1910f1a99ff42c473ff60f8225baa1cdd3539fe2819d9543e9df"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:072623554418a9911446278f16ecb398fb3b540147a7828c06e2011fa531e773"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f654882311409afb1d780b940234208a252322c24a93b442ca714d119e68086c"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:89d75e7293d2b3e674db7d4d9b1bee7f8f3d1609428e293771d1a962617150cc"}, + {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f65557897fc977a44ab205ea871b690adaef6b9da6afda4790a2484b04293a5f"}, + {file = "regex-2024.11.6-cp38-cp38-win32.whl", hash = "sha256:6f44ec28b1f858c98d3036ad5d7d0bfc568bdd7a74f9c24e25f41ef1ebfd81a4"}, + {file = "regex-2024.11.6-cp38-cp38-win_amd64.whl", hash = "sha256:bb8f74f2f10dbf13a0be8de623ba4f9491faf58c24064f32b65679b021ed0001"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e"}, + {file = "regex-2024.11.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48"}, + {file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f"}, + {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b"}, + {file = "regex-2024.11.6-cp39-cp39-win32.whl", hash = "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57"}, + {file = "regex-2024.11.6-cp39-cp39-win_amd64.whl", hash = "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983"}, + {file = "regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519"}, +] + +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "responses" +version = "0.25.7" +description = "A utility library for mocking out the `requests` Python library." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "responses-0.25.7-py3-none-any.whl", hash = "sha256:92ca17416c90fe6b35921f52179bff29332076bb32694c0df02dcac2c6bc043c"}, + {file = "responses-0.25.7.tar.gz", hash = "sha256:8ebae11405d7a5df79ab6fd54277f6f2bc29b2d002d0dd2d5c632594d1ddcedb"}, +] + +[package.dependencies] +pyyaml = "*" +requests = ">=2.30.0,<3.0" +urllib3 = ">=1.25.10,<3.0" + +[package.extras] +tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli ; python_version < \"3.11\"", "tomli-w", "types-PyYAML", "types-requests"] + +[[package]] +name = "rpds-py" +version = "0.24.0" +description = "Python bindings to Rust's persistent data structures (rpds)" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "rpds_py-0.24.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:006f4342fe729a368c6df36578d7a348c7c716be1da0a1a0f86e3021f8e98724"}, + {file = "rpds_py-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2d53747da70a4e4b17f559569d5f9506420966083a31c5fbd84e764461c4444b"}, + {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8acd55bd5b071156bae57b555f5d33697998752673b9de554dd82f5b5352727"}, + {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7e80d375134ddb04231a53800503752093dbb65dad8dabacce2c84cccc78e964"}, + {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60748789e028d2a46fc1c70750454f83c6bdd0d05db50f5ae83e2db500b34da5"}, + {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e1daf5bf6c2be39654beae83ee6b9a12347cb5aced9a29eecf12a2d25fff664"}, + {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b221c2457d92a1fb3c97bee9095c874144d196f47c038462ae6e4a14436f7bc"}, + {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:66420986c9afff67ef0c5d1e4cdc2d0e5262f53ad11e4f90e5e22448df485bf0"}, + {file = "rpds_py-0.24.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:43dba99f00f1d37b2a0265a259592d05fcc8e7c19d140fe51c6e6f16faabeb1f"}, + {file = "rpds_py-0.24.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a88c0d17d039333a41d9bf4616bd062f0bd7aa0edeb6cafe00a2fc2a804e944f"}, + {file = "rpds_py-0.24.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc31e13ce212e14a539d430428cd365e74f8b2d534f8bc22dd4c9c55b277b875"}, + {file = "rpds_py-0.24.0-cp310-cp310-win32.whl", hash = "sha256:fc2c1e1b00f88317d9de6b2c2b39b012ebbfe35fe5e7bef980fd2a91f6100a07"}, + {file = "rpds_py-0.24.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0145295ca415668420ad142ee42189f78d27af806fcf1f32a18e51d47dd2052"}, + {file = "rpds_py-0.24.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2d3ee4615df36ab8eb16c2507b11e764dcc11fd350bbf4da16d09cda11fcedef"}, + {file = "rpds_py-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e13ae74a8a3a0c2f22f450f773e35f893484fcfacb00bb4344a7e0f4f48e1f97"}, + {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf86f72d705fc2ef776bb7dd9e5fbba79d7e1f3e258bf9377f8204ad0fc1c51e"}, + {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c43583ea8517ed2e780a345dd9960896afc1327e8cf3ac8239c167530397440d"}, + {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4cd031e63bc5f05bdcda120646a0d32f6d729486d0067f09d79c8db5368f4586"}, + {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34d90ad8c045df9a4259c47d2e16a3f21fdb396665c94520dbfe8766e62187a4"}, + {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e838bf2bb0b91ee67bf2b889a1a841e5ecac06dd7a2b1ef4e6151e2ce155c7ae"}, + {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04ecf5c1ff4d589987b4d9882872f80ba13da7d42427234fce8f22efb43133bc"}, + {file = "rpds_py-0.24.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:630d3d8ea77eabd6cbcd2ea712e1c5cecb5b558d39547ac988351195db433f6c"}, + {file = "rpds_py-0.24.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ebcb786b9ff30b994d5969213a8430cbb984cdd7ea9fd6df06663194bd3c450c"}, + {file = "rpds_py-0.24.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:174e46569968ddbbeb8a806d9922f17cd2b524aa753b468f35b97ff9c19cb718"}, + {file = "rpds_py-0.24.0-cp311-cp311-win32.whl", hash = "sha256:5ef877fa3bbfb40b388a5ae1cb00636a624690dcb9a29a65267054c9ea86d88a"}, + {file = "rpds_py-0.24.0-cp311-cp311-win_amd64.whl", hash = "sha256:e274f62cbd274359eff63e5c7e7274c913e8e09620f6a57aae66744b3df046d6"}, + {file = "rpds_py-0.24.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d8551e733626afec514b5d15befabea0dd70a343a9f23322860c4f16a9430205"}, + {file = "rpds_py-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e374c0ce0ca82e5b67cd61fb964077d40ec177dd2c4eda67dba130de09085c7"}, + {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d69d003296df4840bd445a5d15fa5b6ff6ac40496f956a221c4d1f6f7b4bc4d9"}, + {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8212ff58ac6dfde49946bea57474a386cca3f7706fc72c25b772b9ca4af6b79e"}, + {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:528927e63a70b4d5f3f5ccc1fa988a35456eb5d15f804d276709c33fc2f19bda"}, + {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a824d2c7a703ba6daaca848f9c3d5cb93af0505be505de70e7e66829affd676e"}, + {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44d51febb7a114293ffd56c6cf4736cb31cd68c0fddd6aa303ed09ea5a48e029"}, + {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3fab5f4a2c64a8fb64fc13b3d139848817a64d467dd6ed60dcdd6b479e7febc9"}, + {file = "rpds_py-0.24.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9be4f99bee42ac107870c61dfdb294d912bf81c3c6d45538aad7aecab468b6b7"}, + {file = "rpds_py-0.24.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:564c96b6076a98215af52f55efa90d8419cc2ef45d99e314fddefe816bc24f91"}, + {file = "rpds_py-0.24.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:75a810b7664c17f24bf2ffd7f92416c00ec84b49bb68e6a0d93e542406336b56"}, + {file = "rpds_py-0.24.0-cp312-cp312-win32.whl", hash = "sha256:f6016bd950be4dcd047b7475fdf55fb1e1f59fc7403f387be0e8123e4a576d30"}, + {file = "rpds_py-0.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:998c01b8e71cf051c28f5d6f1187abbdf5cf45fc0efce5da6c06447cba997034"}, + {file = "rpds_py-0.24.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3d2d8e4508e15fc05b31285c4b00ddf2e0eb94259c2dc896771966a163122a0c"}, + {file = "rpds_py-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f00c16e089282ad68a3820fd0c831c35d3194b7cdc31d6e469511d9bffc535c"}, + {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951cc481c0c395c4a08639a469d53b7d4afa252529a085418b82a6b43c45c240"}, + {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9ca89938dff18828a328af41ffdf3902405a19f4131c88e22e776a8e228c5a8"}, + {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0ef550042a8dbcd657dfb284a8ee00f0ba269d3f2286b0493b15a5694f9fe8"}, + {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b2356688e5d958c4d5cb964af865bea84db29971d3e563fb78e46e20fe1848b"}, + {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78884d155fd15d9f64f5d6124b486f3d3f7fd7cd71a78e9670a0f6f6ca06fb2d"}, + {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6a4a535013aeeef13c5532f802708cecae8d66c282babb5cd916379b72110cf7"}, + {file = "rpds_py-0.24.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:84e0566f15cf4d769dade9b366b7b87c959be472c92dffb70462dd0844d7cbad"}, + {file = "rpds_py-0.24.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:823e74ab6fbaa028ec89615ff6acb409e90ff45580c45920d4dfdddb069f2120"}, + {file = "rpds_py-0.24.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c61a2cb0085c8783906b2f8b1f16a7e65777823c7f4d0a6aaffe26dc0d358dd9"}, + {file = "rpds_py-0.24.0-cp313-cp313-win32.whl", hash = "sha256:60d9b630c8025b9458a9d114e3af579a2c54bd32df601c4581bd054e85258143"}, + {file = "rpds_py-0.24.0-cp313-cp313-win_amd64.whl", hash = "sha256:6eea559077d29486c68218178ea946263b87f1c41ae7f996b1f30a983c476a5a"}, + {file = "rpds_py-0.24.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:d09dc82af2d3c17e7dd17120b202a79b578d79f2b5424bda209d9966efeed114"}, + {file = "rpds_py-0.24.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5fc13b44de6419d1e7a7e592a4885b323fbc2f46e1f22151e3a8ed3b8b920405"}, + {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c347a20d79cedc0a7bd51c4d4b7dbc613ca4e65a756b5c3e57ec84bd43505b47"}, + {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20f2712bd1cc26a3cc16c5a1bfee9ed1abc33d4cdf1aabd297fe0eb724df4272"}, + {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aad911555286884be1e427ef0dc0ba3929e6821cbeca2194b13dc415a462c7fd"}, + {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0aeb3329c1721c43c58cae274d7d2ca85c1690d89485d9c63a006cb79a85771a"}, + {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a0f156e9509cee987283abd2296ec816225145a13ed0391df8f71bf1d789e2d"}, + {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aa6800adc8204ce898c8a424303969b7aa6a5e4ad2789c13f8648739830323b7"}, + {file = "rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a18fc371e900a21d7392517c6f60fe859e802547309e94313cd8181ad9db004d"}, + {file = "rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9168764133fd919f8dcca2ead66de0105f4ef5659cbb4fa044f7014bed9a1797"}, + {file = "rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f6e3cec44ba05ee5cbdebe92d052f69b63ae792e7d05f1020ac5e964394080c"}, + {file = "rpds_py-0.24.0-cp313-cp313t-win32.whl", hash = "sha256:8ebc7e65ca4b111d928b669713865f021b7773350eeac4a31d3e70144297baba"}, + {file = "rpds_py-0.24.0-cp313-cp313t-win_amd64.whl", hash = "sha256:675269d407a257b8c00a6b58205b72eec8231656506c56fd429d924ca00bb350"}, + {file = "rpds_py-0.24.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a36b452abbf29f68527cf52e181fced56685731c86b52e852053e38d8b60bc8d"}, + {file = "rpds_py-0.24.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b3b397eefecec8e8e39fa65c630ef70a24b09141a6f9fc17b3c3a50bed6b50e"}, + {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdabcd3beb2a6dca7027007473d8ef1c3b053347c76f685f5f060a00327b8b65"}, + {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5db385bacd0c43f24be92b60c857cf760b7f10d8234f4bd4be67b5b20a7c0b6b"}, + {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8097b3422d020ff1c44effc40ae58e67d93e60d540a65649d2cdaf9466030791"}, + {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:493fe54318bed7d124ce272fc36adbf59d46729659b2c792e87c3b95649cdee9"}, + {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8aa362811ccdc1f8dadcc916c6d47e554169ab79559319ae9fae7d7752d0d60c"}, + {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d8f9a6e7fd5434817526815f09ea27f2746c4a51ee11bb3439065f5fc754db58"}, + {file = "rpds_py-0.24.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8205ee14463248d3349131bb8099efe15cd3ce83b8ef3ace63c7e976998e7124"}, + {file = "rpds_py-0.24.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:921ae54f9ecba3b6325df425cf72c074cd469dea843fb5743a26ca7fb2ccb149"}, + {file = "rpds_py-0.24.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:32bab0a56eac685828e00cc2f5d1200c548f8bc11f2e44abf311d6b548ce2e45"}, + {file = "rpds_py-0.24.0-cp39-cp39-win32.whl", hash = "sha256:f5c0ed12926dec1dfe7d645333ea59cf93f4d07750986a586f511c0bc61fe103"}, + {file = "rpds_py-0.24.0-cp39-cp39-win_amd64.whl", hash = "sha256:afc6e35f344490faa8276b5f2f7cbf71f88bc2cda4328e00553bd451728c571f"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:619ca56a5468f933d940e1bf431c6f4e13bef8e688698b067ae68eb4f9b30e3a"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:4b28e5122829181de1898c2c97f81c0b3246d49f585f22743a1246420bb8d399"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e5ab32cf9eb3647450bc74eb201b27c185d3857276162c101c0f8c6374e098"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:208b3a70a98cf3710e97cabdc308a51cd4f28aa6e7bb11de3d56cd8b74bab98d"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbc4362e06f950c62cad3d4abf1191021b2ffaf0b31ac230fbf0526453eee75e"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ebea2821cdb5f9fef44933617be76185b80150632736f3d76e54829ab4a3b4d1"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4df06c35465ef4d81799999bba810c68d29972bf1c31db61bfdb81dd9d5bb"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3aa13bdf38630da298f2e0d77aca967b200b8cc1473ea05248f6c5e9c9bdb44"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:041f00419e1da7a03c46042453598479f45be3d787eb837af382bfc169c0db33"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:d8754d872a5dfc3c5bf9c0e059e8107451364a30d9fd50f1f1a85c4fb9481164"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:896c41007931217a343eff197c34513c154267636c8056fb409eafd494c3dcdc"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:92558d37d872e808944c3c96d0423b8604879a3d1c86fdad508d7ed91ea547d5"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f9e0057a509e096e47c87f753136c9b10d7a91842d8042c2ee6866899a717c0d"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6e109a454412ab82979c5b1b3aee0604eca4bbf9a02693bb9df027af2bfa91a"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc1c892b1ec1f8cbd5da8de287577b455e388d9c328ad592eabbdcb6fc93bee5"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c39438c55983d48f4bb3487734d040e22dad200dab22c41e331cee145e7a50d"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d7e8ce990ae17dda686f7e82fd41a055c668e13ddcf058e7fb5e9da20b57793"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9ea7f4174d2e4194289cb0c4e172d83e79a6404297ff95f2875cf9ac9bced8ba"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb2954155bb8f63bb19d56d80e5e5320b61d71084617ed89efedb861a684baea"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04f2b712a2206e13800a8136b07aaedc23af3facab84918e7aa89e4be0260032"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:eda5c1e2a715a4cbbca2d6d304988460942551e4e5e3b7457b50943cd741626d"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:9abc80fe8c1f87218db116016de575a7998ab1629078c90840e8d11ab423ee25"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6a727fd083009bc83eb83d6950f0c32b3c94c8b80a9b667c87f4bd1274ca30ba"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e0f3ef95795efcd3b2ec3fe0a5bcfb5dadf5e3996ea2117427e524d4fbf309c6"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:2c13777ecdbbba2077670285dd1fe50828c8742f6a4119dbef6f83ea13ad10fb"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79e8d804c2ccd618417e96720ad5cd076a86fa3f8cb310ea386a3e6229bae7d1"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd822f019ccccd75c832deb7aa040bb02d70a92eb15a2f16c7987b7ad4ee8d83"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0047638c3aa0dbcd0ab99ed1e549bbf0e142c9ecc173b6492868432d8989a046"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5b66d1b201cc71bc3081bc2f1fc36b0c1f268b773e03bbc39066651b9e18391"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbcbb6db5582ea33ce46a5d20a5793134b5365110d84df4e30b9d37c6fd40ad3"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:63981feca3f110ed132fd217bf7768ee8ed738a55549883628ee3da75bb9cb78"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:3a55fc10fdcbf1a4bd3c018eea422c52cf08700cf99c28b5cb10fe97ab77a0d3"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:c30ff468163a48535ee7e9bf21bd14c7a81147c0e58a36c1078289a8ca7af0bd"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:369d9c6d4c714e36d4a03957b4783217a3ccd1e222cdd67d464a3a479fc17796"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:24795c099453e3721fda5d8ddd45f5dfcc8e5a547ce7b8e9da06fecc3832e26f"}, + {file = "rpds_py-0.24.0.tar.gz", hash = "sha256:772cc1b2cd963e7e17e6cc55fe0371fb9c704d63e44cacec7b9b7f523b78919e"}, +] + +[[package]] +name = "rsa" +version = "4.9" +description = "Pure-Python RSA implementation" +optional = false +python-versions = ">=3.6,<4" +groups = ["main"] +files = [ + {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, + {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, +] + +[package.dependencies] +pyasn1 = ">=0.1.3" + +[[package]] +name = "ruff" +version = "0.11.13" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46"}, + {file = "ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48"}, + {file = "ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b"}, + {file = "ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a"}, + {file = "ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc"}, + {file = "ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629"}, + {file = "ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933"}, + {file = "ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165"}, + {file = "ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71"}, + {file = "ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9"}, + {file = "ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc"}, + {file = "ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7"}, + {file = "ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432"}, + {file = "ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492"}, + {file = "ruff-0.11.13-py3-none-win32.whl", hash = "sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250"}, + {file = "ruff-0.11.13-py3-none-win_amd64.whl", hash = "sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3"}, + {file = "ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b"}, + {file = "ruff-0.11.13.tar.gz", hash = "sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514"}, +] + +[[package]] +name = "s3transfer" +version = "0.11.4" +description = "An Amazon S3 Transfer Manager" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "s3transfer-0.11.4-py3-none-any.whl", hash = "sha256:ac265fa68318763a03bf2dc4f39d5cbd6a9e178d81cc9483ad27da33637e320d"}, + {file = "s3transfer-0.11.4.tar.gz", hash = "sha256:559f161658e1cf0a911f45940552c696735f5c74e64362e515f333ebed87d679"}, +] + +[package.dependencies] +botocore = ">=1.37.4,<2.0a.0" + +[package.extras] +crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"] + +[[package]] +name = "sdc-cryptography" +version = "1.2.1" +description = "A shared library for SDC services that use JWT with JWE" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "sdc_cryptography-1.2.1-py3-none-any.whl", hash = "sha256:2b87da178c0d9c6a8add4e2afbe81139bbb056598af0cda61b52f421fb29451f"}, + {file = "sdc_cryptography-1.2.1.tar.gz", hash = "sha256:cea2de62e65940efb72bfd3ed677b1e685678521a52eb4c06769ffce506f5847"}, +] + +[package.dependencies] +cryptography = "*" +jwcrypto = "*" +PyYAML = "*" + +[[package]] +name = "setuptools" +version = "78.1.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "setuptools-78.1.0-py3-none-any.whl", hash = "sha256:3e386e96793c8702ae83d17b853fb93d3e09ef82ec62722e61da5cd22376dcd8"}, + {file = "setuptools-78.1.0.tar.gz", hash = "sha256:18fd474d4a82a5f83dac888df697af65afa82dec7323d09c3e37d1f14288da54"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] + +[[package]] +name = "simplejson" +version = "3.20.1" +description = "Simple, fast, extensible JSON encoder/decoder for Python" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.5" +groups = ["main"] +files = [ + {file = "simplejson-3.20.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:f5272b5866b259fe6c33c4a8c5073bf8b359c3c97b70c298a2f09a69b52c7c41"}, + {file = "simplejson-3.20.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5c0de368f3052a59a1acf21f8b2dd28686a9e4eba2da7efae7ed9554cb31e7bc"}, + {file = "simplejson-3.20.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:0821871404a537fd0e22eba240c74c0467c28af6cc435903eca394cfc74a0497"}, + {file = "simplejson-3.20.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:c939a1e576bded47d7d03aa2afc2ae90b928b2cf1d9dc2070ceec51fd463f430"}, + {file = "simplejson-3.20.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:3c4f0a61cdc05550782ca4a2cdb311ea196c2e6be6b24a09bf71360ca8c3ca9b"}, + {file = "simplejson-3.20.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:6c21f5c026ca633cfffcb6bc1fac2e99f65cb2b24657d3bef21aed9916cc3bbf"}, + {file = "simplejson-3.20.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:8d23b7f8d6b72319d6d55a0261089ff621ce87e54731c2d3de6a9bf7be5c028c"}, + {file = "simplejson-3.20.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:cda5c32a98f392909088111ecec23f2b0d39346ceae1a0fea23ab2d1f84ec21d"}, + {file = "simplejson-3.20.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e580aa65d5f6c3bf41b9b4afe74be5d5ddba9576701c107c772d936ea2b5043a"}, + {file = "simplejson-3.20.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4a586ce4f78cec11f22fe55c5bee0f067e803aab9bad3441afe2181693b5ebb5"}, + {file = "simplejson-3.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74a1608f9e6e8c27a4008d70a54270868306d80ed48c9df7872f9f4b8ac87808"}, + {file = "simplejson-3.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03db8cb64154189a92a7786209f24e391644f3a3fa335658be2df2af1960b8d8"}, + {file = "simplejson-3.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eea7e2b7d858f6fdfbf0fe3cb846d6bd8a45446865bc09960e51f3d473c2271b"}, + {file = "simplejson-3.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e66712b17d8425bb7ff8968d4c7c7fd5a2dd7bd63728b28356223c000dd2f91f"}, + {file = "simplejson-3.20.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2cc4f6486f9f515b62f5831ff1888886619b84fc837de68f26d919ba7bbdcbc"}, + {file = "simplejson-3.20.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a3c2df555ee4016148fa192e2b9cd9e60bc1d40769366134882685e90aee2a1e"}, + {file = "simplejson-3.20.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:78520f04b7548a5e476b5396c0847e066f1e0a4c0c5e920da1ad65e95f410b11"}, + {file = "simplejson-3.20.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f4bd49ecde87b0fe9f55cc971449a32832bca9910821f7072bbfae1155eaa007"}, + {file = "simplejson-3.20.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7eaae2b88eb5da53caaffdfa50e2e12022553949b88c0df4f9a9663609373f72"}, + {file = "simplejson-3.20.1-cp310-cp310-win32.whl", hash = "sha256:e836fb88902799eac8debc2b642300748f4860a197fa3d9ea502112b6bb8e142"}, + {file = "simplejson-3.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:b122a19b552b212fc3b5b96fc5ce92333d4a9ac0a800803e1f17ebb16dac4be5"}, + {file = "simplejson-3.20.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:325b8c107253d3217e89d7b50c71015b5b31e2433e6c5bf38967b2f80630a8ca"}, + {file = "simplejson-3.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88a7baa8211089b9e58d78fbc1b0b322103f3f3d459ff16f03a36cece0d0fcf0"}, + {file = "simplejson-3.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:299b1007b8101d50d95bc0db1bf5c38dc372e85b504cf77f596462083ee77e3f"}, + {file = "simplejson-3.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ec618ed65caab48e81e3ed29586236a8e57daef792f1f3bb59504a7e98cd10"}, + {file = "simplejson-3.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2cdead1d3197f0ff43373cf4730213420523ba48697743e135e26f3d179f38"}, + {file = "simplejson-3.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3466d2839fdc83e1af42e07b90bc8ff361c4e8796cd66722a40ba14e458faddd"}, + {file = "simplejson-3.20.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d492ed8e92f3a9f9be829205f44b1d0a89af6582f0cf43e0d129fa477b93fe0c"}, + {file = "simplejson-3.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f924b485537b640dc69434565463fd6fc0c68c65a8c6e01a823dd26c9983cf79"}, + {file = "simplejson-3.20.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e8eacf6a3491bf76ea91a8d46726368a6be0eb94993f60b8583550baae9439e"}, + {file = "simplejson-3.20.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d34d04bf90b4cea7c22d8b19091633908f14a096caa301b24c2f3d85b5068fb8"}, + {file = "simplejson-3.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:69dd28d4ce38390ea4aaf212902712c0fd1093dc4c1ff67e09687c3c3e15a749"}, + {file = "simplejson-3.20.1-cp311-cp311-win32.whl", hash = "sha256:dfe7a9da5fd2a3499436cd350f31539e0a6ded5da6b5b3d422df016444d65e43"}, + {file = "simplejson-3.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:896a6c04d7861d507d800da7642479c3547060bf97419d9ef73d98ced8258766"}, + {file = "simplejson-3.20.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f31c4a3a7ab18467ee73a27f3e59158255d1520f3aad74315edde7a940f1be23"}, + {file = "simplejson-3.20.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:884e6183d16b725e113b83a6fc0230152ab6627d4d36cb05c89c2c5bccfa7bc6"}, + {file = "simplejson-3.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03d7a426e416fe0d3337115f04164cd9427eb4256e843a6b8751cacf70abc832"}, + {file = "simplejson-3.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:000602141d0bddfcff60ea6a6e97d5e10c9db6b17fd2d6c66199fa481b6214bb"}, + {file = "simplejson-3.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af8377a8af78226e82e3a4349efdde59ffa421ae88be67e18cef915e4023a595"}, + {file = "simplejson-3.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15c7de4c88ab2fbcb8781a3b982ef883696736134e20b1210bca43fb42ff1acf"}, + {file = "simplejson-3.20.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:455a882ff3f97d810709f7b620007d4e0aca8da71d06fc5c18ba11daf1c4df49"}, + {file = "simplejson-3.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fc0f523ce923e7f38eb67804bc80e0a028c76d7868500aa3f59225574b5d0453"}, + {file = "simplejson-3.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76461ec929282dde4a08061071a47281ad939d0202dc4e63cdd135844e162fbc"}, + {file = "simplejson-3.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ab19c2da8c043607bde4d4ef3a6b633e668a7d2e3d56f40a476a74c5ea71949f"}, + {file = "simplejson-3.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b2578bedaedf6294415197b267d4ef678fea336dd78ee2a6d2f4b028e9d07be3"}, + {file = "simplejson-3.20.1-cp312-cp312-win32.whl", hash = "sha256:339f407373325a36b7fd744b688ba5bae0666b5d340ec6d98aebc3014bf3d8ea"}, + {file = "simplejson-3.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:627d4486a1ea7edf1f66bb044ace1ce6b4c1698acd1b05353c97ba4864ea2e17"}, + {file = "simplejson-3.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:71e849e7ceb2178344998cbe5ade101f1b329460243c79c27fbfc51c0447a7c3"}, + {file = "simplejson-3.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b63fdbab29dc3868d6f009a59797cefaba315fd43cd32ddd998ee1da28e50e29"}, + {file = "simplejson-3.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1190f9a3ce644fd50ec277ac4a98c0517f532cfebdcc4bd975c0979a9f05e1fb"}, + {file = "simplejson-3.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1336ba7bcb722ad487cd265701ff0583c0bb6de638364ca947bb84ecc0015d1"}, + {file = "simplejson-3.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e975aac6a5acd8b510eba58d5591e10a03e3d16c1cf8a8624ca177491f7230f0"}, + {file = "simplejson-3.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a6dd11ee282937ad749da6f3b8d87952ad585b26e5edfa10da3ae2536c73078"}, + {file = "simplejson-3.20.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab980fcc446ab87ea0879edad41a5c28f2d86020014eb035cf5161e8de4474c6"}, + {file = "simplejson-3.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f5aee2a4cb6b146bd17333ac623610f069f34e8f31d2f4f0c1a2186e50c594f0"}, + {file = "simplejson-3.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:652d8eecbb9a3b6461b21ec7cf11fd0acbab144e45e600c817ecf18e4580b99e"}, + {file = "simplejson-3.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:8c09948f1a486a89251ee3a67c9f8c969b379f6ffff1a6064b41fea3bce0a112"}, + {file = "simplejson-3.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cbbd7b215ad4fc6f058b5dd4c26ee5c59f72e031dfda3ac183d7968a99e4ca3a"}, + {file = "simplejson-3.20.1-cp313-cp313-win32.whl", hash = "sha256:ae81e482476eaa088ef9d0120ae5345de924f23962c0c1e20abbdff597631f87"}, + {file = "simplejson-3.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:1b9fd15853b90aec3b1739f4471efbf1ac05066a2c7041bf8db821bb73cd2ddc"}, + {file = "simplejson-3.20.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c7edf279c1376f28bf41e916c015a2a08896597869d57d621f55b6a30c7e1e6d"}, + {file = "simplejson-3.20.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9202b9de38f12e99a40addd1a8d508a13c77f46d87ab1f9095f154667f4fe81"}, + {file = "simplejson-3.20.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:391345b4157cc4e120027e013bd35c45e2c191e2bf48b8913af488cdc3b9243c"}, + {file = "simplejson-3.20.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c6fdcc9debb711ddd2ad6d69f9386a3d9e8e253234bbb30513e0a7caa9510c51"}, + {file = "simplejson-3.20.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9daf8cdc7ee8a9e9f7a3b313ba0a003391857e90d0e82fbcd4d614aa05cb7c3b"}, + {file = "simplejson-3.20.1-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:c02f4868a3a46ffe284a51a88d134dc96feff6079a7115164885331a1ba8ed9f"}, + {file = "simplejson-3.20.1-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:3d7310172d5340febd258cb147f46aae30ad57c445f4d7e1ae8461c10aaf43b0"}, + {file = "simplejson-3.20.1-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:4762e05577955312a4c6802f58dd02e040cc79ae59cda510aa1564d84449c102"}, + {file = "simplejson-3.20.1-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:8bb98fdf318c05aefd08a92583bd6ee148e93c6756fb1befb7b2d5f27824be78"}, + {file = "simplejson-3.20.1-cp36-cp36m-win32.whl", hash = "sha256:9a74e70818818981294b8e6956ce3496c5e1bd4726ac864fae473197671f7b85"}, + {file = "simplejson-3.20.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e041add470e8f8535cc05509485eb7205729a84441f03b25cde80ad48823792e"}, + {file = "simplejson-3.20.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7e9d73f46119240e4f4f07868241749d67d09873f40cb968d639aa9ccc488b86"}, + {file = "simplejson-3.20.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae6e637dc24f8fee332ed23dd070e81394138e42cd4fd9d0923e5045ba122e27"}, + {file = "simplejson-3.20.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:efd3bc6c6b17e3d4620eb6be5196f0d1c08b6ce7c3101fa8e292b79e0908944b"}, + {file = "simplejson-3.20.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87fc623d457173a0213bc9ca4e346b83c9d443f63ed5cca847fb0cacea3cfc95"}, + {file = "simplejson-3.20.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec6a1e0a7aff76f0e008bebfa950188b9c50b58c1885d898145f48fc8e189a56"}, + {file = "simplejson-3.20.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:9c079606f461a6e950099167e21e13985147c8a24be8eea66c9ad68f73fad744"}, + {file = "simplejson-3.20.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:9faceb68fba27ef17eda306e4cd97a7b4b14fdadca5fbb15790ba8b26ebeec0c"}, + {file = "simplejson-3.20.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:7ceed598e4bacbf5133fe7a418f7991bb2df0683f3ac11fbf9e36a2bc7aa4b85"}, + {file = "simplejson-3.20.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:ede69c765e9901861ad7c6139023b7b7d5807c48a2539d817b4ab40018002d5f"}, + {file = "simplejson-3.20.1-cp37-cp37m-win32.whl", hash = "sha256:d8853c269a4c5146ddca4aa7c70e631795e9d11239d5fedb1c6bbc91ffdebcac"}, + {file = "simplejson-3.20.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ed6a17fd397f0e2b3ad668fc9e19253ed2e3875ad9086bd7f795c29a3223f4a1"}, + {file = "simplejson-3.20.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:7551682b60bba3a9e2780742e101cf0a64250e76de7d09b1c4b0c8a7c7cc6834"}, + {file = "simplejson-3.20.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bd9577ec1c8c3a43040e3787711e4c257c70035b7551a21854b5dec88dad09e1"}, + {file = "simplejson-3.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8e197e4cf6d42c2c57e7c52cd7c1e7b3e37c5911df1314fb393320131e2101"}, + {file = "simplejson-3.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bd09c8c75666e7f62a33d2f1fb57f81da1fcbb19a9fe7d7910b5756e1dd6048"}, + {file = "simplejson-3.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bd6bfe5678d73fbd5328eea6a35216503796428fc47f1237432522febaf3a0c"}, + {file = "simplejson-3.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:71b75d448fd0ceb2e7c90e72bb82c41f8462550d48529980bc0bab1d2495bfbb"}, + {file = "simplejson-3.20.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7e15b716d09f318c8cda3e20f82fae81684ce3d3acd1d7770fa3007df1769de"}, + {file = "simplejson-3.20.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3e7963197d958fcf9e98b212b80977d56c022384621ff463d98afc3b6b1ce7e8"}, + {file = "simplejson-3.20.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:2e671dd62051129185d3a9a92c60101f56cbc174854a1a3dfb69114ebd9e1699"}, + {file = "simplejson-3.20.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e25b2a0c396f3b84fb89573d07b0e1846ed563eb364f2ea8230ca92b8a8cb786"}, + {file = "simplejson-3.20.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:489c3a43116082bad56795215786313832ba3991cca1f55838e52a553f451ab6"}, + {file = "simplejson-3.20.1-cp38-cp38-win32.whl", hash = "sha256:4a92e948bad8df7fa900ba2ba0667a98303f3db206cbaac574935c332838208e"}, + {file = "simplejson-3.20.1-cp38-cp38-win_amd64.whl", hash = "sha256:49d059b8363327eee3c94799dd96782314b2dbd7bcc293b4ad48db69d6f4d362"}, + {file = "simplejson-3.20.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a8011f1dd1d676befcd4d675ebdbfdbbefd3bf350052b956ba8c699fca7d8cef"}, + {file = "simplejson-3.20.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e91703a4c5fec53e36875ae426ad785f4120bd1d93b65bed4752eeccd1789e0c"}, + {file = "simplejson-3.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e39eaa57c7757daa25bcd21f976c46be443b73dd6c3da47fe5ce7b7048ccefe2"}, + {file = "simplejson-3.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceab2ce2acdc7fbaa433a93006758db6ba9a659e80c4faa13b80b9d2318e9b17"}, + {file = "simplejson-3.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d4f320c33277a5b715db5bf5b10dae10c19076bd6d66c2843e04bd12d1f1ea5"}, + {file = "simplejson-3.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b6436c48e64378fa844d8c9e58a5ed0352bbcfd4028369a9b46679b7ab79d2d"}, + {file = "simplejson-3.20.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e18345c8dda5d699be8166b61f9d80aaee4545b709f1363f60813dc032dac53"}, + {file = "simplejson-3.20.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:90b573693d1526bed576f6817e2a492eaaef68f088b57d7a9e83d122bbb49e51"}, + {file = "simplejson-3.20.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:272cc767826e924a6bd369ea3dbf18e166ded29059c7a4d64d21a9a22424b5b5"}, + {file = "simplejson-3.20.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:51b41f284d603c4380732d7d619f8b34bd04bc4aa0ed0ed5f4ffd0539b14da44"}, + {file = "simplejson-3.20.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6e6697a3067d281f01de0fe96fc7cba4ea870d96d7deb7bfcf85186d74456503"}, + {file = "simplejson-3.20.1-cp39-cp39-win32.whl", hash = "sha256:6dd3a1d5aca87bf947f3339b0f8e8e329f1badf548bdbff37fac63c17936da8e"}, + {file = "simplejson-3.20.1-cp39-cp39-win_amd64.whl", hash = "sha256:463f1fca8fbf23d088e5850fdd0dd4d5faea8900a9f9680270bd98fd649814ca"}, + {file = "simplejson-3.20.1-py3-none-any.whl", hash = "sha256:8a6c1bbac39fa4a79f83cbf1df6ccd8ff7069582a9fd8db1e52cea073bc2c697"}, + {file = "simplejson-3.20.1.tar.gz", hash = "sha256:e64139b4ec4f1f24c142ff7dcafe55a22b811a74d86d66560c8815687143037d"}, +] + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main", "dev"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, +] + +[[package]] +name = "soupsieve" +version = "2.6" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, + {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, +] + +[[package]] +name = "structlog" +version = "25.2.0" +description = "Structured Logging for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "structlog-25.2.0-py3-none-any.whl", hash = "sha256:0fecea2e345d5d491b72f3db2e5fcd6393abfc8cd06a4851f21fcd4d1a99f437"}, + {file = "structlog-25.2.0.tar.gz", hash = "sha256:d9f9776944207d1035b8b26072b9b140c63702fd7aa57c2f85d28ab701bd8e92"}, +] + +[package.extras] +dev = ["freezegun (>=0.2.8)", "mypy (>=1.4)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "rich", "simplejson", "twisted"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-mermaid", "sphinxext-opengraph", "twisted"] +tests = ["freezegun (>=0.2.8)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "simplejson"] +typing = ["mypy (>=1.4)", "rich", "twisted"] + +[[package]] +name = "termcolor" +version = "2.5.0" +description = "ANSI color formatting for output in terminal" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "termcolor-2.5.0-py3-none-any.whl", hash = "sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8"}, + {file = "termcolor-2.5.0.tar.gz", hash = "sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f"}, +] + +[package.extras] +tests = ["pytest", "pytest-cov"] + +[[package]] +name = "tomlkit" +version = "0.13.2" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, + {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +description = "Fast, Extensible Progress Meter" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, + {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] +discord = ["requests"] +notebook = ["ipywidgets (>=6)"] +slack = ["slack-sdk"] +telegram = ["requests"] + +[[package]] +name = "types-cachetools" +version = "6.0.0.20250525" +description = "Typing stubs for cachetools" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "types_cachetools-6.0.0.20250525-py3-none-any.whl", hash = "sha256:1de8f0fe4bdcb187a48d2026c1e3672830f67943ad2bf3486abe031b632f1252"}, + {file = "types_cachetools-6.0.0.20250525.tar.gz", hash = "sha256:baf06f234cac3aeb44c07893447ba03ecdb6c0742ba2607e28a35d38e6821b02"}, +] + +[[package]] +name = "types-cffi" +version = "1.17.0.20250326" +description = "Typing stubs for cffi" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "types_cffi-1.17.0.20250326-py3-none-any.whl", hash = "sha256:5af4ecd7374ae0d5fa9e80864e8d4b31088cc32c51c544e3af7ed5b5ed681447"}, + {file = "types_cffi-1.17.0.20250326.tar.gz", hash = "sha256:6c8fea2c2f34b55e5fb77b1184c8ad849d57cf0ddccbc67a62121ac4b8b32254"}, +] + +[package.dependencies] +types-setuptools = "*" + +[[package]] +name = "types-pyopenssl" +version = "24.1.0.20240722" +description = "Typing stubs for pyOpenSSL" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "types-pyOpenSSL-24.1.0.20240722.tar.gz", hash = "sha256:47913b4678a01d879f503a12044468221ed8576263c1540dcb0484ca21b08c39"}, + {file = "types_pyOpenSSL-24.1.0.20240722-py3-none-any.whl", hash = "sha256:6a7a5d2ec042537934cfb4c9d4deb0e16c4c6250b09358df1f083682fe6fda54"}, +] + +[package.dependencies] +cryptography = ">=35.0.0" +types-cffi = "*" + +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20250516" +description = "Typing stubs for python-dateutil" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "types_python_dateutil-2.9.0.20250516-py3-none-any.whl", hash = "sha256:2b2b3f57f9c6a61fba26a9c0ffb9ea5681c9b83e69cd897c6b5f668d9c0cab93"}, + {file = "types_python_dateutil-2.9.0.20250516.tar.gz", hash = "sha256:13e80d6c9c47df23ad773d54b2826bd52dbbb41be87c3f339381c1700ad21ee5"}, +] + +[[package]] +name = "types-pytz" +version = "2025.2.0.20250516" +description = "Typing stubs for pytz" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "types_pytz-2025.2.0.20250516-py3-none-any.whl", hash = "sha256:e0e0c8a57e2791c19f718ed99ab2ba623856b11620cb6b637e5f62ce285a7451"}, + {file = "types_pytz-2025.2.0.20250516.tar.gz", hash = "sha256:e1216306f8c0d5da6dafd6492e72eb080c9a166171fa80dd7a1990fd8be7a7b3"}, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.20250516" +description = "Typing stubs for PyYAML" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "types_pyyaml-6.0.12.20250516-py3-none-any.whl", hash = "sha256:8478208feaeb53a34cb5d970c56a7cd76b72659442e733e268a94dc72b2d0530"}, + {file = "types_pyyaml-6.0.12.20250516.tar.gz", hash = "sha256:9f21a70216fc0fa1b216a8176db5f9e0af6eb35d2f2932acb87689d03a5bf6ba"}, +] + +[[package]] +name = "types-redis" +version = "4.6.0.20241004" +description = "Typing stubs for redis" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "types-redis-4.6.0.20241004.tar.gz", hash = "sha256:5f17d2b3f9091ab75384153bfa276619ffa1cf6a38da60e10d5e6749cc5b902e"}, + {file = "types_redis-4.6.0.20241004-py3-none-any.whl", hash = "sha256:ef5da68cb827e5f606c8f9c0b49eeee4c2669d6d97122f301d3a55dc6a63f6ed"}, +] + +[package.dependencies] +cryptography = ">=35.0.0" +types-pyOpenSSL = "*" + +[[package]] +name = "types-requests" +version = "2.32.4.20250611" +description = "Typing stubs for requests" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "types_requests-2.32.4.20250611-py3-none-any.whl", hash = "sha256:ad2fe5d3b0cb3c2c902c8815a70e7fb2302c4b8c1f77bdcd738192cdb3878072"}, + {file = "types_requests-2.32.4.20250611.tar.gz", hash = "sha256:741c8777ed6425830bf51e54d6abe245f79b4dcb9019f1622b773463946bf826"}, +] + +[package.dependencies] +urllib3 = ">=2" + +[[package]] +name = "types-setuptools" +version = "78.1.0.20250329" +description = "Typing stubs for setuptools" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "types_setuptools-78.1.0.20250329-py3-none-any.whl", hash = "sha256:ea47eab891afb506f470eee581dcde44d64dc99796665da794da6f83f50f6776"}, + {file = "types_setuptools-78.1.0.20250329.tar.gz", hash = "sha256:31e62950c38b8cc1c5114b077504e36426860a064287cac11b9666ab3a483234"}, +] + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "types-simplejson" +version = "3.20.0.20250326" +description = "Typing stubs for simplejson" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "types_simplejson-3.20.0.20250326-py3-none-any.whl", hash = "sha256:db1ddea7b8f7623b27a137578f22fc6c618db8c83ccfb1828ca0d2f0ec11efa7"}, + {file = "types_simplejson-3.20.0.20250326.tar.gz", hash = "sha256:b2689bc91e0e672d7a5a947b4cb546b76ae7ddc2899c6678e72a10bf96cd97d2"}, +] + +[[package]] +name = "typing-extensions" +version = "4.13.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "typing_extensions-4.13.0-py3-none-any.whl", hash = "sha256:c8dd92cc0d6425a97c18fbb9d1954e5ff92c1ca881a309c45f06ebc0b79058e5"}, + {file = "typing_extensions-4.13.0.tar.gz", hash = "sha256:0a4ac55a5820789d87e297727d229866c9650f6521b64206413c4fbada24d95b"}, +] + +[[package]] +name = "ua-parser" +version = "1.0.1" +description = "Python port of Browserscope's user agent parser" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "ua_parser-1.0.1-py3-none-any.whl", hash = "sha256:b059f2cb0935addea7e551251cbbf42e9a8872f86134163bc1a4f79e0945ffea"}, + {file = "ua_parser-1.0.1.tar.gz", hash = "sha256:f9d92bf19d4329019cef91707aecc23c6d65143ad7e29a233f0580fb0d15547d"}, +] + +[package.dependencies] +ua-parser-builtins = "*" + +[package.extras] +re2 = ["google-re2"] +regex = ["ua-parser-rs"] +yaml = ["PyYaml"] + +[[package]] +name = "ua-parser-builtins" +version = "0.18.0.post1" +description = "Precompiled rules for User Agent Parser" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "ua_parser_builtins-0.18.0.post1-py3-none-any.whl", hash = "sha256:eb4f93504040c3a990a6b0742a2afd540d87d7f9f05fd66e94c101db1564674d"}, +] + +[[package]] +name = "urllib3" +version = "2.3.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, + {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "uwsgi" +version = "2.0.28" +description = "The uWSGI server" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "uwsgi-2.0.28.tar.gz", hash = "sha256:79ca1891ef2df14508ab0471ee8c0eb94bd2d51d03f32f90c4bbe557ab1e99d0"}, +] + +[[package]] +name = "werkzeug" +version = "3.1.3" +description = "The comprehensive WSGI web application library." +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"}, + {file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"}, +] + +[package.dependencies] +MarkupSafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog (>=2.3)"] + +[[package]] +name = "wrapt" +version = "1.17.2" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984"}, + {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22"}, + {file = "wrapt-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7"}, + {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c"}, + {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72"}, + {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061"}, + {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2"}, + {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c"}, + {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62"}, + {file = "wrapt-1.17.2-cp310-cp310-win32.whl", hash = "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563"}, + {file = "wrapt-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f"}, + {file = "wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58"}, + {file = "wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda"}, + {file = "wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438"}, + {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a"}, + {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000"}, + {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6"}, + {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b"}, + {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662"}, + {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72"}, + {file = "wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317"}, + {file = "wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3"}, + {file = "wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925"}, + {file = "wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392"}, + {file = "wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40"}, + {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d"}, + {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b"}, + {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98"}, + {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82"}, + {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae"}, + {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9"}, + {file = "wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9"}, + {file = "wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991"}, + {file = "wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125"}, + {file = "wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998"}, + {file = "wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5"}, + {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8"}, + {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6"}, + {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc"}, + {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2"}, + {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b"}, + {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504"}, + {file = "wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a"}, + {file = "wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845"}, + {file = "wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192"}, + {file = "wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b"}, + {file = "wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0"}, + {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306"}, + {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb"}, + {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681"}, + {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6"}, + {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6"}, + {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f"}, + {file = "wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555"}, + {file = "wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c"}, + {file = "wrapt-1.17.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5c803c401ea1c1c18de70a06a6f79fcc9c5acfc79133e9869e730ad7f8ad8ef9"}, + {file = "wrapt-1.17.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f917c1180fdb8623c2b75a99192f4025e412597c50b2ac870f156de8fb101119"}, + {file = "wrapt-1.17.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ecc840861360ba9d176d413a5489b9a0aff6d6303d7e733e2c4623cfa26904a6"}, + {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb87745b2e6dc56361bfde481d5a378dc314b252a98d7dd19a651a3fa58f24a9"}, + {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58455b79ec2661c3600e65c0a716955adc2410f7383755d537584b0de41b1d8a"}, + {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4e42a40a5e164cbfdb7b386c966a588b1047558a990981ace551ed7e12ca9c2"}, + {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:91bd7d1773e64019f9288b7a5101f3ae50d3d8e6b1de7edee9c2ccc1d32f0c0a"}, + {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:bb90fb8bda722a1b9d48ac1e6c38f923ea757b3baf8ebd0c82e09c5c1a0e7a04"}, + {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:08e7ce672e35efa54c5024936e559469436f8b8096253404faeb54d2a878416f"}, + {file = "wrapt-1.17.2-cp38-cp38-win32.whl", hash = "sha256:410a92fefd2e0e10d26210e1dfb4a876ddaf8439ef60d6434f21ef8d87efc5b7"}, + {file = "wrapt-1.17.2-cp38-cp38-win_amd64.whl", hash = "sha256:95c658736ec15602da0ed73f312d410117723914a5c91a14ee4cdd72f1d790b3"}, + {file = "wrapt-1.17.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99039fa9e6306880572915728d7f6c24a86ec57b0a83f6b2491e1d8ab0235b9a"}, + {file = "wrapt-1.17.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2696993ee1eebd20b8e4ee4356483c4cb696066ddc24bd70bcbb80fa56ff9061"}, + {file = "wrapt-1.17.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:612dff5db80beef9e649c6d803a8d50c409082f1fedc9dbcdfde2983b2025b82"}, + {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c2caa1585c82b3f7a7ab56afef7b3602021d6da34fbc1cf234ff139fed3cd9"}, + {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c958bcfd59bacc2d0249dcfe575e71da54f9dcf4a8bdf89c4cb9a68a1170d73f"}, + {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc78a84e2dfbc27afe4b2bd7c80c8db9bca75cc5b85df52bfe634596a1da846b"}, + {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ba0f0eb61ef00ea10e00eb53a9129501f52385c44853dbd6c4ad3f403603083f"}, + {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1e1fe0e6ab7775fd842bc39e86f6dcfc4507ab0ffe206093e76d61cde37225c8"}, + {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c86563182421896d73858e08e1db93afdd2b947a70064b813d515d66549e15f9"}, + {file = "wrapt-1.17.2-cp39-cp39-win32.whl", hash = "sha256:f393cda562f79828f38a819f4788641ac7c4085f30f1ce1a68672baa686482bb"}, + {file = "wrapt-1.17.2-cp39-cp39-win_amd64.whl", hash = "sha256:36ccae62f64235cf8ddb682073a60519426fdd4725524ae38874adf72b5f2aeb"}, + {file = "wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8"}, + {file = "wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3"}, +] + +[[package]] +name = "wtforms" +version = "3.2.1" +description = "Form validation and rendering for Python web development." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "wtforms-3.2.1-py3-none-any.whl", hash = "sha256:583bad77ba1dd7286463f21e11aa3043ca4869d03575921d1a1698d0715e0fd4"}, + {file = "wtforms-3.2.1.tar.gz", hash = "sha256:df3e6b70f3192e92623128123ec8dca3067df9cfadd43d59681e210cfb8d4682"}, +] + +[package.dependencies] +markupsafe = "*" + +[package.extras] +email = ["email-validator"] + +[[package]] +name = "xmltodict" +version = "0.14.2" +description = "Makes working with XML feel like you are working with JSON" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "xmltodict-0.14.2-py2.py3-none-any.whl", hash = "sha256:20cc7d723ed729276e808f26fb6b3599f786cbc37e06c65e192ba77c40f20aac"}, + {file = "xmltodict-0.14.2.tar.gz", hash = "sha256:201e7c28bb210e374999d1dde6382923ab0ed1a8a5faeece48ab525b7810a553"}, +] + +[[package]] +name = "zipp" +version = "3.21.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, + {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] + +[[package]] +name = "zope-event" +version = "5.0" +description = "Very basic event publishing system" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "zope.event-5.0-py3-none-any.whl", hash = "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26"}, + {file = "zope.event-5.0.tar.gz", hash = "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd"}, +] + +[package.dependencies] +setuptools = "*" + +[package.extras] +docs = ["Sphinx"] +test = ["zope.testrunner"] + +[[package]] +name = "zope-interface" +version = "7.2" +description = "Interfaces for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "zope.interface-7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce290e62229964715f1011c3dbeab7a4a1e4971fd6f31324c4519464473ef9f2"}, + {file = "zope.interface-7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:05b910a5afe03256b58ab2ba6288960a2892dfeef01336dc4be6f1b9ed02ab0a"}, + {file = "zope.interface-7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:550f1c6588ecc368c9ce13c44a49b8d6b6f3ca7588873c679bd8fd88a1b557b6"}, + {file = "zope.interface-7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ef9e2f865721553c6f22a9ff97da0f0216c074bd02b25cf0d3af60ea4d6931d"}, + {file = "zope.interface-7.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27f926f0dcb058211a3bb3e0e501c69759613b17a553788b2caeb991bed3b61d"}, + {file = "zope.interface-7.2-cp310-cp310-win_amd64.whl", hash = "sha256:144964649eba4c5e4410bb0ee290d338e78f179cdbfd15813de1a664e7649b3b"}, + {file = "zope.interface-7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1909f52a00c8c3dcab6c4fad5d13de2285a4b3c7be063b239b8dc15ddfb73bd2"}, + {file = "zope.interface-7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:80ecf2451596f19fd607bb09953f426588fc1e79e93f5968ecf3367550396b22"}, + {file = "zope.interface-7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:033b3923b63474800b04cba480b70f6e6243a62208071fc148354f3f89cc01b7"}, + {file = "zope.interface-7.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a102424e28c6b47c67923a1f337ede4a4c2bba3965b01cf707978a801fc7442c"}, + {file = "zope.interface-7.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25e6a61dcb184453bb00eafa733169ab6d903e46f5c2ace4ad275386f9ab327a"}, + {file = "zope.interface-7.2-cp311-cp311-win_amd64.whl", hash = "sha256:3f6771d1647b1fc543d37640b45c06b34832a943c80d1db214a37c31161a93f1"}, + {file = "zope.interface-7.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:086ee2f51eaef1e4a52bd7d3111a0404081dadae87f84c0ad4ce2649d4f708b7"}, + {file = "zope.interface-7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:21328fcc9d5b80768bf051faa35ab98fb979080c18e6f84ab3f27ce703bce465"}, + {file = "zope.interface-7.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6dd02ec01f4468da0f234da9d9c8545c5412fef80bc590cc51d8dd084138a89"}, + {file = "zope.interface-7.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e7da17f53e25d1a3bde5da4601e026adc9e8071f9f6f936d0fe3fe84ace6d54"}, + {file = "zope.interface-7.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cab15ff4832580aa440dc9790b8a6128abd0b88b7ee4dd56abacbc52f212209d"}, + {file = "zope.interface-7.2-cp312-cp312-win_amd64.whl", hash = "sha256:29caad142a2355ce7cfea48725aa8bcf0067e2b5cc63fcf5cd9f97ad12d6afb5"}, + {file = "zope.interface-7.2-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:3e0350b51e88658d5ad126c6a57502b19d5f559f6cb0a628e3dc90442b53dd98"}, + {file = "zope.interface-7.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15398c000c094b8855d7d74f4fdc9e73aa02d4d0d5c775acdef98cdb1119768d"}, + {file = "zope.interface-7.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:802176a9f99bd8cc276dcd3b8512808716492f6f557c11196d42e26c01a69a4c"}, + {file = "zope.interface-7.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb23f58a446a7f09db85eda09521a498e109f137b85fb278edb2e34841055398"}, + {file = "zope.interface-7.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a71a5b541078d0ebe373a81a3b7e71432c61d12e660f1d67896ca62d9628045b"}, + {file = "zope.interface-7.2-cp313-cp313-win_amd64.whl", hash = "sha256:4893395d5dd2ba655c38ceb13014fd65667740f09fa5bb01caa1e6284e48c0cd"}, + {file = "zope.interface-7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d3a8ffec2a50d8ec470143ea3d15c0c52d73df882eef92de7537e8ce13475e8a"}, + {file = "zope.interface-7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:31d06db13a30303c08d61d5fb32154be51dfcbdb8438d2374ae27b4e069aac40"}, + {file = "zope.interface-7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e204937f67b28d2dca73ca936d3039a144a081fc47a07598d44854ea2a106239"}, + {file = "zope.interface-7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:224b7b0314f919e751f2bca17d15aad00ddbb1eadf1cb0190fa8175edb7ede62"}, + {file = "zope.interface-7.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baf95683cde5bc7d0e12d8e7588a3eb754d7c4fa714548adcd96bdf90169f021"}, + {file = "zope.interface-7.2-cp38-cp38-win_amd64.whl", hash = "sha256:7dc5016e0133c1a1ec212fc87a4f7e7e562054549a99c73c8896fa3a9e80cbc7"}, + {file = "zope.interface-7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bd449c306ba006c65799ea7912adbbfed071089461a19091a228998b82b1fdb"}, + {file = "zope.interface-7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a19a6cc9c6ce4b1e7e3d319a473cf0ee989cbbe2b39201d7c19e214d2dfb80c7"}, + {file = "zope.interface-7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72cd1790b48c16db85d51fbbd12d20949d7339ad84fd971427cf00d990c1f137"}, + {file = "zope.interface-7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52e446f9955195440e787596dccd1411f543743c359eeb26e9b2c02b077b0519"}, + {file = "zope.interface-7.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ad9913fd858274db8dd867012ebe544ef18d218f6f7d1e3c3e6d98000f14b75"}, + {file = "zope.interface-7.2-cp39-cp39-win_amd64.whl", hash = "sha256:1090c60116b3da3bfdd0c03406e2f14a1ff53e5771aebe33fec1edc0a350175d"}, + {file = "zope.interface-7.2.tar.gz", hash = "sha256:8b49f1a3d1ee4cdaf5b32d2e738362c7f5e40ac8b46dd7d1a65e82a4872728fe"}, +] + +[package.dependencies] +setuptools = "*" + +[package.extras] +docs = ["Sphinx", "furo", "repoze.sphinx.autointerface"] +test = ["coverage[toml]", "zope.event", "zope.testing"] +testing = ["coverage[toml]", "zope.event", "zope.testing"] + +[[package]] +name = "zstandard" +version = "0.23.0" +description = "Zstandard bindings for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "zstandard-0.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bf0a05b6059c0528477fba9054d09179beb63744355cab9f38059548fedd46a9"}, + {file = "zstandard-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fc9ca1c9718cb3b06634c7c8dec57d24e9438b2aa9a0f02b8bb36bf478538880"}, + {file = "zstandard-0.23.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77da4c6bfa20dd5ea25cbf12c76f181a8e8cd7ea231c673828d0386b1740b8dc"}, + {file = "zstandard-0.23.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2170c7e0367dde86a2647ed5b6f57394ea7f53545746104c6b09fc1f4223573"}, + {file = "zstandard-0.23.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c16842b846a8d2a145223f520b7e18b57c8f476924bda92aeee3a88d11cfc391"}, + {file = "zstandard-0.23.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:157e89ceb4054029a289fb504c98c6a9fe8010f1680de0201b3eb5dc20aa6d9e"}, + {file = "zstandard-0.23.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:203d236f4c94cd8379d1ea61db2fce20730b4c38d7f1c34506a31b34edc87bdd"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dc5d1a49d3f8262be192589a4b72f0d03b72dcf46c51ad5852a4fdc67be7b9e4"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:752bf8a74412b9892f4e5b58f2f890a039f57037f52c89a740757ebd807f33ea"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80080816b4f52a9d886e67f1f96912891074903238fe54f2de8b786f86baded2"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:84433dddea68571a6d6bd4fbf8ff398236031149116a7fff6f777ff95cad3df9"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ab19a2d91963ed9e42b4e8d77cd847ae8381576585bad79dbd0a8837a9f6620a"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:59556bf80a7094d0cfb9f5e50bb2db27fefb75d5138bb16fb052b61b0e0eeeb0"}, + {file = "zstandard-0.23.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:27d3ef2252d2e62476389ca8f9b0cf2bbafb082a3b6bfe9d90cbcbb5529ecf7c"}, + {file = "zstandard-0.23.0-cp310-cp310-win32.whl", hash = "sha256:5d41d5e025f1e0bccae4928981e71b2334c60f580bdc8345f824e7c0a4c2a813"}, + {file = "zstandard-0.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:519fbf169dfac1222a76ba8861ef4ac7f0530c35dd79ba5727014613f91613d4"}, + {file = "zstandard-0.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:34895a41273ad33347b2fc70e1bff4240556de3c46c6ea430a7ed91f9042aa4e"}, + {file = "zstandard-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:77ea385f7dd5b5676d7fd943292ffa18fbf5c72ba98f7d09fc1fb9e819b34c23"}, + {file = "zstandard-0.23.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:983b6efd649723474f29ed42e1467f90a35a74793437d0bc64a5bf482bedfa0a"}, + {file = "zstandard-0.23.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80a539906390591dd39ebb8d773771dc4db82ace6372c4d41e2d293f8e32b8db"}, + {file = "zstandard-0.23.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:445e4cb5048b04e90ce96a79b4b63140e3f4ab5f662321975679b5f6360b90e2"}, + {file = "zstandard-0.23.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd30d9c67d13d891f2360b2a120186729c111238ac63b43dbd37a5a40670b8ca"}, + {file = "zstandard-0.23.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d20fd853fbb5807c8e84c136c278827b6167ded66c72ec6f9a14b863d809211c"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed1708dbf4d2e3a1c5c69110ba2b4eb6678262028afd6c6fbcc5a8dac9cda68e"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:be9b5b8659dff1f913039c2feee1aca499cfbc19e98fa12bc85e037c17ec6ca5"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:65308f4b4890aa12d9b6ad9f2844b7ee42c7f7a4fd3390425b242ffc57498f48"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98da17ce9cbf3bfe4617e836d561e433f871129e3a7ac16d6ef4c680f13a839c"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8ed7d27cb56b3e058d3cf684d7200703bcae623e1dcc06ed1e18ecda39fee003"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:b69bb4f51daf461b15e7b3db033160937d3ff88303a7bc808c67bbc1eaf98c78"}, + {file = "zstandard-0.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:034b88913ecc1b097f528e42b539453fa82c3557e414b3de9d5632c80439a473"}, + {file = "zstandard-0.23.0-cp311-cp311-win32.whl", hash = "sha256:f2d4380bf5f62daabd7b751ea2339c1a21d1c9463f1feb7fc2bdcea2c29c3160"}, + {file = "zstandard-0.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:62136da96a973bd2557f06ddd4e8e807f9e13cbb0bfb9cc06cfe6d98ea90dfe0"}, + {file = "zstandard-0.23.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b4567955a6bc1b20e9c31612e615af6b53733491aeaa19a6b3b37f3b65477094"}, + {file = "zstandard-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e172f57cd78c20f13a3415cc8dfe24bf388614324d25539146594c16d78fcc8"}, + {file = "zstandard-0.23.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0e166f698c5a3e914947388c162be2583e0c638a4703fc6a543e23a88dea3c1"}, + {file = "zstandard-0.23.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a289832e520c6bd4dcaad68e944b86da3bad0d339ef7989fb7e88f92e96072"}, + {file = "zstandard-0.23.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d50d31bfedd53a928fed6707b15a8dbeef011bb6366297cc435accc888b27c20"}, + {file = "zstandard-0.23.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72c68dda124a1a138340fb62fa21b9bf4848437d9ca60bd35db36f2d3345f373"}, + {file = "zstandard-0.23.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53dd9d5e3d29f95acd5de6802e909ada8d8d8cfa37a3ac64836f3bc4bc5512db"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6a41c120c3dbc0d81a8e8adc73312d668cd34acd7725f036992b1b72d22c1772"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:40b33d93c6eddf02d2c19f5773196068d875c41ca25730e8288e9b672897c105"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9206649ec587e6b02bd124fb7799b86cddec350f6f6c14bc82a2b70183e708ba"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76e79bc28a65f467e0409098fa2c4376931fd3207fbeb6b956c7c476d53746dd"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:66b689c107857eceabf2cf3d3fc699c3c0fe8ccd18df2219d978c0283e4c508a"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9c236e635582742fee16603042553d276cca506e824fa2e6489db04039521e90"}, + {file = "zstandard-0.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8fffdbd9d1408006baaf02f1068d7dd1f016c6bcb7538682622c556e7b68e35"}, + {file = "zstandard-0.23.0-cp312-cp312-win32.whl", hash = "sha256:dc1d33abb8a0d754ea4763bad944fd965d3d95b5baef6b121c0c9013eaf1907d"}, + {file = "zstandard-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:64585e1dba664dc67c7cdabd56c1e5685233fbb1fc1966cfba2a340ec0dfff7b"}, + {file = "zstandard-0.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:576856e8594e6649aee06ddbfc738fec6a834f7c85bf7cadd1c53d4a58186ef9"}, + {file = "zstandard-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38302b78a850ff82656beaddeb0bb989a0322a8bbb1bf1ab10c17506681d772a"}, + {file = "zstandard-0.23.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2240ddc86b74966c34554c49d00eaafa8200a18d3a5b6ffbf7da63b11d74ee2"}, + {file = "zstandard-0.23.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ef230a8fd217a2015bc91b74f6b3b7d6522ba48be29ad4ea0ca3a3775bf7dd5"}, + {file = "zstandard-0.23.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:774d45b1fac1461f48698a9d4b5fa19a69d47ece02fa469825b442263f04021f"}, + {file = "zstandard-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f77fa49079891a4aab203d0b1744acc85577ed16d767b52fc089d83faf8d8ed"}, + {file = "zstandard-0.23.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac184f87ff521f4840e6ea0b10c0ec90c6b1dcd0bad2f1e4a9a1b4fa177982ea"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c363b53e257246a954ebc7c488304b5592b9c53fbe74d03bc1c64dda153fb847"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e7792606d606c8df5277c32ccb58f29b9b8603bf83b48639b7aedf6df4fe8171"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a0817825b900fcd43ac5d05b8b3079937073d2b1ff9cf89427590718b70dd840"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9da6bc32faac9a293ddfdcb9108d4b20416219461e4ec64dfea8383cac186690"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fd7699e8fd9969f455ef2926221e0233f81a2542921471382e77a9e2f2b57f4b"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d477ed829077cd945b01fc3115edd132c47e6540ddcd96ca169facff28173057"}, + {file = "zstandard-0.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ce8b52c5987b3e34d5674b0ab529a4602b632ebab0a93b07bfb4dfc8f8a33"}, + {file = "zstandard-0.23.0-cp313-cp313-win32.whl", hash = "sha256:a9b07268d0c3ca5c170a385a0ab9fb7fdd9f5fd866be004c4ea39e44edce47dd"}, + {file = "zstandard-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:f3513916e8c645d0610815c257cbfd3242adfd5c4cfa78be514e5a3ebb42a41b"}, + {file = "zstandard-0.23.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2ef3775758346d9ac6214123887d25c7061c92afe1f2b354f9388e9e4d48acfc"}, + {file = "zstandard-0.23.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4051e406288b8cdbb993798b9a45c59a4896b6ecee2f875424ec10276a895740"}, + {file = "zstandard-0.23.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2d1a054f8f0a191004675755448d12be47fa9bebbcffa3cdf01db19f2d30a54"}, + {file = "zstandard-0.23.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f83fa6cae3fff8e98691248c9320356971b59678a17f20656a9e59cd32cee6d8"}, + {file = "zstandard-0.23.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32ba3b5ccde2d581b1e6aa952c836a6291e8435d788f656fe5976445865ae045"}, + {file = "zstandard-0.23.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f146f50723defec2975fb7e388ae3a024eb7151542d1599527ec2aa9cacb152"}, + {file = "zstandard-0.23.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1bfe8de1da6d104f15a60d4a8a768288f66aa953bbe00d027398b93fb9680b26"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:29a2bc7c1b09b0af938b7a8343174b987ae021705acabcbae560166567f5a8db"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:61f89436cbfede4bc4e91b4397eaa3e2108ebe96d05e93d6ccc95ab5714be512"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:53ea7cdc96c6eb56e76bb06894bcfb5dfa93b7adcf59d61c6b92674e24e2dd5e"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:a4ae99c57668ca1e78597d8b06d5af837f377f340f4cce993b551b2d7731778d"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:379b378ae694ba78cef921581ebd420c938936a153ded602c4fea612b7eaa90d"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:50a80baba0285386f97ea36239855f6020ce452456605f262b2d33ac35c7770b"}, + {file = "zstandard-0.23.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:61062387ad820c654b6a6b5f0b94484fa19515e0c5116faf29f41a6bc91ded6e"}, + {file = "zstandard-0.23.0-cp38-cp38-win32.whl", hash = "sha256:b8c0bd73aeac689beacd4e7667d48c299f61b959475cdbb91e7d3d88d27c56b9"}, + {file = "zstandard-0.23.0-cp38-cp38-win_amd64.whl", hash = "sha256:a05e6d6218461eb1b4771d973728f0133b2a4613a6779995df557f70794fd60f"}, + {file = "zstandard-0.23.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3aa014d55c3af933c1315eb4bb06dd0459661cc0b15cd61077afa6489bec63bb"}, + {file = "zstandard-0.23.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7f0804bb3799414af278e9ad51be25edf67f78f916e08afdb983e74161b916"}, + {file = "zstandard-0.23.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb2b1ecfef1e67897d336de3a0e3f52478182d6a47eda86cbd42504c5cbd009a"}, + {file = "zstandard-0.23.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:837bb6764be6919963ef41235fd56a6486b132ea64afe5fafb4cb279ac44f259"}, + {file = "zstandard-0.23.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1516c8c37d3a053b01c1c15b182f3b5f5eef19ced9b930b684a73bad121addf4"}, + {file = "zstandard-0.23.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48ef6a43b1846f6025dde6ed9fee0c24e1149c1c25f7fb0a0585572b2f3adc58"}, + {file = "zstandard-0.23.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11e3bf3c924853a2d5835b24f03eeba7fc9b07d8ca499e247e06ff5676461a15"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2fb4535137de7e244c230e24f9d1ec194f61721c86ebea04e1581d9d06ea1269"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8c24f21fa2af4bb9f2c492a86fe0c34e6d2c63812a839590edaf177b7398f700"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a8c86881813a78a6f4508ef9daf9d4995b8ac2d147dcb1a450448941398091c9"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fe3b385d996ee0822fd46528d9f0443b880d4d05528fd26a9119a54ec3f91c69"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:82d17e94d735c99621bf8ebf9995f870a6b3e6d14543b99e201ae046dfe7de70"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c7c517d74bea1a6afd39aa612fa025e6b8011982a0897768a2f7c8ab4ebb78a2"}, + {file = "zstandard-0.23.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fd7e0f1cfb70eb2f95a19b472ee7ad6d9a0a992ec0ae53286870c104ca939e5"}, + {file = "zstandard-0.23.0-cp39-cp39-win32.whl", hash = "sha256:43da0f0092281bf501f9c5f6f3b4c975a8a0ea82de49ba3f7100e64d422a1274"}, + {file = "zstandard-0.23.0-cp39-cp39-win_amd64.whl", hash = "sha256:f8346bfa098532bc1fb6c7ef06783e969d87a99dd1d2a5a18a892c1d7a643c58"}, + {file = "zstandard-0.23.0.tar.gz", hash = "sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09"}, +] + +[package.dependencies] +cffi = {version = ">=1.11", optional = true, markers = "platform_python_implementation == \"PyPy\" or extra == \"cffi\""} + +[package.extras] +cffi = ["cffi (>=1.11)"] + +[metadata] +lock-version = "2.1" +python-versions = "^3.12.6" +content-hash = "a286592c9f1bfc434bb5b8557bb5aed60a411f9288d2535ec6cdc1561aae4994" diff --git a/profile_application.py b/profile_application.py index e19d344540..82e847a30d 100644 --- a/profile_application.py +++ b/profile_application.py @@ -8,7 +8,6 @@ def setup_profiling(application): - profiling_dir = "profiling" if os.path.exists(profiling_dir): diff --git a/pyproject.toml b/pyproject.toml index 4d9e58430b..6d62b3ac9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,195 @@ -[tool.black] -target_version = ['py37'] \ No newline at end of file +[tool.djlint] +blank_line_after_tag="load,extends,import,from" +blank_line_before_tag="load,extends,block" +max_line_length=120 +# _base.html excluded due to the bad nesting after format +exclude=".templates/layouts/_base.html" +# when using double quote escape sequence in strings we get "H025 Tag seems to be an orphan" false positives +ignore="H025" + +[tool.poetry] +name = "eq-questionnaire-runner" +version = "1.0.0" +description = "ONS Digital eQ Questionnaire Runner App" +authors = ["ONSDigital"] + +[tool.poetry.group.dev.dependencies] +# update dependabot.yaml when adding new dependencies +pep8 = "^1.7.1" +mock = "^5.1.0" +pytest-cov = "^6.0.0" +jsonschema = "^4.21.1" +pylint = "^3.2.7" +pylint-mccabe = "^0.1.3" +pylint-absolute-imports = "^1.1.0" +beautifulsoup4 = "^4.12.3" +httmock = "^1.4.0" +moto = "^5.0.13" +freezegun = "^1.4.0" +pytest-xdist = "^3.5.0" +fakeredis = "^2.23.5" +mypy = "^1.11.2" +pytest-flask = "^1.3.0" +pytest = "^8.3.2" +pytest-sugar = "^1.0.0" +responses = "^0.25.0" +types-simplejson = "^3.19.0.20240310" +types-requests = "^2.31.0.20240406" +types-redis = "^4.6.0.20240819" +types-PyYAML = "^6.0.12.20240808" +types-python-dateutil = "^2.9.0.20240821" +pytest-mock = "^3.12.0" +types-cachetools = "^6.0.0.20250525" +types-pytz = "^2025.2.0.20250516" +playwright = "^1.42.0" +black = "^25.1.0" +djlint = "^1.34.2" +ruff = "^0.11.13" + + +[tool.poetry.dependencies] +# update dependabot.yaml when adding new dependencies +python = "^3.12.6" +colorama = "^0.4.6" +flask = "^3.0.2" +flask-babel = "^4.0.0" +flask-login = "^0.6.3" +flask-wtf = "^1.2.1" +google-cloud-datastore = "^2.19.0" +grpcio = "^1.64.1" +gunicorn = "^23.0.0" +pika = "^1.3.2" +pyyaml = "^6.0.1" +requests = "^2.32.0" +sdc-cryptography = "^1.2.1" +structlog = "^25.2.0" +ua-parser = "^1.0.0" +blinker = "^1.7.0" +boto3 = "^1.34.151" +humanize = "^4.9.0" +flask-talisman = "^1.1.0" +marshmallow = "^3.21.3" +python-snappy = "^0.7.1" +google-cloud-storage = "^3.1.0" +jsonpointer = "^3.0" +redis = "^5.0.8" +flask-compress = "^1.14" +htmlmin = "^0.1.12" +coloredlogs = "^15.0.1" +uwsgi = "^2.0.24" +email-validator = "^2.1.2" +itsdangerous = "^2.1.2" +google-cloud-pubsub = "^2.23.0" +google-cloud-tasks = "^2.16.3" +simplejson = "^3.19.2" +markupsafe = "^3.0.1" +pdfkit = "^1.0.0" +ordered-set = "^4.1.0" +cachetools = "^5.3.0.7" +gevent = "^24.2.1" +babel = "==2.14.0" # Temporarily pinned - problem for translations found in v2.15.0, see: https://github.com/ONSdigital/eq-questionnaire-runner/pull/1384 +wtforms = "^3.2.1" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.ruff] +target-version = "py312" +include = ["*.py"] +line-length = 160 +indent-width = 4 +cache-dir = "~/.cache/ruff" +exclude = ["tests/*", "scripts/*"] + + +[tool.ruff.lint] +extend-ignore = [ + "A005", # Reusing a builtin module name for the name of a module + "B024", # No abstract methods in abstract base class + "B027", # Non-abstract empty methods in abstract base classes + "B028",# No explicit keyword argument found + "B905", # zip() without an explicit strict= parameter + "RUF100", # Allow Unused blanket `noqa` directive + "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` + "RUF005", # Consider {expression} instead of concatenation + "RUF009", # Do not perform function call `lazy_gettext` in dataclass defaults + "RUF015", # Prefer `next(...)` over single element slice + "RUF010", # Use explicit conversion flag + "RUF001", # String contains ambiguous character + "ARG001", # Allow Unused function argument: `method_name` + "ARG002", # Unused method argument + "ARG004", # Unused static method argument: `kwargs + "N818", # Exception name should be named with an Error suffix + "UP032", # Use f-string instead of `format` call + "UP018", # Unnecessary {literal_type} call (rewrite as a literal) + "UP015", # Unnecessary open mode parameters + "UP009", # UTF-8 encoding declaration is unnecessary + "UP017", # Use `datetime.UTC` alias + "UP033", # Use @functools.cache instead of @functools.lru_cache(maxsize=None) + "UP037", # Remove quotes from type annotation + "UP038", # Use X | Y in {} call instead of (X, Y) + "UP035", # Import from {target} instead: {names} + "UP040", # Type alias {name} uses {type_alias_method} instead of the type keyword + "S105", # Possible hardcoded password assigned to: "{}" + "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes + "C408", # Unnecessary `tuple` call (rewrite as a literal) + "C400", # Unnecessary generator (rewrite as a `list` comprehension) + "N802", # Function name {name} should be lowercase + "I001", # Import block is un-sorted or un-formatted + "FBT001", # Boolean-typed keyword argument in function definition + "FBT002", # Boolean-typed positional argument in function definition + "FBT003", # Boolean positional value in function call + "COM812", # Trailing comma missing + "G004", # Logging statement uses f-string + "PIE810", # Call {attr} once with a tuple + "PIE800", # Unnecessary spread ** + "SLF001", # Private member accessed: {access} + "TRY003", # Avoid specifying long messages outside the exception class + "TRY400", # Use logging.exception instead of logging.error + "TRY201", # Avoid using `raise Exception` without specifying an exception class + "TRY300", # Consider moving this statement to an `else` block + "RET501", # Do not explicitly `return None` in function if it is the only possible return value + "RET503", # Missing explicit `return` at the end of function able to return non-`None` value + "RET504", # Unnecessary assignment to `context` before `return` statement +] +extend-select = [ + "E4", "E7", "E9", "E5", # On top of the defaults (`E4`, E7`, `E9`, and `F`), enable E5 (Adds line length check - error "E501") + "Q", # flake8-quotes (Q) + "B", # flake8-bugbear (B) + "A", # flake8-builtins (A) + "C4", # flake8-comprehensions (C4) + "PIE", # flake8-pie (PIE) + "SLF", # flake8-self (SLF) + "ARG", # flake8-unused-arguments (ARG) + "YTT", # flake8-2020 (YTT) + "C", # flake8-comprehensions (C) + "DTZ", # flake8-datetimez (DTZ) + "S", # flake8-bandit (S) + "TID", # flake8-tidy-imports (TID) + "ICN", # flake8-import-conventions (ICN) + "ISC", # flake8-implicit-str-concat (ISC) + "COM", # flake8-commas (COM) + "LOG", # flake8-logging (LOG) + "G", # flake8-logging-format (G) + "EM", # flake8-errmsg (EM) + "FBT", # flake8-boolean-trap (FBT) + "TD", # flake8-todo (TD) + "FA", # flake8-future-annotations (FA) + "T20", # flake8-print (T20) + "RET", # flake8-return (RET) + "E", # pycodestyle Error (E) + "W", # pycodestyle Warning (W) + "F", # pyflakes (F) + "I", # isort (I) + "N", # pep8-naming (N) + "RUF", # Ruff-specific rules (RUF) + "UP", # pyupgrade (UP) + "ERA", # eradicate (ERA) + "FURB", # refurb (FURB) + "TRY", # tryceratops (TRY) + "FLY", # flynt (FLY) + "PERF", # Perflint (PERF) +] +[tool.ruff.lint.isort] +case-sensitive = true diff --git a/schemas/test/en/test_answer_codes.json b/schemas/test/en/test_answer_codes.json new file mode 100644 index 0000000000..6ec68041a9 --- /dev/null +++ b/schemas/test/en/test_answer_codes.json @@ -0,0 +1,164 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Test Answer Codes", + "theme": "default", + "description": "A questionnaire to demo answer codes.", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Linear", + "options": { + "summary": { + "collapsible": false + } + } + }, + "answer_codes": [ + { + "answer_id": "mandatory-checkbox-answer", + "answer_value": "None", + "code": "1a" + }, + { + "answer_id": "mandatory-checkbox-answer", + "answer_value": "Ham & Cheese", + "code": "1b" + }, + { + "answer_id": "mandatory-checkbox-answer", + "answer_value": "Ham", + "code": "1c" + }, + { + "answer_id": "mandatory-checkbox-answer", + "answer_value": "Pepperoni", + "code": "1d" + }, + { + "answer_id": "mandatory-checkbox-answer", + "answer_value": "Other", + "code": "1e" + }, + { + "answer_id": "other-answer-mandatory", + "code": "1f" + }, + { + "answer_id": "name-answer", + "code": "2" + }, + { + "answer_id": "name-answer-2", + "code": "3" + } + ], + "sections": [ + { + "id": "default-section", + "groups": [ + { + "blocks": [ + { + "type": "Question", + "id": "mandatory-checkbox", + "question": { + "answers": [ + { + "id": "mandatory-checkbox-answer", + "mandatory": false, + "options": [ + { + "label": "None", + "value": "None" + }, + { + "label": "Ham & Cheese", + "value": "Ham & Cheese" + }, + { + "label": "Ham", + "value": "Ham" + }, + { + "label": "Pepperoni", + "value": "Pepperoni" + }, + { + "label": "Other", + "description": "Choose any other topping", + "value": "Other", + "detail_answer": { + "mandatory": true, + "id": "other-answer-mandatory", + "label": "Please specify other", + "type": "TextField" + } + } + ], + "type": "Checkbox" + } + ], + "id": "mandatory-checkbox-question", + "title": "Which pizza toppings would you like?", + "type": "General" + } + }, + { + "type": "Question", + "id": "name-block", + "question": { + "answers": [ + { + "id": "name-answer", + "label": "What is your name?", + "max_length": 20, + "mandatory": false, + "type": "TextField" + } + ], + "id": "name-question", + "title": "Title", + "type": "General" + } + }, + { + "type": "Question", + "id": "name-block-2", + "question": { + "answers": [ + { + "id": "name-answer-2", + "label": "What is your surname?", + "max_length": 20, + "mandatory": false, + "type": "TextField" + } + ], + "id": "name-question-2", + "title": "Title", + "type": "General" + } + } + ], + "id": "checkboxes" + } + ] + } + ] +} diff --git a/schemas/test/en/test_benchmark_business.json b/schemas/test/en/test_benchmark_business.json index 65a82081f4..d410688f17 100644 --- a/schemas/test/en/test_benchmark_business.json +++ b/schemas/test/en/test_benchmark_business.json @@ -2,17 +2,21 @@ "language": "en", "mime_type": "application/json/ons/eq", "schema_version": "0.0.1", - "data_version": "0.0.1", - "survey_id": "017", - "form_type": "0070", + "data_version": "0.0.3", + "survey_id": "0", + "theme": "default", "legal_basis": "Notice is given under section 1 of the Statistics of Trade Act 1947.", - "title": "Quarterly Stocks Survey", + "title": "Benchmark-Survey", "questionnaire_flow": { - "type": "Linear", + "type": "Hub", "options": { - "summary": { - "collapsible": false - } + "required_completed_sections": [ + "stock-section", + "household-section", + "questions-section", + "section-companies", + "grand-calculated-summary-section-1" + ] } }, "post_submission": { @@ -21,11 +25,12 @@ }, "sections": [ { - "id": "sectionquestionnaire-introduction", - "title": "Introduction", + "id": "stock-section", + "title": "Quarterly Stocks Survey", + "show_on_hub": true, "groups": [ { - "id": "groupquestionnaire-introduction", + "id": "group-questionnaire-introduction", "title": "Introduction", "blocks": [ { @@ -108,9 +113,7 @@ "id": "secondary-content", "contents": [ { - "title": "How we use your data" - }, - { + "title": "How we use your data", "list": [ "The information supplied is used to estimate changes in stock levels which are used in the compilation of Gross Domestic Product (GDP), the total UK economic activity.", "GDP is used to measure the UK's financial health and prosperity over time and in comparison to other countries.", @@ -122,21 +125,15 @@ ] } ] - } - ] - }, - { - "id": "section91", - "title": "Quarterly Stocks Survey", - "groups": [ + }, { - "id": "group91", + "id": "group-91", "blocks": [ { - "id": "block379", + "id": "block-379", "type": "Question", "question": { - "id": "question379", + "id": "question-379", "title": { "text": "Are you able to report for the period {ref_p_start_date} to {ref_p_end_date}?", "placeholders": [ @@ -175,7 +172,7 @@ "type": "General", "answers": [ { - "id": "answer434", + "id": "answer-434", "mandatory": true, "type": "Radio", "options": [ @@ -193,38 +190,35 @@ }, "routing_rules": [ { - "goto": { - "block": "block381", - "when": [ + "block": "block-381", + "when": { + "==": [ { - "id": "answer434", - "condition": "equals any", - "values": ["Yes"] - } + "identifier": "answer-434", + "source": "answers" + }, + "Yes" ] } }, { - "goto": { - "block": "block380" - } + "block": "block-380" } ] }, { - "id": "block380", + "id": "block-380", "type": "Question", "question": { - "id": "question380", + "id": "question-380", "title": "For which period are you able to report?", "type": "DateRange", "answers": [ { - "id": "answerfrom", + "id": "answer-from", "type": "Date", "mandatory": true, "label": "Period from", - "q_code": "11", "minimum": { "value": { "source": "metadata", @@ -236,11 +230,10 @@ } }, { - "id": "answerto", + "id": "answer-to", "type": "Date", "mandatory": true, "label": "Period to", - "q_code": "12", "maximum": { "value": { "source": "metadata", @@ -263,11 +256,11 @@ } }, { - "id": "block381", + "id": "block-381", "type": "Question", "question": { - "id": "question381", - "title": "What was the total value of stocks held (net of progress payments on long-term contracts)?", + "id": "question-381", + "title": "What was the total value of stocks held (net of progress payments on long-term contracts)?", "guidance": { "contents": [ { @@ -289,35 +282,31 @@ } ] }, - "definitions": [ - { - "title": "What is work in progress?", - "contents": [ - { - "description": "This refers to goods and services that have been partially completed (e.g. a solicitor working on a legal case over a period of time and being paid at the end of the contract for the services provided i.e. unbilled work)." - } - ] - } - ], + "definition": { + "title": "What is work in progress?", + "contents": [ + { + "description": "This refers to goods and services that have been partially completed (e.g. a solicitor working on a legal case over a period of time and being paid at the end of the contract for the services provided i.e. unbilled work)." + } + ] + }, "type": "General", "answers": [ { - "id": "answer436", + "id": "answer-436", "mandatory": true, "type": "Currency", "label": "Total value of stocks held at start of period", "description": "Enter the full value (e.g. 56,234.33) or a value to the nearest ÂŖthousand (e.g. 56,000). Do not enter ‘56’ for ÂŖ56,000.", - "q_code": "598", "decimal_places": 2, "currency": "GBP" }, { - "id": "answer437", + "id": "answer-437", "mandatory": true, "type": "Currency", "label": "Total value of stocks held at end of period", "description": "Enter the full value (e.g. 56,234.33) or a value to the nearest ÂŖthousand (e.g. 56,000). Do not enter ‘56’ for ÂŖ56,000.", - "q_code": "599", "decimal_places": 2, "currency": "GBP" } @@ -325,18 +314,17 @@ } }, { - "id": "block4616", + "id": "block-4616", "type": "Question", "question": { - "id": "question4616", + "id": "question-4616", "title": "Are the end of period figures you have provided estimated?", "type": "General", "answers": [ { - "id": "answer5873", + "id": "answer-5873", "mandatory": true, "type": "Radio", - "q_code": "15", "options": [ { "label": "Yes", @@ -352,10 +340,10 @@ } }, { - "id": "block4952", + "id": "block-4952", "type": "Question", "question": { - "id": "question4952", + "id": "question-4952", "title": { "text": "Did any significant changes occur to the total value of stocks for {trad_as}?", "placeholders": [ @@ -411,10 +399,9 @@ "type": "General", "answers": [ { - "id": "answer6287", + "id": "answer-6287", "mandatory": true, "type": "Radio", - "q_code": "146a", "options": [ { "label": "Yes", @@ -430,29 +417,27 @@ }, "routing_rules": [ { - "goto": { - "block": "block4953", - "when": [ + "block": "block-4953", + "when": { + "==": [ { - "id": "answer6287", - "condition": "equals any", - "values": ["Yes"] - } + "identifier": "answer-6287", + "source": "answers" + }, + "Yes" ] } }, { - "goto": { - "block": "block383" - } + "block": "block-383" } ] }, { - "id": "block4953", + "id": "block-4953", "type": "Question", "question": { - "id": "question4953", + "id": "question-4953", "title": { "text": "Please indicate the reasons for any changes in the total value of stocks for {trad_as}", "placeholders": [ @@ -481,44 +466,37 @@ "type": "General", "answers": [ { - "id": "answer6288", + "id": "answer-6288", "mandatory": true, "type": "Checkbox", "options": [ { "label": "Change of business structure, merger or takeover", - "value": "Change of business structure, merger or takeover", - "q_code": "146e" + "value": "Change of business structure, merger or takeover" }, { "label": "End of accounting period or financial year", - "value": "End of accounting period or financial year", - "q_code": "146c" + "value": "End of accounting period or financial year" }, { "label": "Introduction or removal of new legislation or incentive", - "value": "Introduction or removal of new legislation or incentive", - "q_code": "146g" + "value": "Introduction or removal of new legislation or incentive" }, { "label": "Normal movement for the time of year", - "value": "Normal movement for the time of year", - "q_code": "146d" + "value": "Normal movement for the time of year" }, { "label": "One-off increase in stocks", - "value": "One-off increase in stocks", - "q_code": "146f" + "value": "One-off increase in stocks" }, { "label": "Start or end of long term project", - "value": "Start or end of long term project", - "q_code": "146b" + "value": "Start or end of long term project" }, { "label": "Other (for example, end of the EU transition period, leaving the EU or other global economic conditions.", - "value": "Other (for example, end of the EU transition period, leaving the EU or other global economic conditions.", - "q_code": "146h" + "value": "Other (for example, end of the EU transition period, leaving the EU or other global economic conditions." } ] } @@ -526,20 +504,19 @@ } }, { - "id": "block383", + "id": "block-383", "type": "Question", "question": { - "id": "question383", + "id": "question-383", "title": "Explain any differences between this quarter's opening value and the previously returned closing value", - "description": ["

Include any unusual fluctuations in figures

"], + "description": ["

Include any unusual fluctuations in figures

"], "type": "General", "answers": [ { - "id": "answer439", + "id": "answer-439", "mandatory": false, "type": "TextArea", "label": "Comments", - "q_code": "146", "max_length": 2000 } ] @@ -548,9 +525,1571 @@ ] } ] - } - ], - "theme": "default", + }, + { + "id": "household-section", + "title": "Household", + "show_on_hub": true, + "groups": [ + { + "id": "group", + "title": "List", + "blocks": [ + { + "id": "primary-person-list-collector", + "type": "PrimaryPersonListCollector", + "for_list": "people", + "add_or_edit_block": { + "id": "add-or-edit-primary-person", + "type": "PrimaryPersonListAddOrEditQuestion", + "question": { + "id": "primary-person-add-or-edit-question", + "type": "General", + "title": "What is your name?", + "answers": [ + { + "id": "first-name", + "label": "First name", + "mandatory": true, + "type": "TextField" + }, + { + "id": "last-name", + "label": "Last name", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "question": { + "id": "primary-confirmation-question", + "type": "General", + "title": "Do you live here?", + "answers": [ + { + "id": "you-live-here", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + { + "id": "list-collector", + "type": "ListCollector", + "for_list": "people", + "question": { + "id": "confirmation-question", + "type": "General", + "title": "Does anyone else live here?", + "answers": [ + { + "id": "anyone-else", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "add_block": { + "id": "add-person", + "type": "ListAddQuestion", + "question": { + "id": "add-question", + "type": "General", + "title": "What is the name of the person?", + "answers": [ + { + "id": "first-name", + "label": "First name", + "mandatory": true, + "type": "TextField" + }, + { + "id": "last-name", + "label": "Last name", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "edit_block": { + "id": "edit-person", + "type": "ListEditQuestion", + "question": { + "id": "edit-question", + "type": "General", + "title": "What is the name of the person?", + "answers": [ + { + "id": "first-name", + "label": "First name", + "mandatory": true, + "type": "TextField" + }, + { + "id": "last-name", + "label": "Last name", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "remove_block": { + "id": "remove-person", + "type": "ListRemoveQuestion", + "question": { + "id": "remove-question", + "type": "General", + "title": "Are you sure you want to remove this person?", + "answers": [ + { + "id": "remove-confirmation", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RemoveListItemAndAnswers" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + "summary": { + "title": "Household members", + "item_title": { + "text": "{person_name}", + "placeholders": [ + { + "placeholder": "person_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "first-name" + }, + { + "source": "answers", + "identifier": "last-name" + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + } + } + } + ] + } + ] + }, + { + "id": "questions-section", + "title": "Questions", + "summary": { + "show_on_completion": true + }, + "show_on_hub": true, + "groups": [ + { + "id": "radio", + "title": "Questions", + "blocks": [ + { + "type": "Question", + "id": "skip-first-block", + "question": { + "type": "General", + "id": "skip-first-block-question", + "title": "Skip First Block so it doesn’t appear in Total?", + "answers": [ + { + "type": "Radio", + "id": "skip-first-block-answer", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + { + "skip_conditions": { + "when": { + "==": [ + { + "identifier": "skip-first-block-answer", + "source": "answers" + }, + "Yes" + ] + } + }, + "type": "Question", + "id": "first-number-block", + "question": { + "id": "first-number-question", + "title": "First Number Question Title", + "type": "General", + "answers": [ + { + "id": "first-number-answer", + "label": "First answer label (optional)", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "skip_conditions": { + "when": { + "==": [ + { + "identifier": "skip-first-block-answer", + "source": "answers" + }, + "Yes" + ] + } + }, + "type": "Question", + "id": "first-and-a-half-number-block", + "question": { + "id": "first-and-a-half-number-question-also-in-total", + "title": "First Number Additional Question Title", + "type": "General", + "answers": [ + { + "id": "first-and-a-half-number-answer-also-in-total", + "label": "First answer label also in total (optional)", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "Question", + "id": "second-number-block", + "question": { + "id": "second-number-question-also-in-total", + "title": "Second Number Additional Question Title", + "type": "General", + "answers": [ + { + "id": "second-number-answer-also-in-total", + "label": "Second answer label also in total (optional)", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "currency-total-playback-1", + "title": "We calculate the total of currency values entered to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "answers", + "identifier": "first-number-answer" + }, + { + "source": "answers", + "identifier": "first-and-a-half-number-answer-also-in-total" + }, + { + "source": "answers", + "identifier": "second-number-answer-also-in-total" + } + ] + }, + "title": "Grand total of previous values" + } + } + ] + } + ] + }, + { + "id": "calculated-summary-section", + "title": "Calculated Summary", + "summary": { + "show_on_completion": true + }, + "repeat": { + "for_list": "people", + "title": { + "text": "{person_name}", + "placeholders": [ + { + "placeholder": "person_name", + "transforms": [ + { + "transform": "concatenate_list", + "arguments": { + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "first-name" + }, + { + "source": "answers", + "identifier": "last-name" + } + ], + "delimiter": " " + } + } + ] + } + ] + } + }, + "groups": [ + { + "blocks": [ + { + "type": "Question", + "id": "grand-calculated-summary-third-number-block", + "question": { + "id": "grand-calculated-summary-third-number-question", + "title": "Third Number Question Title", + "type": "General", + "answers": [ + { + "id": "third-number-answer", + "label": "Third answer in currency label", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + }, + { + "id": "third-number-answer-also-in-total", + "label": "Third answer label also in currency total (optional)", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "currency-total-playback-2", + "title": "We calculate the total of currency values entered to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "answers", + "identifier": "third-number-answer" + }, + { + "source": "answers", + "identifier": "third-number-answer-also-in-total" + } + ] + }, + "title": "Grand total of previous values" + } + }, + { + "type": "Question", + "id": "mutually-exclusive-checkbox", + "question": { + "id": "mutually-exclusive-checkbox-question", + "type": "MutuallyExclusive", + "title": "Which answer did you give to question 4 and a half?", + "mandatory": false, + "answers": [ + { + "id": "checkbox-answer", + "instruction": "Select an answer", + "type": "Checkbox", + "mandatory": false, + "options": [ + { + "label": { + "placeholders": [ + { + "placeholder": "answer_value_1", + "value": { + "identifier": "first-and-a-half-number-answer-also-in-total", + "source": "answers" + } + } + ], + "text": "{answer_value_1} - first and a half answer" + }, + "value": "{answer_value_1}" + }, + { + "label": { + "placeholders": [ + { + "placeholder": "calc_value_1", + "value": { + "identifier": "currency-total-playback-1", + "source": "calculated_summary" + } + } + ], + "text": "{calc_value_1} - calculated summary answer (previous section)" + }, + "value": "{calc_value_1}" + }, + { + "label": { + "placeholders": [ + { + "placeholder": "calc_value_2", + "value": { + "identifier": "currency-total-playback-2", + "source": "calculated_summary" + } + } + ], + "text": "{calc_value_2} - calculated summary answer (current section)" + }, + "value": "{calc_value_2}" + }, + { + "label": { + "placeholders": [ + { + "placeholder": "third_answer_value", + "value": { + "identifier": "third-number-answer", + "source": "answers" + } + } + ], + "text": "{third_answer_value} - third answer" + }, + "value": "{third_answer_value}" + } + ] + }, + { + "id": "checkbox-exclusive-answer", + "mandatory": false, + "type": "Checkbox", + "options": [ + { + "label": "I prefer not to say", + "description": "Some description", + "value": "I prefer not to say" + } + ] + } + ] + } + }, + { + "type": "Question", + "id": "set-min-max-block", + "question": { + "answers": [ + { + "id": "set-minimum-answer", + "label": "Set a value greater than the total above", + "mandatory": true, + "description": "This is a description of the minimum value", + "type": "Currency", + "currency": "GBP", + "decimal_places": 2, + "minimum": { + "value": { + "source": "calculated_summary", + "identifier": "currency-total-playback-1" + } + } + }, + { + "id": "set-maximum-answer", + "description": "This is a description of the maximum value", + "label": "Set a value less than the total above", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2, + "maximum": { + "value": { + "source": "calculated_summary", + "identifier": "currency-total-playback-1" + } + } + } + ], + "id": "set-min-question", + "title": { + "placeholders": [ + { + "placeholder": "calculated_summary_answer", + "value": { + "identifier": "currency-total-playback-1", + "source": "calculated_summary" + } + } + ], + "text": "Set minimum and maximum values based on your calculated summary total of ÂŖ{calculated_summary_answer}" + }, + "type": "General" + } + } + ], + "id": "calculated-summary" + } + ] + }, + { + "id": "grand-calculated-summary-section-1", + "title": "Commuting", + "show_on_hub": true, + "summary": { + "show_on_completion": true + }, + "groups": [ + { + "id": "grand-calculated-summary-group", + "title": "Commuting", + "blocks": [ + { + "type": "Question", + "id": "grand-calculated-summary-first-number-block", + "question": { + "id": "grand-calculated-summary-first-number-question", + "title": "How much do you walk per week?", + "type": "General", + "answers": [ + { + "id": "q1-a1", + "label": "Weekly distance travelled on foot", + "mandatory": true, + "type": "Unit", + "unit_length": "short", + "unit": "length-mile", + "decimal_places": 2 + }, + { + "id": "q1-a2", + "label": "Number of walks per week", + "mandatory": true, + "type": "Number" + } + ] + } + }, + { + "type": "Question", + "id": "grand-calculated-summary-second-number-block", + "question": { + "id": "grand-calculated-summary-second-number-question", + "title": "How much do you drive per week?", + "type": "General", + "answers": [ + { + "id": "q2-a1", + "label": "Weekly distance travelled by car", + "mandatory": true, + "type": "Unit", + "unit_length": "short", + "unit": "length-mile", + "decimal_places": 2 + }, + { + "id": "q2-a2", + "label": "Number of car journeys per week", + "mandatory": true, + "type": "Number" + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "distance-calculated-summary-1", + "title": "We calculate the total of distance travelled by foot and car to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "answers", + "identifier": "q1-a1" + }, + { + "source": "answers", + "identifier": "q2-a1" + } + ] + }, + "title": "Calculated distance on foot and driving" + } + }, + { + "type": "CalculatedSummary", + "id": "number-calculated-summary-1", + "title": "We calculate the total number of journeys on foot and in a car to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "answers", + "identifier": "q1-a2" + }, + { + "source": "answers", + "identifier": "q2-a2" + } + ] + }, + "title": "Calculated journeys on foot and driving" + } + } + ] + } + ] + }, + { + "id": "grand-calculated-summary-section-2", + "title": "Alternative Transport", + "groups": [ + { + "id": "transport-group", + "title": "Alternative Transport", + "blocks": [ + { + "type": "Question", + "id": "third-number-block", + "question": { + "id": "third-number-question", + "title": "How much do you cycle per week?", + "type": "General", + "answers": [ + { + "id": "q3-a1", + "label": "Weekly distance travelled by bike", + "mandatory": true, + "type": "Unit", + "unit_length": "short", + "unit": "length-mile", + "decimal_places": 2 + }, + { + "id": "q3-a2", + "label": "Number of bicycle journeys per week", + "mandatory": true, + "type": "Number" + } + ] + } + }, + { + "type": "Question", + "id": "fourth-number-block", + "question": { + "id": "fourth-number-question", + "title": "How much do you voi per week?", + "type": "General", + "answers": [ + { + "id": "q4-a1", + "label": "Weekly distance travelled on a Voi", + "mandatory": true, + "type": "Unit", + "unit_length": "short", + "unit": "length-mile", + "decimal_places": 2 + }, + { + "id": "q4-a2", + "label": "Number of scooter trips per week", + "mandatory": true, + "type": "Number" + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "distance-calculated-summary-2", + "title": "We calculate the total of distance travelled by bike and voi to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "answers", + "identifier": "q3-a1" + }, + { + "source": "answers", + "identifier": "q4-a1" + } + ] + }, + "title": "Calculated weekly distance on bike and scooter" + } + }, + { + "type": "CalculatedSummary", + "id": "number-calculated-summary-2", + "title": "We calculate the total number of journeys on bike and on a voi to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "answers", + "identifier": "q3-a2" + }, + { + "source": "answers", + "identifier": "q4-a2" + } + ] + }, + "title": "Calculated journeys on bike and scooter" + } + } + ] + } + ] + }, + { + "id": "grand-calculated-summary-section-3", + "title": "Grand calculated summaries", + "groups": [ + { + "id": "summary-group", + "title": "Grand calculated summary group", + "blocks": [ + { + "type": "GrandCalculatedSummary", + "id": "distance-grand-calculated-summary", + "title": "We calculate the grand total weekly distance travelled to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "calculated_summary", + "identifier": "distance-calculated-summary-1" + }, + { + "source": "calculated_summary", + "identifier": "distance-calculated-summary-2" + } + ] + }, + "title": "Grand calculated summary of distance travelled" + } + }, + { + "type": "GrandCalculatedSummary", + "id": "number-grand-calculated-summary", + "title": "We calculate the grand total journeys per week to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "calculated_summary", + "identifier": "number-calculated-summary-1" + }, + { + "source": "calculated_summary", + "identifier": "number-calculated-summary-2" + } + ] + }, + "title": "Grand calculated summary of journeys" + } + } + ] + } + ] + }, + { + "id": "section-companies", + "title": "General insurance companies", + "summary": { + "show_on_completion": true, + "items": [ + { + "type": "List", + "for_list": "companies", + "title": "Companies or UK branches", + "item_anchor_answer_id": "company-or-branch-name", + "item_label": "Name of UK company or branch", + "add_link_text": "Add another UK company or branch", + "empty_list_text": "No UK company or branch added" + } + ], + "show_non_item_answers": true + }, + "groups": [ + { + "id": "group-companies", + "blocks": [ + { + "type": "Question", + "id": "responsible-party", + "question": { + "type": "General", + "id": "responsible-party-question", + "title": "Are you the responsible party for reporting trading details for a company of branch?", + "answers": [ + { + "type": "Radio", + "id": "responsible-party-answer", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "routing_rules": [ + { + "block": "any-companies-or-branches", + "when": { + "==": [ + "Yes", + { + "source": "answers", + "identifier": "responsible-party-answer" + } + ] + } + }, + { + "section": "End" + } + ] + }, + { + "type": "ListCollectorDrivingQuestion", + "id": "any-companies-or-branches", + "for_list": "companies", + "question": { + "type": "General", + "id": "any-companies-or-branches-question", + "title": "Do any companies or branches within your United Kingdom group undertake general insurance business?", + "answers": [ + { + "type": "Radio", + "id": "any-companies-or-branches-answer", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock", + "params": { + "block_id": "add-company", + "list_name": "companies" + } + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "routing_rules": [ + { + "when": { + "==": [ + { + "source": "answers", + "identifier": "any-companies-or-branches-answer" + }, + "Yes" + ] + }, + "block": "any-other-companies-or-branches" + }, + { + "section": "End" + } + ] + }, + { + "id": "any-other-companies-or-branches", + "type": "ListCollector", + "for_list": "companies", + "question": { + "id": "any-other-companies-or-branches-question", + "type": "General", + "title": "Do you need to add any other UK companies or branches that undertake general insurance business?", + "answers": [ + { + "id": "any-other-companies-or-branches-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "add_block": { + "id": "add-company", + "type": "ListAddQuestion", + "question": { + "id": "add-question-companies", + "type": "General", + "title": "What is the name and registration number of the company?", + "answers": [ + { + "id": "company-or-branch-name", + "label": "Name of UK company or branch (Mandatory)", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "repeating_blocks": [ + { + "id": "companies-repeating-block-1", + "type": "ListRepeatingQuestion", + "question": { + "id": "companies-repeating-block-1-question", + "type": "General", + "title": { + "text": "Give details about {company_name}", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "answers", + "identifier": "company-or-branch-name" + } + } + ] + }, + "answers": [ + { + "id": "registration-number", + "label": "Registration number (Mandatory)", + "mandatory": true, + "type": "Number", + "maximum": { + "value": 999, + "exclusive": false + }, + "decimal_places": 0 + }, + { + "id": "registration-date", + "label": "Date of Registration (Mandatory)", + "mandatory": true, + "type": "Date", + "maximum": { + "value": "now" + } + } + ] + } + }, + { + "id": "companies-repeating-block-2", + "type": "ListRepeatingQuestion", + "question": { + "id": "companies-repeating-block-2-question", + "type": "General", + "title": { + "text": "Give details about how {company_name} has been trading over the past {date_difference}.", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "answers", + "identifier": "company-or-branch-name" + } + }, + { + "placeholder": "date_difference", + "transforms": [ + { + "transform": "calculate_date_difference", + "arguments": { + "first_date": { + "source": "answers", + "identifier": "registration-date" + }, + "second_date": { + "value": "now" + } + } + } + ] + } + ] + }, + "answers": [ + { + "type": "Radio", + "label": "Has this company been trading in the UK? (Mandatory)", + "id": "authorised-trader-uk-radio", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + }, + { + "type": "Radio", + "label": "Has this company been trading in the EU? (Not mandatory)", + "id": "authorised-trader-eu-radio", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + } + ], + "edit_block": { + "id": "edit-company", + "type": "ListEditQuestion", + "question": { + "id": "edit-question-companies", + "type": "General", + "title": "What is the name and registration number of the company?", + "answers": [ + { + "id": "company-or-branch-name", + "label": "Name of UK company or branch (Mandatory)", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "remove_block": { + "id": "remove-company", + "type": "ListRemoveQuestion", + "question": { + "id": "remove-question-companies", + "type": "General", + "title": "Are you sure you want to remove this company or UK branch?", + "answers": [ + { + "id": "remove-confirmation-company", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RemoveListItemAndAnswers" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + "summary": { + "title": "Companies or UK branches", + "item_title": { + "text": "{company_name}", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "answers", + "identifier": "company-or-branch-name" + } + } + ] + } + } + }, + { + "id": "any-other-trading-details", + "type": "Question", + "question": { + "id": "any-other-trading-details-question", + "type": "General", + "title": "Do you have any other details about the trading you have reported for?", + "answers": [ + { + "id": "any-other-trading-details-answer", + "label": "Additional details", + "mandatory": false, + "type": "TextField" + } + ] + } + } + ] + } + ] + }, + { + "id": "section-businesses", + "title": "General insurance business", + "summary": { + "show_on_completion": true, + "items": [ + { + "type": "List", + "for_list": "businesses", + "title": "Businesses or UK branches", + "item_anchor_answer_id": "business-or-branch-name", + "item_label": "Name of UK business or branch", + "add_link_text": "Add another UK business or branch", + "empty_list_text": "No UK business or branch added" + } + ], + "show_non_item_answers": true + }, + "groups": [ + { + "id": "group-businesses", + "blocks": [ + { + "type": "Question", + "id": "responsible-party-business", + "question": { + "type": "General", + "id": "responsible-party-business-question", + "title": "Are you the responsible party for reporting trading details for a business of branch?", + "answers": [ + { + "type": "Radio", + "id": "responsible-party-business-answer", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "routing_rules": [ + { + "block": "any-businesses-or-branches", + "when": { + "==": [ + "Yes", + { + "source": "answers", + "identifier": "responsible-party-business-answer" + } + ] + } + }, + { + "section": "End" + } + ] + }, + { + "type": "ListCollectorDrivingQuestion", + "id": "any-businesses-or-branches", + "for_list": "businesses", + "question": { + "type": "General", + "id": "any-businesses-or-branches-question", + "title": "Do any businesses or branches within your United Kingdom group undertake general insurance business?", + "answers": [ + { + "type": "Radio", + "id": "any-businesses-or-branches-answer", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock", + "params": { + "block_id": "add-business", + "list_name": "businesses" + } + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "routing_rules": [ + { + "when": { + "==": [ + { + "source": "answers", + "identifier": "any-businesses-or-branches-answer" + }, + "Yes" + ] + }, + "block": "any-other-business-businesses-or-branches" + }, + { + "section": "End" + } + ] + }, + { + "id": "any-other-business-businesses-or-branches", + "type": "ListCollector", + "for_list": "businesses", + "question": { + "id": "any-other-business-businesses-or-branches-question", + "type": "General", + "title": "Do you need to add any other UK businesses or branches that undertake general insurance business?", + "answers": [ + { + "id": "any-other-business-businesses-or-branches-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "add_block": { + "id": "add-business", + "type": "ListAddQuestion", + "question": { + "id": "add-question-businesses", + "type": "General", + "title": "What is the name and registration number of the business?", + "answers": [ + { + "id": "business-or-branch-name", + "label": "Name of UK business or branch (Mandatory)", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "repeating_blocks": [ + { + "id": "businesses-repeating-block-1", + "type": "ListRepeatingQuestion", + "question": { + "id": "businesses-repeating-block-1-question", + "type": "General", + "title": { + "text": "Give details about {business_name}", + "placeholders": [ + { + "placeholder": "business_name", + "value": { + "source": "answers", + "identifier": "business-or-branch-name" + } + } + ] + }, + "answers": [ + { + "id": "registration-business-number", + "label": "Registration number (Mandatory)", + "mandatory": true, + "type": "Number", + "maximum": { + "value": 999, + "exclusive": false + }, + "decimal_places": 0 + }, + { + "id": "registration-business-date", + "label": "Date of Registration (Mandatory)", + "mandatory": true, + "type": "Date", + "maximum": { + "value": "now" + } + } + ] + } + }, + { + "id": "businesses-repeating-block-2", + "type": "ListRepeatingQuestion", + "question": { + "id": "businesses-repeating-block-2-question", + "type": "General", + "title": { + "text": "Give details about how {business_name} has been trading over the past {date_difference}.", + "placeholders": [ + { + "placeholder": "business_name", + "value": { + "source": "answers", + "identifier": "business-or-branch-name" + } + }, + { + "placeholder": "date_difference", + "transforms": [ + { + "transform": "calculate_date_difference", + "arguments": { + "first_date": { + "source": "answers", + "identifier": "registration-business-date" + }, + "second_date": { + "value": "now" + } + } + } + ] + } + ] + }, + "answers": [ + { + "type": "Radio", + "label": "Has this business been trading in the UK? (Mandatory)", + "id": "authorised-business-trader-uk-radio", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + }, + { + "type": "Radio", + "label": "Has this business been trading in the EU? (Not mandatory)", + "id": "authorised-business-trader-eu-radio", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + } + ], + "edit_block": { + "id": "edit-business", + "type": "ListEditQuestion", + "question": { + "id": "edit-question-businesses", + "type": "General", + "title": "What is the name and registration number of the business?", + "answers": [ + { + "id": "business-or-branch-name", + "label": "Name of UK business or branch (Mandatory)", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "remove_block": { + "id": "remove-business", + "type": "ListRemoveQuestion", + "question": { + "id": "remove-question-businesses", + "type": "General", + "title": "Are you sure you want to remove this business or UK branch?", + "answers": [ + { + "id": "remove-confirmation-business", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RemoveListItemAndAnswers" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + "summary": { + "title": "Businesses or UK branches", + "item_title": { + "text": "{business_name}", + "placeholders": [ + { + "placeholder": "business_name", + "value": { + "source": "answers", + "identifier": "business-or-branch-name" + } + } + ] + } + } + }, + { + "id": "any-other-business-trading-details", + "type": "Question", + "question": { + "id": "any-other-business-trading-details-question", + "type": "General", + "title": "Do you have any other details about the trading you have reported for?", + "answers": [ + { + "id": "any-other-business-trading-details-answer", + "label": "Additional details", + "mandatory": false, + "type": "TextField" + } + ] + } + } + ] + } + ] + } + ], "navigation": { "visible": false }, diff --git a/schemas/test/en/test_big_list_naughty_strings.json b/schemas/test/en/test_big_list_naughty_strings.json index 1443ecb7a1..fa748320e9 100644 --- a/schemas/test/en/test_big_list_naughty_strings.json +++ b/schemas/test/en/test_big_list_naughty_strings.json @@ -34,3549 +34,3042 @@ "answers": [ { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer0" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer1" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer2" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer3" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer4" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer5" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer6" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer7" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer8" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer9" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer10" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer11" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer12" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer13" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer14" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer15" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer16" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer17" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer18" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer19" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer20" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer21" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer22" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer23" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer24" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer25" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer26" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer27" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer28" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer29" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer30" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer31" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer32" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer33" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer34" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer35" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer36" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer37" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer38" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer39" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer40" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer41" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer42" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer43" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer44" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer45" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer46" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer47" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer48" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer49" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer50" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer51" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer52" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer53" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer54" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer55" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer56" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer57" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer58" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer59" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer60" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer61" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer62" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer63" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer64" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer65" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer66" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer67" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer68" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer69" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer70" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer71" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer72" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer73" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer74" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer75" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer76" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer77" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer78" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer79" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer80" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer81" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer82" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer83" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer84" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer85" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer86" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer87" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer88" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer89" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer90" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer91" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer92" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer93" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer94" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer95" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer96" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer97" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer98" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer99" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer100" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer101" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer102" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer103" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer104" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer105" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer106" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer107" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer108" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer109" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer110" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer111" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer112" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer113" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer114" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer115" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer116" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer117" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer118" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer119" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer120" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer121" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer122" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer123" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer124" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer125" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer126" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer127" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer128" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer129" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer130" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer131" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer132" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer133" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer134" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer135" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer136" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer137" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer138" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer139" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer140" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer141" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer142" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer143" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer144" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer145" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer146" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer147" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer148" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer149" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer150" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer151" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer152" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer153" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer154" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer155" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer156" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer157" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer158" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer159" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer160" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer161" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer162" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer163" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer164" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer165" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer166" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer167" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer168" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer169" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer170" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer171" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer172" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer173" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer174" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer175" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer176" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer177" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer178" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer179" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer180" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer181" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer182" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer183" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer184" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer185" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer186" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer187" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer188" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer189" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer190" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer191" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer192" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer193" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer194" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer195" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer196" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer197" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer198" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer199" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer200" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer201" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer202" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer203" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer204" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer205" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer206" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer207" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer208" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer209" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer210" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer211" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer212" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer213" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer214" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer215" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer216" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer217" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer218" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer219" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer220" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer221" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer222" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer223" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer224" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer225" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer226" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer227" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer228" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer229" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer230" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer231" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer232" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer233" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer234" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer235" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer236" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer237" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer238" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer239" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer240" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer241" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer242" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer243" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer244" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer245" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer246" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer247" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer248" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer249" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer250" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer251" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer252" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer253" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer254" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer255" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer256" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer257" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer258" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer259" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer260" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer261" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer262" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer263" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer264" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer265" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer266" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer267" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer268" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer269" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer270" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer271" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer272" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer273" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer274" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer275" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer276" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer277" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer278" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer279" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer280" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer281" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer282" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer283" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer284" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer285" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer286" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer287" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer288" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer289" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer290" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer291" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer292" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer293" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer294" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer295" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer296" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer297" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer298" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer299" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer300" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer301" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer302" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer303" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer304" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer305" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer306" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer307" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer308" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer309" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer310" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer311" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer312" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer313" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer314" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer315" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer316" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer317" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer318" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer319" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer320" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer321" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer322" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer323" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer324" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer325" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer326" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer327" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer328" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer329" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer330" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer331" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer332" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer333" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer334" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer335" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer336" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer337" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer338" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer339" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer340" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer341" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer342" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer343" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer344" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer345" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer346" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer347" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer348" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer349" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer350" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer351" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer352" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer353" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer354" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer355" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer356" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer357" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer358" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer359" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer360" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer361" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer362" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer363" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer364" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer365" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer366" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer367" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer368" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer369" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer370" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer371" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer372" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer373" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer374" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer375" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer376" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer377" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer378" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer379" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer380" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer381" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer382" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer383" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer384" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer385" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer386" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer387" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer388" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer389" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer390" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer391" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer392" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer393" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer394" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer395" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer396" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer397" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer398" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer399" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer400" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer401" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer402" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer403" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer404" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer405" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer406" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer407" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer408" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer409" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer410" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer411" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer412" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer413" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer414" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer415" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer416" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer417" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer418" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer419" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer420" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer421" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer422" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer423" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer424" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer425" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer426" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer427" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer428" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer429" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer430" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer431" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer432" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer433" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer434" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer435" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer436" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer437" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer438" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer439" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer440" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer441" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer442" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer443" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer444" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer445" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer446" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer447" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer448" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer449" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer450" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer451" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer452" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer453" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer454" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer455" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer456" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer457" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer458" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer459" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer460" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer461" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer462" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer463" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer464" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer465" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer466" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer467" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer468" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer469" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer470" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer471" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer472" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer473" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer474" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer475" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer476" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer477" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer478" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer479" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer480" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer481" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer482" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer483" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer484" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer485" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer486" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer487" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer488" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer489" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer490" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer491" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer492" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer493" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer494" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer495" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer496" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer497" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer498" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer499" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer500" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer501" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer502" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer503" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer504" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer505" }, { "type": "TextArea", - "q_code": "0", "mandatory": false, "label": "Enter your comments", "id": "answer506" diff --git a/schemas/test/en/test_calculated_and_grand_calculated_summary_decimals.json b/schemas/test/en/test_calculated_and_grand_calculated_summary_decimals.json new file mode 100644 index 0000000000..278f140a47 --- /dev/null +++ b/schemas/test/en/test_calculated_and_grand_calculated_summary_decimals.json @@ -0,0 +1,271 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "A test schema to demo Calculated Summary", + "theme": "default", + "description": "A schema to showcase Calculated Summary pages and usage in value source.", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Linear", + "options": { + "summary": { + "collapsible": false + } + } + }, + "sections": [ + { + "id": "default-section", + "groups": [ + { + "id": "group", + "title": "Total a range of values", + "blocks": [ + { + "type": "Question", + "id": "first-number-block", + "question": { + "id": "first-number-question", + "title": "First Number Question Title", + "type": "General", + "answers": [ + { + "id": "first-number-answer", + "label": "First answer label (Decimal limit: 1)", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 1 + } + ] + } + }, + { + "type": "Question", + "id": "second-number-block", + "question": { + "id": "second-number-question", + "title": "Second Number Question Title", + "type": "General", + "answers": [ + { + "id": "second-number-answer", + "label": "Second answer in currency label (Decimal limit: 2)", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + }, + { + "id": "second-number-answer-also-in-total", + "label": "Second answer label also in currency total (Decimal limit: 3)", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 3 + } + ] + } + }, + { + "type": "Question", + "id": "third-number-block", + "question": { + "id": "third-number-question", + "title": "Third Number Question Title", + "type": "General", + "answers": [ + { + "id": "third-number-answer", + "label": "Third answer label (Decimal limit: 4)", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 4 + } + ] + } + }, + { + "type": "Question", + "id": "fourth-number-block", + "question": { + "id": "fourth-number-question", + "title": "Fourth Number Question Title", + "type": "General", + "answers": [ + { + "id": "fourth-number-answer", + "label": "Fourth answer label (Decimal limit: 5)", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 5 + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "currency-total-playback", + "title": "We calculate the total of currency values entered to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "answers", + "identifier": "first-number-answer" + }, + { + "source": "answers", + "identifier": "second-number-answer" + }, + { + "source": "answers", + "identifier": "second-number-answer-also-in-total" + }, + { + "source": "answers", + "identifier": "third-number-answer" + }, + { + "source": "answers", + "identifier": "fourth-number-answer" + } + ] + }, + "title": "Grand total of previous values" + } + }, + { + "type": "Interstitial", + "id": "calculated-summary-total-confirmation", + "content": { + "title": "You have provided the following grand totals.", + "contents": [ + { + "list": [ + { + "text": "Total currency values: {currency_total}", + "placeholders": [ + { + "placeholder": "currency_total", + "transforms": [ + { + "transform": "format_currency", + "arguments": { + "number": { + "source": "calculated_summary", + "identifier": "currency-total-playback" + } + } + } + ] + } + ] + } + ] + } + ] + } + }, + { + "type": "Question", + "id": "fifth-number-block", + "question": { + "id": "fifth-number-question", + "title": "Fifth Number Question Title", + "type": "General", + "answers": [ + { + "id": "fifth-number-answer", + "label": "Fifth answer label (Decimal limit: 2)", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "Question", + "id": "sixth-number-block", + "question": { + "id": "sixth-number-question", + "title": "Sixth Number Question Title", + "type": "General", + "answers": [ + { + "id": "sixth-number-answer", + "label": "Fifth answer label (Decimal limit: 2)", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "currency-total-playback-2", + "title": "We calculate the total of currency values entered to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "answers", + "identifier": "fifth-number-answer" + }, + { + "source": "answers", + "identifier": "sixth-number-answer" + } + ] + }, + "title": "Grand total of previous values" + } + }, + { + "type": "GrandCalculatedSummary", + "id": "currency-grand-summary", + "title": "We calculate the grand total to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "calculated_summary", + "identifier": "currency-total-playback" + }, + { + "source": "calculated_summary", + "identifier": "currency-total-playback-2" + } + ] + }, + "title": "Grand calculated summary of journeys" + } + } + ] + } + ] + } + ] +} diff --git a/schemas/test/en/test_calculated_summary.json b/schemas/test/en/test_calculated_summary.json index 0599f2ba22..82d6586c60 100644 --- a/schemas/test/en/test_calculated_summary.json +++ b/schemas/test/en/test_calculated_summary.json @@ -51,7 +51,11 @@ "mandatory": true, "type": "Currency", "currency": "GBP", - "decimal_places": 2 + "decimal_places": 2, + "minimum": { + "value": -1000, + "exclusive": false + } } ] } @@ -70,7 +74,11 @@ "mandatory": true, "type": "Currency", "currency": "GBP", - "decimal_places": 2 + "decimal_places": 2, + "minimum": { + "value": -1000, + "exclusive": false + } }, { "id": "second-number-answer-unit-total", @@ -86,7 +94,11 @@ "mandatory": false, "type": "Currency", "currency": "GBP", - "decimal_places": 2 + "decimal_places": 2, + "minimum": { + "value": -1000, + "exclusive": false + } } ] } @@ -105,7 +117,11 @@ "mandatory": true, "type": "Currency", "currency": "GBP", - "decimal_places": 2 + "decimal_places": 2, + "minimum": { + "value": -1000, + "exclusive": false + } } ] } @@ -156,17 +172,17 @@ } }, { - "skip_conditions": [ - { - "when": [ + "skip_conditions": { + "when": { + "==": [ { - "id": "skip-fourth-block-answer", - "condition": "equals", - "value": "Yes" - } + "identifier": "skip-fourth-block-answer", + "source": "answers" + }, + "Yes" ] } - ], + }, "type": "Question", "id": "fourth-number-block", "question": { @@ -186,17 +202,17 @@ } }, { - "skip_conditions": [ - { - "when": [ + "skip_conditions": { + "when": { + "==": [ { - "id": "skip-fourth-block-answer", - "condition": "equals", - "value": "Yes" - } + "identifier": "skip-fourth-block-answer", + "source": "answers" + }, + "Yes" ] } - ], + }, "type": "Question", "id": "fourth-and-a-half-number-block", "question": { @@ -271,34 +287,8 @@ }, { "type": "CalculatedSummary", - "id": "currency-total-playback-skipped-fourth", - "title": "We calculate the total of currency values entered to be %(total)s. Is this correct? (Skipped Fourth)", - "calculation": { - "calculation_type": "sum", - "answers_to_calculate": [ - "first-number-answer", - "second-number-answer", - "second-number-answer-also-in-total", - "third-number-answer" - ], - "title": "Grand total of previous values" - }, - "skip_conditions": [ - { - "when": [ - { - "id": "skip-fourth-block-answer", - "condition": "equals", - "value": "No" - } - ] - } - ] - }, - { - "type": "CalculatedSummary", - "id": "currency-total-playback-with-fourth", - "title": "We calculate the total of currency values entered to be %(total)s. Is this correct? (With Fourth)", + "id": "currency-total-playback", + "title": "We calculate the total of currency values entered to be %(total)s. Is this correct?", "calculation": { "calculation_type": "sum", "answers_to_calculate": [ @@ -310,23 +300,13 @@ "fourth-and-a-half-number-answer-also-in-total" ], "title": "Grand total of previous values" - }, - "skip_conditions": [ - { - "when": [ - { - "id": "skip-fourth-block-answer", - "condition": "equals", - "value": "Yes" - } - ] - } - ] + } }, { "type": "CalculatedSummary", "id": "unit-total-playback", "title": "We calculate the total of unit values entered to be %(total)s. Is this correct?", + "page_title": "Total Unit Values", "calculation": { "calculation_type": "sum", "answers_to_calculate": ["second-number-answer-unit-total", "third-and-a-half-number-answer-unit-total"], @@ -362,17 +342,17 @@ { "list": [ { - "text": "Total currency values (if Q4 not skipped): {total}", + "text": "Total currency values: {currency_total}", "placeholders": [ { - "placeholder": "total", + "placeholder": "currency_total", "transforms": [ { "transform": "format_currency", "arguments": { "number": { "source": "calculated_summary", - "identifier": "currency-total-playback-with-fourth" + "identifier": "currency-total-playback" } } } @@ -381,17 +361,17 @@ ] }, { - "text": "Total currency values (if Q4 skipped)): {total}", + "text": "Total unformatted unit values: {unit_total}", "placeholders": [ { - "placeholder": "total", + "placeholder": "unit_total", "transforms": [ { - "transform": "format_currency", + "transform": "format_number", "arguments": { "number": { "source": "calculated_summary", - "identifier": "currency-total-playback-skipped-fourth" + "identifier": "unit-total-playback" } } } @@ -400,18 +380,20 @@ ] }, { - "text": "Total unit values: {total}", + "text": "Total formatted unit values: {unit_total}", "placeholders": [ { - "placeholder": "total", + "placeholder": "unit_total", "transforms": [ { - "transform": "format_number", + "transform": "format_unit", "arguments": { - "number": { + "value": { "source": "calculated_summary", "identifier": "unit-total-playback" - } + }, + "unit": "length-centimeter", + "unit_length": "short" } } ] @@ -419,10 +401,10 @@ ] }, { - "text": "Total percentage values: {total}", + "text": "Total unformatted percentage values: {percentage_total}", "placeholders": [ { - "placeholder": "total", + "placeholder": "percentage_total", "transforms": [ { "transform": "format_number", @@ -438,10 +420,29 @@ ] }, { - "text": "Total number values: {total}", + "text": "Total formatted percentage values: {percentage_total}", "placeholders": [ { - "placeholder": "total", + "placeholder": "percentage_total", + "transforms": [ + { + "transform": "format_percentage", + "arguments": { + "value": { + "source": "calculated_summary", + "identifier": "percentage-total-playback" + } + } + } + ] + } + ] + }, + { + "text": "Total number values: {number_total}", + "placeholders": [ + { + "placeholder": "number_total", "transforms": [ { "transform": "format_number", @@ -460,6 +461,58 @@ } ] } + }, + { + "type": "Question", + "id": "set-min-max-block", + "question": { + "answers": [ + { + "id": "set-minimum-answer", + "label": "Set a value greater than the total above", + "mandatory": true, + "description": "This is a description of the minimum value", + "type": "Currency", + "currency": "GBP", + "decimal_places": 2, + "minimum": { + "value": { + "source": "calculated_summary", + "identifier": "currency-total-playback" + } + } + }, + { + "id": "set-maximum-answer", + "description": "This is a description of the maximum value", + "label": "Set a value less than the total above", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2, + "maximum": { + "value": { + "source": "calculated_summary", + "identifier": "currency-total-playback" + } + } + } + ], + "id": "set-min-question", + "title": { + "placeholders": [ + { + "placeholder": "calculated_summary_answer", + "value": { + "identifier": "currency-total-playback", + "source": "calculated_summary" + } + } + ], + "text": "Set minimum and maximum values based on your calculated summary total of ÂŖ{calculated_summary_answer}" + }, + "type": "General" + } } ] } diff --git a/schemas/test/en/test_calculated_summary_cross_section_dependencies.json b/schemas/test/en/test_calculated_summary_cross_section_dependencies.json new file mode 100644 index 0000000000..5d8192d724 --- /dev/null +++ b/schemas/test/en/test_calculated_summary_cross_section_dependencies.json @@ -0,0 +1,355 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Calculated Summary Cross Section Dependencies", + "theme": "default", + "description": "A questionnaire to demo resolution of calculated summary values across sections", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Hub", + "options": {} + }, + "sections": [ + { + "id": "questions-section", + "title": "Questions", + "summary": { "show_on_completion": true }, + "groups": [ + { + "id": "radio", + "title": "Questions", + "blocks": [ + { + "type": "Question", + "id": "skip-first-block", + "question": { + "type": "General", + "id": "skip-first-block-question", + "title": "Skip First Block so it doesn’t appear in Total?", + "answers": [ + { + "type": "Radio", + "id": "skip-first-block-answer", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + { + "skip_conditions": { + "when": { + "==": [ + { + "identifier": "skip-first-block-answer", + "source": "answers" + }, + "Yes" + ] + } + }, + "type": "Question", + "id": "first-number-block", + "question": { + "id": "first-number-question", + "title": "First Number Question Title", + "type": "General", + "answers": [ + { + "id": "first-number-answer", + "label": "First answer label (optional)", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "skip_conditions": { + "when": { + "==": [ + { + "identifier": "skip-first-block-answer", + "source": "answers" + }, + "Yes" + ] + } + }, + "type": "Question", + "id": "first-and-a-half-number-block", + "question": { + "id": "first-and-a-half-number-question-also-in-total", + "title": "First Number Additional Question Title", + "type": "General", + "answers": [ + { + "id": "first-and-a-half-number-answer-also-in-total", + "label": "First answer label also in total (optional)", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "Question", + "id": "second-number-block", + "question": { + "id": "second-number-question-also-in-total", + "title": "Second Number Additional Question Title", + "type": "General", + "answers": [ + { + "id": "second-number-answer-also-in-total", + "label": "Second answer label also in total (optional)", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "currency-total-playback-1", + "title": "We calculate the total of currency values entered to be %(total)s. Is this correct?", + "calculation": { + "calculation_type": "sum", + "answers_to_calculate": [ + "first-number-answer", + "first-and-a-half-number-answer-also-in-total", + "second-number-answer-also-in-total" + ], + "title": "Grand total of previous values" + } + } + ] + } + ] + }, + { + "id": "calculated-summary-section", + "title": "Calculated Summary", + "summary": { "show_on_completion": true }, + "groups": [ + { + "blocks": [ + { + "type": "Question", + "id": "third-number-block", + "question": { + "id": "third-number-question", + "title": "Third Number Question Title", + "type": "General", + "answers": [ + { + "id": "third-number-answer", + "label": "Third answer in currency label", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + }, + { + "id": "third-number-answer-also-in-total", + "label": "Third answer label also in currency total (optional)", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "currency-total-playback-2", + "title": "We calculate the total of currency values entered to be %(total)s. Is this correct?", + "calculation": { + "calculation_type": "sum", + "answers_to_calculate": ["third-number-answer", "third-number-answer-also-in-total"], + "title": "Grand total of previous values" + } + }, + { + "type": "Question", + "id": "mutually-exclusive-checkbox", + "question": { + "id": "mutually-exclusive-checkbox-question", + "type": "MutuallyExclusive", + "title": "Which answer did you give to question 4 and a half?", + "mandatory": false, + "answers": [ + { + "id": "checkbox-answer", + "instruction": "Select an answer", + "type": "Checkbox", + "mandatory": false, + "options": [ + { + "label": { + "placeholders": [ + { + "placeholder": "answer_value_1", + "value": { + "identifier": "first-and-a-half-number-answer-also-in-total", + "source": "answers" + } + } + ], + "text": "{answer_value_1} - first and a half answer" + }, + "value": "{answer_value_1}" + }, + { + "label": { + "placeholders": [ + { + "placeholder": "calc_value_1", + "value": { + "identifier": "currency-total-playback-1", + "source": "calculated_summary" + } + } + ], + "text": "{calc_value_1} - calculated summary answer (previous section)" + }, + "value": "{calc_value_1}" + }, + { + "label": { + "placeholders": [ + { + "placeholder": "calc_value_2", + "value": { + "identifier": "currency-total-playback-2", + "source": "calculated_summary" + } + } + ], + "text": "{calc_value_2} - calculated summary answer (current section)" + }, + "value": "{calc_value_2}" + }, + { + "label": { + "placeholders": [ + { + "placeholder": "third_answer_value", + "value": { + "identifier": "third-number-answer", + "source": "answers" + } + } + ], + "text": "{third_answer_value} - third answer" + }, + "value": "{third_answer_value}" + } + ] + }, + { + "id": "checkbox-exclusive-answer", + "mandatory": false, + "type": "Checkbox", + "options": [ + { + "label": "I prefer not to say", + "description": "Some description", + "value": "I prefer not to say" + } + ] + } + ] + } + }, + { + "type": "Question", + "id": "set-min-max-block", + "question": { + "answers": [ + { + "id": "set-minimum-answer", + "label": "Set a value greater than the total above", + "mandatory": true, + "description": "This is a description of the minimum value", + "type": "Currency", + "currency": "GBP", + "decimal_places": 2, + "minimum": { + "value": { + "source": "calculated_summary", + "identifier": "currency-total-playback-1" + } + } + }, + { + "id": "set-maximum-answer", + "description": "This is a description of the maximum value", + "label": "Set a value less than the total above", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2, + "maximum": { + "value": { + "source": "calculated_summary", + "identifier": "currency-total-playback-1" + } + } + } + ], + "id": "set-min-question", + "title": { + "placeholders": [ + { + "placeholder": "calculated_summary_answer", + "value": { + "identifier": "currency-total-playback-1", + "source": "calculated_summary" + } + } + ], + "text": "Set minimum and maximum values based on your calculated summary total of ÂŖ{calculated_summary_answer}" + }, + "type": "General" + } + } + ], + "id": "calculated-summary" + } + ] + } + ] +} diff --git a/schemas/test/en/test_calculated_summary_dependent_questions.json b/schemas/test/en/test_calculated_summary_dependent_questions.json new file mode 100644 index 0000000000..2d15fa8d37 --- /dev/null +++ b/schemas/test/en/test_calculated_summary_dependent_questions.json @@ -0,0 +1,463 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "A test schema to demo Calculated Summary", + "description": "A schema to showcase Calculated Summary with dependent questions.", + "questionnaire_flow": { + "type": "Hub", + "options": { + "required_completed_sections": ["default-section", "list-collector-section"] + } + }, + "sections": [ + { + "id": "default-section", + "title": "Section 1", + "summary": { + "show_on_completion": false, + "collapsible": false + }, + "show_on_hub": true, + "groups": [ + { + "id": "group-1", + "blocks": [ + { + "id": "block-1", + "type": "Question", + "question": { + "id": "question-1", + "title": "How much did you spend on food?", + "type": "General", + "answers": [ + { + "id": "answer-1", + "mandatory": true, + "type": "Currency", + "label": "Money spent on food", + "description": "Enter the full value", + "minimum": { + "value": 0, + "exclusive": true + }, + "decimal_places": 2, + "currency": "GBP" + } + ] + } + }, + { + "id": "block-2", + "type": "Question", + "question": { + "id": "question-2", + "title": "Of the money spent on food, how much did you spend on vegetables?", + "type": "General", + "answers": [ + { + "id": "answer-2", + "mandatory": true, + "type": "Currency", + "label": "Money spent on vegetables", + "description": "Enter the full value", + "minimum": { + "value": 0, + "exclusive": true + }, + "maximum": { + "value": { + "identifier": "answer-1", + "source": "answers" + }, + "exclusive": false + }, + "decimal_places": 2, + "currency": "GBP" + } + ] + } + }, + { + "id": "block-3", + "type": "Question", + "question": { + "id": "question-3", + "title": "How much did you spend on clothes?", + "type": "General", + "answers": [ + { + "id": "answer-3", + "mandatory": true, + "type": "Currency", + "label": "Money spent on clothes", + "description": "Enter the full value", + "minimum": { + "value": 0, + "exclusive": true + }, + "decimal_places": 2, + "currency": "GBP" + } + ] + } + }, + { + "id": "block-4", + "type": "Question", + "question": { + "id": "question-4", + "title": "Of the money spent on clothes, how much did you spend on shoes?", + "type": "General", + "answers": [ + { + "id": "answer-4", + "mandatory": true, + "type": "Currency", + "label": "Money spent on shoes", + "description": "Enter the full value", + "minimum": { + "value": 0, + "exclusive": true + }, + "maximum": { + "value": { + "identifier": "answer-3", + "source": "answers" + }, + "exclusive": false + }, + "decimal_places": 2, + "currency": "GBP" + } + ] + } + }, + + { + "id": "calculated-summary-block", + "type": "CalculatedSummary", + "title": "We have calculated your total spend to be %(total)s. Is this correct?", + "calculation": { + "calculation_type": "sum", + "answers_to_calculate": ["answer-1", "answer-3"], + "title": "Total capital expenditure" + } + } + ] + } + ] + }, + { + "id": "list-collector-section", + "title": "Additional sites", + "summary": { + "show_on_completion": false, + "collapsible": false, + "items": [ + { + "type": "List", + "for_list": "additional_sites_name", + "title": "What was the business name for this site?", + "item_anchor_answer_id": "business-name", + "item_label": "

Business name

", + "add_link_text": "Add item to this list", + "empty_list_text": "There are no items" + } + ], + "show_non_item_answers": true + }, + "show_on_hub": true, + "groups": [ + { + "id": "list-collector-group", + "blocks": [ + { + "id": "additional-sites-for-your-business", + "type": "ListCollectorDrivingQuestion", + "for_list": "additional_sites_name", + "question": { + "id": "question-driving-further-additional-sites-for-your-business", + "type": "General", + "title": "Did your business have any additional sites that were staffed for a minimum of 20 hours per week or had planned activity for more than a year?", + "answers": [ + { + "id": "additional-sites-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock", + "params": { + "block_id": "add-block-business-name-trading-style-and-address-for-this-additional-site", + "list_name": "additional_sites_name" + } + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "routing_rules": [ + { + "section": "End", + "when": { + "in": [ + { + "source": "answers", + "identifier": "additional-sites-answer" + }, + ["No"] + ] + } + }, + { + "block": "further-additional-sites-for-your-business" + } + ], + "page_title": "Additional sites for your business" + }, + { + "id": "further-additional-sites-for-your-business", + "type": "ListCollector", + "page_title": "Further additional sites for your business", + "for_list": "additional_sites_name", + "question": { + "id": "list-collector-question-further-additional-sites-for-your-business", + "type": "General", + "title": "Did you have any other sites that were staffed for a minimum of 20 hours per week or had planned activity for more than a year?", + "answers": [ + { + "id": "any-other-additional-sites-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "add_block": { + "id": "add-block-business-name-trading-style-and-address-for-this-additional-site", + "type": "ListAddQuestion", + "page_title": "Business name, trading style and address for this additional site", + "cancel_text": "Don’t need to add this item", + "question": { + "id": "add-block-question-business-name-trading-style-and-address-for-this-additional-site", + "type": "General", + "title": "What was the business name for this site?", + "answers": [ + { + "id": "business-name", + "mandatory": true, + "type": "TextField", + "label": "Business name" + } + ] + } + }, + "edit_block": { + "id": "edit-block-business-name-trading-style-and-address-for-this-additional-site", + "type": "ListEditQuestion", + "page_title": "Business name, trading style and address for this additional site", + "cancel_text": "Don’t need to edit this item", + "question": { + "id": "edit-block-question-business-name-trading-style-and-address-for-this-additional-site", + "type": "General", + "title": "What was the business name, trading style and address for this site?", + "answers": [ + { + "id": "business-name", + "mandatory": true, + "type": "TextField", + "label": "Business name" + } + ] + } + }, + "remove_block": { + "id": "remove-block-business-name-trading-style-and-address-for-this-additional-site", + "type": "ListRemoveQuestion", + "cancel_text": "Don’t need to remove this item?", + "question": { + "id": "remove-block-question-business-name-trading-style-and-address-for-this-additional-site", + "type": "General", + "title": "Are you sure you want to remove this item?", + "warning": "All of the information about this item will be deleted", + "answers": [ + { + "id": "remove-confirmation-business-name-trading-style-and-address-for-this-additional-site", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RemoveListItemAndAnswers" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + "summary": { + "title": "Summary", + "item_title": { + "text": "{listcollector_summary_placeholder}", + "placeholders": [ + { + "placeholder": "listcollector_summary_placeholder", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "business-name" + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + } + } + } + ] + } + ] + }, + { + "id": "repeating-section", + "title": "Additional site details", + "repeat": { + "for_list": "additional_sites_name", + "title": { + "text": "{repeat_title_placeholder}", + "placeholders": [ + { + "placeholder": "repeat_title_placeholder", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "business-name" + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + } + }, + "summary": { + "show_on_completion": false, + "page_title": "Details of Additional Sites in Great Britain", + "collapsible": false + }, + "show_on_hub": true, + "groups": [ + { + "id": "repeating-group", + "blocks": [ + { + "id": "number-of-employees-working-at-this-additional-site", + "type": "Question", + "question": { + "id": "question-number-of-employees-working-at-this-additional-site", + "title": "What was the number of full-time and part-time employees that your business paid from its payroll, for this site?", + "type": "General", + "answers": [ + { + "id": "number-full-time-employees", + "mandatory": true, + "type": "Number", + "label": "Number of full-time employees", + "decimal_places": 0 + }, + { + "id": "number-part-time-employees", + "mandatory": true, + "type": "Number", + "label": "Number of part-time employees", + "decimal_places": 0 + } + ] + }, + "page_title": "Number of employees working at this additional site" + }, + { + "id": "calculated-number-of-employees-for-this-additional-site", + "type": "CalculatedSummary", + "page_title": "Calculated number of employees for this additional site", + "title": "We have calculated the total number of employees that your business paid from its payroll, for this site, to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "identifier": "number-full-time-employees", + "source": "answers" + }, + { + "identifier": "number-part-time-employees", + "source": "answers" + } + ] + }, + "title": "Total number of employees paid from your business's payroll" + } + } + ] + } + ] + } + ], + "theme": "default", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ] +} diff --git a/schemas/test/en/test_checkbox.json b/schemas/test/en/test_checkbox.json index 925ce858a8..6232699d07 100644 --- a/schemas/test/en/test_checkbox.json +++ b/schemas/test/en/test_checkbox.json @@ -51,37 +51,30 @@ "options": [ { "label": "None", - "value": "None", - "q_code": "0" + "value": "None" }, { "label": "Ham & Cheese", - "value": "Ham & Cheese", - "q_code": "1" + "value": "Ham & Cheese" }, { "label": "Ham", - "value": "Ham", - "q_code": "2" + "value": "Ham" }, { "label": "Pineapple", - "value": "Pineapple", - "q_code": "3" + "value": "Pineapple" }, { "label": "Tuna", - "value": "Tuna", - "q_code": "4" + "value": "Tuna" }, { "label": "Pepperoni", - "value": "Pepperoni", - "q_code": "5" + "value": "Pepperoni" }, { "label": "Other", - "q_code": "6", "description": "Choose any other topping", "value": "Other", "detail_answer": { @@ -146,7 +139,6 @@ } } ], - "q_code": "20", "type": "Checkbox" } ], diff --git a/schemas/test/en/test_checkbox_detail_answer_multiple.json b/schemas/test/en/test_checkbox_detail_answer_multiple.json index f3221aae6d..99d965bc8b 100644 --- a/schemas/test/en/test_checkbox_detail_answer_multiple.json +++ b/schemas/test/en/test_checkbox_detail_answer_multiple.json @@ -51,13 +51,11 @@ "options": [ { "label": "None", - "value": "None", - "q_code": "0" + "value": "None" }, { "label": "Cheese", "value": "Cheese", - "q_code": "1", "detail_answer": { "mandatory": false, "id": "cheese-type-answer", @@ -67,27 +65,22 @@ }, { "label": "Ham", - "value": "Ham", - "q_code": "2" + "value": "Ham" }, { "label": "Pineapple", - "value": "Pineapple", - "q_code": "3" + "value": "Pineapple" }, { "label": "Tuna", - "value": "Tuna", - "q_code": "4" + "value": "Tuna" }, { "label": "Pepperoni", - "value": "Pepperoni", - "q_code": "5" + "value": "Pepperoni" }, { "label": "Your choice", - "q_code": "6", "description": "Choose any other topping", "value": "Your choice", "detail_answer": { diff --git a/schemas/test/en/test_checkbox_detail_answer_numeric.json b/schemas/test/en/test_checkbox_detail_answer_numeric.json index 812386aa8f..1796bcf13f 100644 --- a/schemas/test/en/test_checkbox_detail_answer_numeric.json +++ b/schemas/test/en/test_checkbox_detail_answer_numeric.json @@ -55,17 +55,14 @@ }, { "label": "1", - "value": "1", - "q_code": "1" + "value": "1" }, { "label": "2", - "value": "2", - "q_code": "2" + "value": "2" }, { "label": "Other", - "q_code": "3", "description": "Choose any number of toppings", "value": "Other", "detail_answer": { diff --git a/schemas/test/en/test_conditional_combined_routing.json b/schemas/test/en/test_conditional_combined_routing.json index d5c02c3187..a8a2935baa 100644 --- a/schemas/test/en/test_conditional_combined_routing.json +++ b/schemas/test/en/test_conditional_combined_routing.json @@ -65,7 +65,6 @@ "value": "No, I don’t drink any hot drinks" } ], - "q_code": "1", "id": "conditional-routing-answer", "label": "Which conditional question should we jump to?", "mandatory": true, @@ -75,33 +74,32 @@ }, "routing_rules": [ { - "goto": { - "block": "response-any", - "when": [ + "block": "response-any", + "when": { + "in": [ { - "id": "conditional-routing-answer", - "condition": "equals any", - "values": ["Yes", "Sometimes"] - } + "identifier": "conditional-routing-answer", + "source": "answers" + }, + ["Yes", "Sometimes"] ] } }, { - "goto": { - "block": "response-not-any", - "when": [ + "block": "response-not-any", + "when": { + "not": [ { - "id": "conditional-routing-answer", - "condition": "not equals any", - "values": ["Yes", "Sometimes", "I don’t like coffee", "No, I don’t drink any hot drinks"] + "in": [ + { "identifier": "conditional-routing-answer", "source": "answers" }, + ["Yes", "Sometimes", "I don’t like coffee", "No, I don’t drink any hot drinks"] + ] } ] } }, { - "goto": { - "section": "End" - } + "section": "End" } ] }, @@ -117,7 +115,6 @@ "id": "response-any-number-of-cups", "label": "Number of cups", "mandatory": true, - "q_code": "2", "type": "Number" } ] @@ -135,7 +132,6 @@ "id": "response-not-any-number-of-cups", "label": "Number of cups", "mandatory": true, - "q_code": "2", "type": "Number" } ] diff --git a/schemas/test/en/test_confirmation_email.json b/schemas/test/en/test_confirmation_email.json index cc7f2c51d3..835b153691 100644 --- a/schemas/test/en/test_confirmation_email.json +++ b/schemas/test/en/test_confirmation_email.json @@ -7,7 +7,7 @@ "form_type": "H", "region_code": "GB-WLS", "title": "Confirmation email test schema", - "theme": "census", + "theme": "default", "description": "A questionnaire to test confirmation email", "metadata": [ { @@ -19,7 +19,7 @@ "type": "string" }, { - "name": "display_address", + "name": "ru_name", "type": "string" } ], diff --git a/schemas/test/en/test_confirmation_question.json b/schemas/test/en/test_confirmation_question.json index 42e8d4fa71..1f526d4182 100644 --- a/schemas/test/en/test_confirmation_question.json +++ b/schemas/test/en/test_confirmation_question.json @@ -52,7 +52,6 @@ "answers": [ { "id": "number-of-employees-total", - "q_code": "50", "label": "Total number of employees", "mandatory": false, "type": "Number", @@ -92,17 +91,17 @@ { "type": "ConfirmationQuestion", "id": "confirm-zero-employees-block", - "skip_conditions": [ - { - "when": [ + "skip_conditions": { + "when": { + ">": [ { - "id": "number-of-employees-total", - "condition": "greater than", - "value": 0 - } + "source": "answers", + "identifier": "number-of-employees-total" + }, + 0 ] } - ], + }, "question": { "type": "General", "answers": [ @@ -115,17 +114,16 @@ "value": "Yes this is correct" }, { - "label": "No I need to change this", - "value": "No I need to change this" + "label": "No I need to correct this", + "value": "No I need to correct this" } ], - "mandatory": true, - "q_code": "d50" + "mandatory": true } ], "id": "confirm-zero-employees-question", "title": { - "text": "The current number of employees for {company_name} is 0, is this correct?", + "text": "The current number of employees for {company_name} is 0, is this correct?", "placeholders": [ { "placeholder": "company_name", @@ -152,21 +150,19 @@ }, "routing_rules": [ { - "goto": { - "when": [ + "when": { + "==": [ { - "value": "No I need to change this", - "id": "confirm-zero-employees-answer", - "condition": "equals" - } - ], - "block": "number-of-employees-total-block" - } + "identifier": "confirm-zero-employees-answer", + "source": "answers" + }, + "No I need to correct this" + ] + }, + "block": "number-of-employees-total-block" }, { - "goto": { - "section": "End" - } + "section": "End" } ] }, @@ -179,7 +175,6 @@ "id": "number-of-employees-male-more-30-hours", "label": "Number of male employees working more than 30 hours per week", "mandatory": false, - "q_code": "51", "type": "Number", "maximum": { "value": { @@ -192,7 +187,6 @@ "id": "number-of-employees-female-more-30-hours", "label": "Number of female employees working more than 30 hours per week", "mandatory": false, - "q_code": "52", "type": "Number", "maximum": { "value": { @@ -204,7 +198,7 @@ ], "id": "number-of-employees-split-question", "title": { - "text": "Of the {number_of_employees_total} total employees employed, how many male and female employees worked the following hours?", + "text": "Of the {number_of_employees_total} total employees employed, how many male and female employees worked the following hours?", "placeholders": [ { "placeholder": "number_of_employees_total", diff --git a/schemas/test/en/test_new_confirmation_question.json b/schemas/test/en/test_confirmation_question_backwards_routing.json similarity index 91% rename from schemas/test/en/test_new_confirmation_question.json rename to schemas/test/en/test_confirmation_question_backwards_routing.json index 6e8af6989d..b8bc14b8fe 100644 --- a/schemas/test/en/test_new_confirmation_question.json +++ b/schemas/test/en/test_confirmation_question_backwards_routing.json @@ -40,11 +40,11 @@ "sections": [ { "id": "default-section", - "title": "Questions", + "title": "Section 1", "groups": [ { "id": "confirmation", - "title": "Confirmation Question Test", + "title": "Confirmation Driver", "blocks": [ { "type": "Question", @@ -71,7 +71,19 @@ } ] } - }, + } + ] + } + ] + }, + { + "id": "section-2", + "title": "Section 2", + "groups": [ + { + "id": "group-2", + "title": "Confirmation Question", + "blocks": [ { "id": "number-of-employees-total-block", "question": { @@ -129,8 +141,8 @@ "value": "Yes" }, { - "label": "No", - "value": "No" + "label": "No I need to correct this", + "value": "No I need to correct this" } ], "mandatory": true @@ -150,7 +162,7 @@ "source": "answers", "identifier": "confirm-zero-employees-answer" }, - "No" + "No I need to correct this" ] }, { diff --git a/schemas/test/en/test_confirmation_question_within_repeating_section.json b/schemas/test/en/test_confirmation_question_within_repeating_section.json index e6bd5e1f9f..6f754192de 100644 --- a/schemas/test/en/test_confirmation_question_within_repeating_section.json +++ b/schemas/test/en/test_confirmation_question_within_repeating_section.json @@ -186,7 +186,9 @@ { "id": "default-section", "title": "Questions", - "summary": { "show_on_completion": true }, + "summary": { + "show_on_completion": true + }, "repeat": { "for_list": "people", "title": { @@ -281,7 +283,7 @@ ] } ], - "text": "What is {person_name_possessive} date of birth?" + "text": "What is {person_name_possessive} date of birth?" }, "type": "General" } @@ -289,22 +291,21 @@ { "type": "ConfirmationQuestion", "id": "confirm-dob-block", - "skip_conditions": [ - { - "when": [ + "skip_conditions": { + "when": { + "<=": [ { - "condition": "less than or equal to", - "date_comparison": { - "offset_by": { - "years": -16 - }, - "value": "now" - }, - "id": "date-of-birth-answer" - } + "date": [ + { + "source": "answers", + "identifier": "date-of-birth-answer" + } + ] + }, + { "date": ["now", { "years": -16 }] } ] } - ], + }, "question": { "answers": [ { @@ -413,21 +414,19 @@ }, "routing_rules": [ { - "goto": { - "block": "dob-block", - "when": [ + "block": "dob-block", + "when": { + "==": [ { - "id": "confirm-date-of-birth-answer", - "condition": "equals", - "value": "No, I need to change their date of birth" - } + "source": "answers", + "identifier": "confirm-date-of-birth-answer" + }, + "No, I need to change their date of birth" ] } }, { - "goto": { - "section": "End" - } + "section": "End" } ] }, @@ -460,19 +459,24 @@ ] } ], - "text": "Does {person_name} look after, or give any help or support to, anyone because they have long-term physical or mental health conditions or illnesses, or problems related to old age?" + "text": "Does {person_name} look after, or give any help or support to, anyone because they have long-term physical or mental health conditions or illnesses, or problems related to old age?" }, "answers": [ { "id": "carer-answer", - "q_code": "50", "label": "Carer", "mandatory": false, "type": "Radio", "default": "Yes", "options": [ - { "label": "Yes", "value": "Yes" }, - { "label": "No", "value": "No" } + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } ] } ] diff --git a/schemas/test/en/test_currency.json b/schemas/test/en/test_currency.json index c803c8da99..9f26654f51 100644 --- a/schemas/test/en/test_currency.json +++ b/schemas/test/en/test_currency.json @@ -8,8 +8,6 @@ "theme": "default", "description": "A questionnaire to test currency input type", "messages": { - "NUMBER_TOO_LARGE": "Number is too large", - "NUMBER_TOO_SMALL": "Number cannot be less than zero", "INVALID_DECIMAL": "Please enter a number to %(max)d decimal places" }, "metadata": [ @@ -52,10 +50,9 @@ "question": { "answers": [ { - "id": "answer", + "id": "answer-gbp", "label": "How much did you spend?", "mandatory": false, - "q_code": "0", "type": "Currency", "currency": "GBP", "decimal_places": 2, @@ -67,7 +64,6 @@ "id": "answer-usd", "label": "How much did you spend?", "mandatory": false, - "q_code": "0", "type": "Currency", "currency": "USD", "decimal_places": 2, @@ -79,7 +75,6 @@ "id": "answer-eur", "label": "How much did you spend?", "mandatory": false, - "q_code": "0", "type": "Currency", "currency": "EUR", "decimal_places": 2, @@ -91,16 +86,98 @@ "id": "answer-jpy", "label": "How much did you spend?", "mandatory": false, - "q_code": "0", "type": "Currency", "currency": "JPY", "maximum": { "value": 1000000 } + }, + { + "id": "answer-gbp-max-range", + "label": "How much did you spend? (Max range)", + "mandatory": false, + "type": "Currency", + "currency": "GBP" + } + ], + "id": "currency-question", + "title": "Currency Input Test Positve", + "type": "General" + } + }, + { + "type": "Question", + "id": "negative-block", + "question": { + "answers": [ + { + "id": "negative-answer-gbp", + "label": "How much did you spend?", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2, + "maximum": { + "value": 10000 + }, + "minimum": { + "value": -1000000 + } + }, + { + "id": "negative-answer-usd", + "label": "How much did you spend?", + "mandatory": false, + "type": "Currency", + "currency": "USD", + "decimal_places": 2, + "maximum": { + "value": 10000 + }, + "minimum": { + "value": -1000000 + } + }, + { + "id": "negative-answer-eur", + "label": "How much did you spend?", + "mandatory": false, + "type": "Currency", + "currency": "EUR", + "decimal_places": 2, + "maximum": { + "value": 10000 + }, + "minimum": { + "value": -1000000 + } + }, + { + "id": "negative-answer-jpy", + "label": "How much did you spend?", + "mandatory": false, + "type": "Currency", + "currency": "JPY", + "maximum": { + "value": 1000000 + }, + "minimum": { + "value": -1000000 + } + }, + { + "id": "answer-gbp-min-range", + "label": "How much did you spend? (Min range)", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "minimum": { + "value": -99999999999999 + } } ], - "id": "question", - "title": "Currency Input Test", + "id": "negative-currency-question", + "title": "Currency Input Test Including Negative Values", "type": "General" } } diff --git a/schemas/test/en/test_custom_page_titles.json b/schemas/test/en/test_custom_page_titles.json index 4246ac6519..1b9865a4a4 100644 --- a/schemas/test/en/test_custom_page_titles.json +++ b/schemas/test/en/test_custom_page_titles.json @@ -196,7 +196,7 @@ "id": "relationship-question", "type": "General", "title": { - "text": "Thinking of {first_person_name}, {second_person_name} is their â€Ļ", + "text": "Thinking of {first_person_name}, {second_person_name} is their â€Ļ", "placeholders": [ { "placeholder": "first_person_name", @@ -264,7 +264,7 @@ "mandatory": true, "type": "Relationship", "playback": { - "text": "{second_person_name} is {first_person_name_possessive} â€Ļ", + "text": "{second_person_name} is {first_person_name_possessive} â€Ļ", "placeholders": [ { "placeholder": "first_person_name_possessive", @@ -339,7 +339,7 @@ "label": "Husband or Wife", "value": "Husband or Wife", "title": { - "text": "Thinking of {first_person_name}, {second_person_name} is their husband or wife", + "text": "Thinking of {first_person_name}, {second_person_name} is their husband or wife", "placeholders": [ { "placeholder": "first_person_name", @@ -402,7 +402,7 @@ ] }, "playback": { - "text": "{second_person_name} is {first_person_name_possessive} husband or wife", + "text": "{second_person_name} is {first_person_name_possessive} husband or wife", "placeholders": [ { "placeholder": "first_person_name_possessive", @@ -477,7 +477,7 @@ "label": "Legally registered civil partner", "value": "Legally registered civil partner", "title": { - "text": "Thinking of {first_person_name}, {second_person_name} is their legally registered civil partner", + "text": "Thinking of {first_person_name}, {second_person_name} is their legally registered civil partner", "placeholders": [ { "placeholder": "first_person_name", @@ -540,7 +540,7 @@ ] }, "playback": { - "text": "{second_person_name} is {first_person_name_possessive} legally registered civil partner", + "text": "{second_person_name} is {first_person_name_possessive} legally registered civil partner", "placeholders": [ { "placeholder": "first_person_name_possessive", @@ -615,7 +615,7 @@ "label": "Son or daughter", "value": "Son or daughter", "title": { - "text": "Thinking of {first_person_name}, {second_person_name} is their son or daughter", + "text": "Thinking of {first_person_name}, {second_person_name} is their son or daughter", "placeholders": [ { "placeholder": "first_person_name", @@ -678,7 +678,7 @@ ] }, "playback": { - "text": "{second_person_name} is {first_person_name_possessive} son or daughter", + "text": "{second_person_name} is {first_person_name_possessive} son or daughter", "placeholders": [ { "placeholder": "first_person_name_possessive", @@ -753,7 +753,7 @@ "label": "Brother or sister", "value": "Brother or sister", "title": { - "text": "Thinking of {first_person_name}, {second_person_name} is their brother or sister", + "text": "Thinking of {first_person_name}, {second_person_name} is their brother or sister", "placeholders": [ { "placeholder": "first_person_name", @@ -816,7 +816,7 @@ ] }, "playback": { - "text": "{second_person_name} is {first_person_name_possessive} brother or sister", + "text": "{second_person_name} is {first_person_name_possessive} brother or sister", "placeholders": [ { "placeholder": "first_person_name_possessive", @@ -892,17 +892,18 @@ } ] }, - "skip_conditions": [ - { - "when": [ + "skip_conditions": { + "when": { + "<": [ { - "list": "household", - "condition": "less than", - "value": 2 - } + "source": "list", + "identifier": "household", + "selector": "count" + }, + 2 ] } - ] + } } ] } @@ -1031,7 +1032,7 @@ ] } ], - "text": "Are you {person_name}?" + "text": "Are you {person_name}?" }, "type": "General" }, diff --git a/schemas/test/en/test_date_range.json b/schemas/test/en/test_date_range.json index 63f377a731..ccfe1d9214 100644 --- a/schemas/test/en/test_date_range.json +++ b/schemas/test/en/test_date_range.json @@ -44,14 +44,12 @@ "id": "date-range-from-answer", "label": "Period from", "mandatory": true, - "q_code": "11", "type": "Date" }, { "id": "date-range-to-answer", "label": "Period to", "mandatory": true, - "q_code": "12", "type": "Date" } ], diff --git a/schemas/test/en/test_date_validation_range.json b/schemas/test/en/test_date_validation_range.json index aaabcc1c55..97658510fe 100644 --- a/schemas/test/en/test_date_validation_range.json +++ b/schemas/test/en/test_date_validation_range.json @@ -47,7 +47,7 @@ "guidance": { "contents": [ { - "list": ["Enter a date range between 23 days and 1 month and 20 days apart"] + "description": "Enter a date range between 23 days and 1 month and 20 days apart" } ] }, diff --git a/schemas/test/en/test_dates.json b/schemas/test/en/test_dates.json index 743c554599..90eadcffdf 100644 --- a/schemas/test/en/test_dates.json +++ b/schemas/test/en/test_dates.json @@ -46,14 +46,12 @@ "id": "date-range-from-answer", "label": "Period from", "mandatory": true, - "q_code": "11", "type": "Date" }, { "id": "date-range-to-answer", "label": "Period to", "mandatory": true, - "q_code": "12", "type": "Date" } ], @@ -70,7 +68,6 @@ { "id": "month-year-answer", "mandatory": true, - "q_code": "11", "type": "MonthYearDate" } ], @@ -88,7 +85,6 @@ "id": "single-date-answer", "label": "Date", "mandatory": true, - "q_code": "11", "type": "Date" } ], @@ -106,7 +102,6 @@ "id": "non-mandatory-date-answer", "label": "Date", "mandatory": false, - "q_code": "17", "type": "Date" } ], @@ -124,7 +119,6 @@ "id": "year-date-answer", "label": "Date", "mandatory": false, - "q_code": "18", "type": "YearDate" } ], diff --git a/schemas/test/en/test_default_with_skip.json b/schemas/test/en/test_default_with_skip.json index 473de10588..08963ca5bf 100644 --- a/schemas/test/en/test_default_with_skip.json +++ b/schemas/test/en/test_default_with_skip.json @@ -70,17 +70,17 @@ "title": "Question Two", "type": "General" }, - "skip_conditions": [ - { - "when": [ + "skip_conditions": { + "when": { + "!=": [ { - "condition": "not equals", - "id": "answer-one", - "value": 1 - } + "source": "answers", + "identifier": "answer-one" + }, + 1 ] } - ] + } }, { "type": "Question", @@ -98,17 +98,17 @@ "title": "Question Three", "type": "General" }, - "skip_conditions": [ - { - "when": [ + "skip_conditions": { + "when": { + "==": [ { - "condition": "equals", - "id": "answer-two", - "value": 1 - } + "source": "answers", + "identifier": "answer-two" + }, + 1 ] } - ] + } } ], "id": "group" diff --git a/schemas/test/en/test_dob_date.json b/schemas/test/en/test_dob_date.json index 0a7005e378..70581d7670 100644 --- a/schemas/test/en/test_dob_date.json +++ b/schemas/test/en/test_dob_date.json @@ -58,43 +58,43 @@ }, "routing_rules": [ { - "goto": { - "block": "over-sixteen", - "when": [ + "block": "over-sixteen", + "when": { + "<": [ { - "id": "date-of-birth-answer", - "condition": "less than", - "date_comparison": { - "value": "now", - "offset_by": { - "years": -16 + "date": [ + { + "identifier": "date-of-birth-answer", + "source": "answers" } - } + ] + }, + { + "date": ["now", { "years": -16 }] } ] } }, { - "goto": { - "block": "under-sixteen", - "when": [ + "block": "under-sixteen", + "when": { + ">=": [ { - "id": "date-of-birth-answer", - "condition": "greater than or equal to", - "date_comparison": { - "value": "now", - "offset_by": { - "years": -16 + "date": [ + { + "identifier": "date-of-birth-answer", + "source": "answers" } - } + ] + }, + { + "date": ["now", { "years": -16 }] } ] } }, { - "goto": { - "block": "dob-age" - } + "block": "dob-age" } ] }, @@ -121,21 +121,19 @@ }, "routing_rules": [ { - "goto": { - "block": "over-sixteen", - "when": [ + "block": "over-sixteen", + "when": { + ">=": [ { - "id": "dob-age-answer", - "condition": "greater than or equal to", - "value": 16 - } + "source": "answers", + "identifier": "dob-age-answer" + }, + 16 ] } }, { - "goto": { - "block": "under-sixteen" - } + "block": "under-sixteen" } ] }, @@ -182,31 +180,29 @@ } ] }, - "skip_conditions": [ - { - "when": [ + "skip_conditions": { + "when": { + "or": [ { - "id": "dob-age-answer", - "condition": "greater than or equal to", - "value": 16 - } - ] - }, - { - "when": [ + ">=": [ + { + "source": "answers", + "identifier": "dob-age-answer" + }, + 16 + ] + }, { - "id": "date-of-birth-answer", - "condition": "less than", - "date_comparison": { - "value": "now", - "offset_by": { - "years": -16 + "<": [ + { "date": [{ "identifier": "date-of-birth-answer", "source": "answers" }] }, + { + "date": ["now", { "years": -16 }] } - } + ] } ] } - ] + } } ], "id": "test" diff --git a/schemas/test/en/test_durations.json b/schemas/test/en/test_durations.json index a719c970d6..fb8c66752a 100644 --- a/schemas/test/en/test_durations.json +++ b/schemas/test/en/test_durations.json @@ -45,7 +45,6 @@ "label": "Years and Months", "mandatory": false, "units": ["years", "months"], - "q_code": "11", "type": "Duration" }, { @@ -53,7 +52,6 @@ "label": "Mandatory Years and Months", "mandatory": true, "units": ["years", "months"], - "q_code": "12", "type": "Duration" }, { @@ -61,7 +59,6 @@ "label": "Years", "mandatory": false, "units": ["years"], - "q_code": "13", "type": "Duration" }, { @@ -69,7 +66,6 @@ "label": "Mandatory Years", "mandatory": true, "units": ["years"], - "q_code": "14", "type": "Duration" }, { @@ -77,7 +73,6 @@ "label": "Months", "mandatory": false, "units": ["months"], - "q_code": "14", "type": "Duration" }, { @@ -85,7 +80,6 @@ "label": "Mandatory Months", "mandatory": true, "units": ["months"], - "q_code": "15", "type": "Duration" } ], diff --git a/schemas/test/en/test_dynamic_answer_options_function_driven.json b/schemas/test/en/test_dynamic_answer_options_function_driven.json index 2c8167dd26..aebd35f764 100644 --- a/schemas/test/en/test_dynamic_answer_options_function_driven.json +++ b/schemas/test/en/test_dynamic_answer_options_function_driven.json @@ -57,7 +57,7 @@ "question": { "id": "dynamic-checkbox-question", "title": { - "text": "In the week of {date}, which days did you work?", + "text": "In the week of {date}, which days did you work?", "placeholders": [ { "placeholder": "date", @@ -125,7 +125,7 @@ "question": { "id": "dynamic-radio-question", "title": { - "text": "In the week of {date}, which day did you work the most?", + "text": "In the week of {date}, which day did you work the most?", "placeholders": [ { "placeholder": "date", @@ -193,7 +193,7 @@ "question": { "id": "dynamic-dropdown-question", "title": { - "text": "In the week of {date}, which day did you work the least?", + "text": "In the week of {date}, which day did you work the least?", "placeholders": [ { "placeholder": "date", @@ -262,7 +262,7 @@ "question": { "id": "dynamic-mutually-exclusive-question", "title": { - "text": "In the week of {date}, which days did you book annual leave?", + "text": "In the week of {date}, which days did you book annual leave?", "placeholders": [ { "placeholder": "date", diff --git a/schemas/test/en/test_dynamic_answer_options_function_driven_with_static_options.json b/schemas/test/en/test_dynamic_answer_options_function_driven_with_static_options.json index 77a61cbe3a..94117b7672 100644 --- a/schemas/test/en/test_dynamic_answer_options_function_driven_with_static_options.json +++ b/schemas/test/en/test_dynamic_answer_options_function_driven_with_static_options.json @@ -57,7 +57,7 @@ "question": { "id": "dynamic-checkbox-question", "title": { - "text": "In the week of {date}, which days did you work?", + "text": "In the week of {date}, which days did you work?", "placeholders": [ { "placeholder": "date", @@ -131,7 +131,7 @@ "question": { "id": "dynamic-radio-question", "title": { - "text": "In the week of {date}, which day did you work the most?", + "text": "In the week of {date}, which day did you work the most?", "placeholders": [ { "placeholder": "date", @@ -205,7 +205,7 @@ "question": { "id": "dynamic-dropdown-question", "title": { - "text": "In the week of {date}, which day did you work the least?", + "text": "In the week of {date}, which day did you work the least?", "placeholders": [ { "placeholder": "date", @@ -280,7 +280,7 @@ "question": { "id": "dynamic-mutually-exclusive-question", "title": { - "text": "In the week of {date}, which days did you book annual leave?", + "text": "In the week of {date}, which days did you book annual leave?", "placeholders": [ { "placeholder": "date", diff --git a/schemas/test/en/test_dynamic_answer_options_function_driven_with_static_options_mandatory.json b/schemas/test/en/test_dynamic_answer_options_function_driven_with_static_options_mandatory.json index fe27b21d53..45962f6437 100644 --- a/schemas/test/en/test_dynamic_answer_options_function_driven_with_static_options_mandatory.json +++ b/schemas/test/en/test_dynamic_answer_options_function_driven_with_static_options_mandatory.json @@ -57,7 +57,7 @@ "question": { "id": "dynamic-checkbox-question", "title": { - "text": "In the week of {date}, which days did you work?", + "text": "In the week of {date}, which days did you work?", "placeholders": [ { "placeholder": "date", @@ -131,7 +131,7 @@ "question": { "id": "dynamic-radio-question", "title": { - "text": "In the week of {date}, which day did you work the most?", + "text": "In the week of {date}, which day did you work the most?", "placeholders": [ { "placeholder": "date", @@ -205,7 +205,7 @@ "question": { "id": "dynamic-dropdown-question", "title": { - "text": "In the week of {date}, which day did you work the least?", + "text": "In the week of {date}, which day did you work the least?", "placeholders": [ { "placeholder": "date", @@ -280,7 +280,7 @@ "question": { "id": "dynamic-mutually-exclusive-question", "title": { - "text": "In the week of {date}, which days did you book annual leave?", + "text": "In the week of {date}, which days did you book annual leave?", "placeholders": [ { "placeholder": "date", diff --git a/schemas/test/en/test_dynamic_answers_list_source.json b/schemas/test/en/test_dynamic_answers_list_source.json new file mode 100644 index 0000000000..b77837d41d --- /dev/null +++ b/schemas/test/en/test_dynamic_answers_list_source.json @@ -0,0 +1,588 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Dynamic Answers List Source", + "theme": "default", + "description": "A questionnaire to demo dynamic answers list source.", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Hub", + "options": { "required_completed_sections": ["list-collector-section"] } + }, + "sections": [ + { + "id": "list-collector-section", + "title": "Supermarket Shopping Section", + "summary": { + "show_on_completion": true, + "items": [ + { + "type": "List", + "for_list": "supermarkets", + "title": "Household members", + "add_link_text": "Add another supermarket", + "empty_list_text": "There are no supermarkets" + } + ], + "show_non_item_answers": true + }, + "groups": [ + { + "id": "list-collector-group", + "blocks": [ + { + "type": "ListCollectorDrivingQuestion", + "id": "any-supermarket", + "for_list": "supermarkets", + "question": { + "type": "General", + "id": "any-supermarket-question", + "title": "Do you need to add any supermarkets?", + "answers": [ + { + "type": "Radio", + "id": "any-supermarket-answer", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock", + "params": { + "block_id": "add-supermarket", + "list_name": "supermarkets" + } + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "routing_rules": [ + { + "section": "End", + "when": { + "==": [ + { + "source": "answers", + "identifier": "any-supermarket-answer" + }, + "No" + ] + } + }, + { + "block": "list-collector" + } + ] + }, + { + "id": "list-collector", + "type": "ListCollector", + "for_list": "supermarkets", + "question": { + "id": "confirmation-question", + "type": "General", + "title": "Do you need to add any more supermarkets?", + "answers": [ + { + "id": "list-collector-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "add_block": { + "id": "add-supermarket", + "type": "ListAddQuestion", + "cancel_text": "Don’t need to add any other supermarkets?", + "question": { + "guidance": { + "contents": [ + { + "description": "Maximum spending value will be used for each supermarket’s max spending validation and placeholders." + } + ] + }, + "id": "add-question", + "type": "General", + "title": "Which supermarkets do you use for your weekly shopping?", + "answers": [ + { + "id": "supermarket-name", + "label": "Supermarket", + "mandatory": true, + "type": "TextField" + }, + { + "id": "set-maximum", + "description": "Maximum amount of spending at this supermarket, should be between 1001 and 10000", + "label": "Maximum Spending", + "mandatory": true, + "type": "Number", + "decimal_places": 2, + "minimum": { + "value": 1001 + }, + "maximum": { + "value": 10000 + } + } + ] + } + }, + "edit_block": { + "id": "edit-supermarket", + "type": "ListEditQuestion", + "cancel_text": "Don’t need to change anything?", + "question": { + "guidance": { + "contents": [ + { + "description": "Maximum spending value will be used for each supermarket’s max spending validation and placeholders." + } + ] + }, + "id": "edit-question", + "type": "General", + "title": "What is the name of the supermarket?", + "answers": [ + { + "id": "supermarket-name", + "label": "Supermarket", + "mandatory": true, + "type": "TextField" + }, + { + "id": "set-maximum", + "description": "Maximum amount of spending at this supermarket", + "label": "Maximum amount of spending", + "mandatory": true, + "type": "Number", + "decimal_places": 2, + "minimum": { + "value": 1001 + }, + "maximum": { + "value": 10000 + } + } + ] + } + }, + "remove_block": { + "id": "remove-supermarket", + "type": "ListRemoveQuestion", + "cancel_text": "Don’t need to remove this supermarket?", + "question": { + "id": "remove-question", + "type": "General", + "title": "Are you sure you want to remove this supermarket?", + "warning": "All of the information about this supermarket will be deleted", + "answers": [ + { + "id": "remove-confirmation", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RemoveListItemAndAnswers" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + "summary": { + "title": "Supermarkets", + "item_title": { + "text": "{supermarket_name}", + "placeholders": [ + { + "placeholder": "supermarket_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "supermarket-name" + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + } + } + }, + { + "type": "Question", + "id": "dynamic-answer", + "skip_conditions": { + "when": { + "==": [ + { + "count": [ + { + "source": "list", + "identifier": "supermarkets" + } + ] + }, + 0 + ] + } + }, + "question": { + "dynamic_answers": { + "values": { + "source": "list", + "identifier": "supermarkets" + }, + "answers": [ + { + "label": { + "text": "Percentage of shopping at {transformed_value}", + "placeholders": [ + { + "placeholder": "transformed_value", + "value": { + "source": "answers", + "identifier": "supermarket-name" + } + } + ] + }, + "id": "percentage-of-shopping", + "mandatory": false, + "type": "Percentage", + "maximum": { + "value": 100 + }, + "decimal_places": 0 + }, + { + "id": "days-a-week", + "label": { + "text": "How many days a week you shop at {transformed_value}", + "placeholders": [ + { + "placeholder": "transformed_value", + "value": { + "source": "answers", + "identifier": "supermarket-name" + } + } + ] + }, + "mandatory": false, + "type": "Number", + "decimal_places": 0, + "minimum": { + "value": 1 + }, + "maximum": { + "value": 7 + } + } + ] + }, + "answers": [ + { + "id": "based-checkbox-answer", + "label": "Are supermarkets UK or non UK based?", + "instruction": "Select any answers that apply", + "mandatory": false, + "options": [ + { + "label": "UK based supermarkets", + "value": "UK based supermarkets" + }, + { + "label": "Non UK based supermarkets", + "value": "Non UK based supermarkets" + } + ], + "type": "Checkbox" + } + ], + "id": "dynamic-answer-question", + "title": "What percent of your shopping do you do at each of the following supermarket?", + "type": "General" + } + }, + { + "type": "Question", + "id": "minimum-spending", + "question": { + "guidance": { + "contents": [ + { + "description": "This value will be used for all supermarkets minimum spending validation and placeholders." + } + ] + }, + "answers": [ + { + "id": "set-minimum", + "label": "Minimum Spending", + "description": "Minium amount of spending at all supermarkets", + "mandatory": true, + "type": "Number", + "decimal_places": 2, + "minimum": { + "value": 0 + }, + "maximum": { + "value": 1000 + } + } + ], + "id": "minimum-spending-question", + "title": "What is your minimum amount of spending?", + "type": "General" + } + }, + { + "type": "Question", + "id": "dynamic-answer-only", + "skip_conditions": { + "when": { + "==": [ + { + "count": [ + { + "source": "list", + "identifier": "supermarkets" + } + ] + }, + 0 + ] + } + }, + "question": { + "guidance": { + "contents": [ + { + "description": "Answers are validated against values piped from previous questions, maximum from repeated question for each supermarket, minimum from non-repeated question. Answer label’s placeholders are resolved from these as well." + } + ] + }, + "dynamic_answers": { + "values": { + "source": "list", + "identifier": "supermarkets" + }, + "answers": [ + { + "label": { + "text": "How much do you spend at {transformed_value}", + "placeholders": [ + { + "placeholder": "transformed_value", + "value": { + "source": "answers", + "identifier": "supermarket-name" + } + } + ] + }, + "id": "amount-of-shopping", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2, + "description": { + "text": "The answers must be less than or equal {max_value} and greater than or equal {min_value}", + "placeholders": [ + { + "placeholder": "min_value", + "value": { + "source": "answers", + "identifier": "set-minimum" + } + }, + { + "placeholder": "max_value", + "value": { + "source": "answers", + "identifier": "set-maximum" + } + } + ] + }, + "maximum": { + "value": { + "source": "answers", + "identifier": "set-maximum" + } + }, + "minimum": { + "value": { + "source": "answers", + "identifier": "set-minimum" + } + } + } + ] + }, + "id": "dynamic-answer-only-question", + "title": "How much do you spend at each of the following supermarket?", + "type": "General" + } + } + ] + } + ] + }, + { + "id": "dynamic-answers-section", + "title": "Online Shopping Section", + "enabled": { + "when": { + "==": [ + "Yes", + { + "source": "answers", + "identifier": "any-supermarket-answer" + } + ] + } + }, + "summary": { + "show_on_completion": true + }, + "groups": [ + { + "id": "dynamic-answers-group", + "blocks": [ + { + "type": "Question", + "id": "dynamic-answer-separate-section", + "skip_conditions": { + "when": { + "==": [ + { + "count": [ + { + "source": "list", + "identifier": "supermarkets" + } + ] + }, + 0 + ] + } + }, + "question": { + "dynamic_answers": { + "values": { + "source": "list", + "identifier": "supermarkets" + }, + "answers": [ + { + "label": { + "text": "Percentage of online shopping at {transformed_value}", + "placeholders": [ + { + "placeholder": "transformed_value", + "value": { + "source": "answers", + "identifier": "supermarket-name" + } + } + ] + }, + "id": "percentage-of-online-shopping", + "mandatory": false, + "type": "Percentage", + "maximum": { + "value": 100 + }, + "decimal_places": 0 + }, + { + "id": "online-days-a-week", + "label": { + "text": "How many days a week do you shop online at {transformed_value}", + "placeholders": [ + { + "placeholder": "transformed_value", + "value": { + "source": "answers", + "identifier": "supermarket-name" + } + } + ] + }, + "mandatory": false, + "type": "Number", + "decimal_places": 0, + "minimum": { + "value": 1 + }, + "maximum": { + "value": 7 + } + } + ] + }, + "id": "dynamic-answer-online-question", + "title": "What percent of your online shopping do you do at each of the following supermarket?", + "type": "General" + } + } + ] + } + ] + } + ] +} diff --git a/schemas/test/en/test_feedback_email_confirmation.json b/schemas/test/en/test_feedback_email_confirmation.json index 85c2aff16d..0a18b7b9e7 100644 --- a/schemas/test/en/test_feedback_email_confirmation.json +++ b/schemas/test/en/test_feedback_email_confirmation.json @@ -5,7 +5,7 @@ "data_version": "0.0.3", "survey_id": "0", "title": "Feedback test schema", - "theme": "census", + "theme": "default", "description": "A questionnaire to test feedback", "metadata": [ { @@ -17,7 +17,7 @@ "type": "string" }, { - "name": "display_address", + "name": "ru_name", "type": "string" } ], diff --git a/schemas/test/en/test_grand_calculated_summary.json b/schemas/test/en/test_grand_calculated_summary.json new file mode 100644 index 0000000000..e2271d03b9 --- /dev/null +++ b/schemas/test/en/test_grand_calculated_summary.json @@ -0,0 +1,294 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Simple Grand Calculated Summary demo", + "theme": "default", + "description": "A schema to showcase Grand Calculated Summary.", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Linear", + "options": { + "summary": { + "collapsible": false + } + } + }, + "sections": [ + { + "id": "section-1", + "title": "Commuting", + "groups": [ + { + "id": "group", + "title": "Commuting", + "blocks": [ + { + "type": "Question", + "id": "first-number-block", + "question": { + "id": "first-number-question", + "title": "How much do you walk per week?", + "type": "General", + "answers": [ + { + "id": "q1-a1", + "label": "Weekly distance travelled on foot", + "mandatory": true, + "type": "Unit", + "unit_length": "short", + "unit": "length-mile", + "decimal_places": 2 + }, + { + "id": "q1-a2", + "label": "Number of walks per week", + "mandatory": true, + "type": "Number" + } + ] + } + }, + { + "type": "Question", + "id": "second-number-block", + "question": { + "id": "second-number-question", + "title": "How much do you drive per week?", + "type": "General", + "answers": [ + { + "id": "q2-a1", + "label": "Weekly distance travelled by car", + "mandatory": true, + "type": "Unit", + "unit_length": "short", + "unit": "length-mile", + "decimal_places": 2 + }, + { + "id": "q2-a2", + "label": "Number of car journeys per week", + "mandatory": true, + "type": "Number" + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "distance-calculated-summary-1", + "title": "We calculate the total of distance travelled by foot and car to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "answers", + "identifier": "q1-a1" + }, + { + "source": "answers", + "identifier": "q2-a1" + } + ] + }, + "title": "Calculated distance on foot and driving" + } + }, + { + "type": "CalculatedSummary", + "id": "number-calculated-summary-1", + "title": "We calculate the total number of journeys on foot and in a car to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "answers", + "identifier": "q1-a2" + }, + { + "source": "answers", + "identifier": "q2-a2" + } + ] + }, + "title": "Calculated journeys on foot and driving" + } + } + ] + } + ] + }, + { + "id": "section-2", + "title": "Alternative Transport", + "groups": [ + { + "id": "transport-group", + "title": "Alternative Transport", + "blocks": [ + { + "type": "Question", + "id": "third-number-block", + "question": { + "id": "third-number-question", + "title": "How much do you cycle per week?", + "type": "General", + "answers": [ + { + "id": "q3-a1", + "label": "Weekly distance travelled by bike", + "mandatory": true, + "type": "Unit", + "unit_length": "short", + "unit": "length-mile", + "decimal_places": 2 + }, + { + "id": "q3-a2", + "label": "Number of bicycle journeys per week", + "mandatory": true, + "type": "Number" + } + ] + } + }, + { + "type": "Question", + "id": "fourth-number-block", + "question": { + "id": "fourth-number-question", + "title": "How much do you voi per week?", + "type": "General", + "answers": [ + { + "id": "q4-a1", + "label": "Weekly distance travelled on a Voi", + "mandatory": true, + "type": "Unit", + "unit_length": "short", + "unit": "length-mile", + "decimal_places": 2 + }, + { + "id": "q4-a2", + "label": "Number of scooter trips per week", + "mandatory": true, + "type": "Number" + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "distance-calculated-summary-2", + "title": "We calculate the total of distance travelled by bike and voi to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "answers", + "identifier": "q3-a1" + }, + { + "source": "answers", + "identifier": "q4-a1" + } + ] + }, + "title": "Calculated weekly distance on bike and scooter" + } + }, + { + "type": "CalculatedSummary", + "id": "number-calculated-summary-2", + "title": "We calculate the total number of journeys on bike and on a voi to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "answers", + "identifier": "q3-a2" + }, + { + "source": "answers", + "identifier": "q4-a2" + } + ] + }, + "title": "Calculated journeys on bike and scooter" + } + } + ] + } + ] + }, + { + "id": "section-3", + "title": "Grand calculated summaries", + "groups": [ + { + "id": "summary-group", + "title": "Grand calculated summary group", + "blocks": [ + { + "type": "GrandCalculatedSummary", + "id": "distance-grand-calculated-summary", + "title": "We calculate the grand total weekly distance travelled to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "calculated_summary", + "identifier": "distance-calculated-summary-1" + }, + { + "source": "calculated_summary", + "identifier": "distance-calculated-summary-2" + } + ] + }, + "title": "Grand calculated summary of distance travelled" + } + }, + { + "type": "GrandCalculatedSummary", + "id": "number-grand-calculated-summary", + "title": "We calculate the grand total journeys per week to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "calculated_summary", + "identifier": "number-calculated-summary-1" + }, + { + "source": "calculated_summary", + "identifier": "number-calculated-summary-2" + } + ] + }, + "title": "Grand calculated summary of journeys" + } + } + ] + } + ] + } + ] +} diff --git a/schemas/test/en/test_grand_calculated_summary_cross_section_dependencies.json b/schemas/test/en/test_grand_calculated_summary_cross_section_dependencies.json new file mode 100644 index 0000000000..61512e3003 --- /dev/null +++ b/schemas/test/en/test_grand_calculated_summary_cross_section_dependencies.json @@ -0,0 +1,368 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Grand Calculated Summary Cross Section Dependencies", + "theme": "default", + "description": "A questionnaire to demo resolution of grand calculated summary values across sections", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Hub", + "options": {} + }, + "sections": [ + { + "id": "questions-section", + "title": "Household bills", + "summary": { + "show_on_completion": true + }, + "groups": [ + { + "id": "radio", + "title": "Questions", + "blocks": [ + { + "type": "Question", + "id": "skip-first-block", + "question": { + "type": "General", + "id": "skip-question-1", + "title": "Are you a student?", + "guidance": { + "contents": [ + { + "description": "If you answer yes, then the question about council tax will be skipped and not included in total monthly expenditure." + } + ] + }, + "answers": [ + { + "type": "Radio", + "id": "skip-answer-1", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + { + "skip_conditions": { + "when": { + "==": [ + { + "identifier": "skip-answer-1", + "source": "answers" + }, + "Yes" + ] + } + }, + "type": "Question", + "id": "first-number-block-part-a", + "question": { + "id": "question-1-a", + "title": "How much do you pay monthly for council tax?", + "type": "General", + "answers": [ + { + "id": "first-number-answer-a", + "label": "Council tax (optional)", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "Question", + "id": "second-number-block", + "question": { + "id": "question-2", + "title": "How much are your monthly gas, water and electricity bills?", + "type": "General", + "answers": [ + { + "id": "second-number-answer-a", + "label": "Electricity Bill", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + }, + { + "id": "second-number-answer-b", + "label": "Gas Bill", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + }, + { + "id": "second-number-answer-c", + "label": "Water Bill", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "currency-section-1", + "title": "We calculate your total monthly expenditure on household bills to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "answers", + "identifier": "second-number-answer-a" + }, + { + "source": "answers", + "identifier": "second-number-answer-b" + }, + { + "source": "answers", + "identifier": "second-number-answer-c" + }, + { + "source": "answers", + "identifier": "first-number-answer-a" + } + ] + }, + "title": "Monthly expenditure on household bills" + } + } + ] + } + ] + }, + { + "id": "calculated-summary-section", + "title": "Other outgoing costs", + "summary": { + "show_on_completion": true + }, + "groups": [ + { + "id": "calculated-summary", + "blocks": [ + { + "type": "Question", + "id": "third-number-block", + "question": { + "id": "third-number-question", + "title": "How much do you spend on internet and television?", + "type": "General", + "guidance": { + "contents": [ + { + "description": "If you enter a value for the TV licence, it will unlock an additional question about premium channels." + } + ] + }, + "answers": [ + { + "id": "third-number-answer-part-a", + "label": "Internet bill", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + }, + { + "id": "third-number-answer-part-b", + "label": "TV licence (optional)", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "skip_conditions": { + "when": { + "==": [ + { + "identifier": "third-number-answer-part-b", + "source": "answers" + }, + null + ] + } + }, + "type": "Question", + "id": "fourth-number-block", + "question": { + "id": "fourth-number-question", + "title": "How much do you spend per month on premium television channels?", + "type": "General", + "answers": [ + { + "id": "fourth-number-answer", + "label": "TV channel subscription fees", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "Question", + "id": "skip-calculated-summary", + "question": { + "type": "General", + "id": "skip-question-2", + "title": "Skip the calculated summary of other outgoing costs so it isn’t included in the grand calculated summary?", + "answers": [ + { + "type": "Radio", + "id": "skip-answer-2", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + { + "skip_conditions": { + "when": { + "==": [ + { + "identifier": "skip-answer-2", + "source": "answers" + }, + "Yes" + ] + } + }, + "type": "CalculatedSummary", + "id": "currency-question-3", + "title": "We calculate the total monthly spending on internet and TV to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "answers", + "identifier": "third-number-answer-part-a" + }, + { + "source": "answers", + "identifier": "third-number-answer-part-b" + }, + { + "source": "answers", + "identifier": "fourth-number-answer" + } + ] + }, + "title": "Total monthly spending on internet and TV" + } + }, + { + "id": "tv-choice-block", + "type": "Question", + "question": { + "id": "tv-choice-question", + "title": "Do you prefer to watch films on a television or computer?", + "type": "General", + "answers": [ + { + "id": "tv-choice-answer", + "label": "Preferred platform", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Television", + "value": "Television" + }, + { + "label": "Computer", + "value": "Computer" + } + ] + } + ] + } + } + ] + } + ] + }, + { + "id": "grand-calculated-summary-section", + "title": "Grand Calculated Summary", + "groups": [ + { + "id": "grand-calculated-summary", + "blocks": [ + { + "type": "GrandCalculatedSummary", + "id": "currency-all", + "title": "The grand calculated summary is calculated to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "calculated_summary", + "identifier": "currency-section-1" + }, + { + "source": "calculated_summary", + "identifier": "currency-question-3" + } + ] + }, + "title": "Grand total monthly expenditure" + } + } + ] + } + ] + } + ] +} diff --git a/schemas/test/en/test_grand_calculated_summary_inside_repeating_section.json b/schemas/test/en/test_grand_calculated_summary_inside_repeating_section.json new file mode 100644 index 0000000000..41930c4a5e --- /dev/null +++ b/schemas/test/en/test_grand_calculated_summary_inside_repeating_section.json @@ -0,0 +1,1109 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Grand Calculated Summary Inside Repeating Section", + "theme": "default", + "description": "A schema to showcase a grand calculated summary inside a repeating section", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Hub", + "options": {} + }, + "sections": [ + { + "id": "base-costs-section", + "title": "Vehicle Costs", + "summary": { + "show_on_completion": true, + "items": [ + { + "type": "List", + "for_list": "costs", + "title": "Base Costs", + "add_link_text": "Add another base cost", + "empty_list_text": "There are no base costs" + } + ], + "show_non_item_answers": true + }, + "groups": [ + { + "id": "base-costs-group", + "blocks": [ + { + "type": "ListCollectorDrivingQuestion", + "id": "any-cost", + "for_list": "costs", + "question": { + "type": "General", + "id": "any-cost-question", + "title": "Do you have any outgoing costs for owning a vehicle?", + "answers": [ + { + "type": "Radio", + "id": "any-cost-answer", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock", + "params": { + "block_id": "add-cost", + "list_name": "costs" + } + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "routing_rules": [ + { + "block": "finance-cost", + "when": { + "==": [ + { + "source": "answers", + "identifier": "any-cost-answer" + }, + "No" + ] + } + }, + { + "block": "list-collector-cost" + } + ] + }, + { + "id": "list-collector-cost", + "type": "ListCollector", + "for_list": "costs", + "question": { + "id": "confirmation-cost-question", + "type": "General", + "title": "Do you need to add other outgoing costs?", + "answers": [ + { + "id": "list-collector-cost-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "add_block": { + "id": "add-cost", + "type": "ListAddQuestion", + "cancel_text": "Don’t need to add any other outgoing costs?", + "question": { + "id": "add-cost-question", + "type": "General", + "title": "What outgoing cost do you have?", + "answers": [ + { + "id": "cost-name", + "label": "Outgoing cost", + "mandatory": true, + "type": "Dropdown", + "options": [ + { + "label": "Road Tax", + "value": "Road Tax" + }, + { + "label": "Parking Permit", + "value": "Parking Permit" + } + ] + } + ] + } + }, + "edit_block": { + "id": "edit-cost", + "type": "ListEditQuestion", + "cancel_text": "Don’t need to change anything?", + "question": { + "id": "edit-cost-question", + "type": "General", + "title": "What outgoing cost do you have?", + "answers": [ + { + "id": "cost-name", + "label": "Outgoing cost", + "mandatory": true, + "type": "Dropdown", + "options": [ + { + "label": "Road Tax", + "value": "Road Tax" + }, + { + "label": "Parking Permit", + "value": "Parking Permit" + } + ] + } + ] + } + }, + "remove_block": { + "id": "remove-cost", + "type": "ListRemoveQuestion", + "cancel_text": "Don’t need to remove this outgoing cost?", + "question": { + "id": "remove-cost-question", + "type": "General", + "title": "Are you sure you want to remove this outgoing cost?", + "warning": "All of the information about this outgoing cost will be deleted", + "answers": [ + { + "id": "remove-cost-confirmation", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RemoveListItemAndAnswers" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + "repeating_blocks": [ + { + "id": "cost-repeating-block-1", + "type": "ListRepeatingQuestion", + "question": { + "id": "cost-repeating-block-1-question", + "type": "General", + "title": { + "text": "What is the base monthly rate for {cost_name} for a single vehicle?", + "placeholders": [ + { + "placeholder": "cost_name", + "value": { + "source": "answers", + "identifier": "cost-name" + } + } + ] + }, + "answers": [ + { + "id": "repeating-block-1-cost-base", + "label": { + "text": "Base {transformed_value} expenditure", + "placeholders": [ + { + "placeholder": "transformed_value", + "value": { + "source": "answers", + "identifier": "cost-name" + } + } + ] + }, + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + } + ], + "summary": { + "title": "cost", + "item_title": { + "text": "{cost_name}", + "placeholders": [ + { + "placeholder": "cost_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "cost-name" + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + } + } + }, + { + "type": "Question", + "id": "dynamic-cost-block", + "skip_conditions": { + "when": { + "==": [ + { + "count": [ + { + "source": "list", + "identifier": "costs" + } + ] + }, + 0 + ] + } + }, + "question": { + "id": "dynamic-answer-question", + "title": "How much extra do you normally spend per month for a single vehicle?", + "type": "General", + "dynamic_answers": { + "values": { + "source": "list", + "identifier": "costs" + }, + "answers": [ + { + "label": { + "text": "Extra {transformed_value} expenditure", + "placeholders": [ + { + "placeholder": "transformed_value", + "value": { + "source": "answers", + "identifier": "cost-name" + } + } + ] + }, + "id": "dynamic-answer-cost-extra", + "type": "Currency", + "mandatory": false, + "currency": "GBP", + "decimal_places": 2 + } + ] + } + } + }, + { + "id": "finance-cost", + "type": "Question", + "question": { + "id": "finance-cost-question", + "type": "General", + "title": "What is your monthly expenditure per vehicle on finance?", + "answers": [ + { + "id": "finance-cost-answer", + "label": "Vehicle monthly finance costs", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "calculated-summary-base-cost", + "title": "We calculate the total base cost for any owned vehicle to be %(total)s. Is this correct?", + "calculation": { + "title": "Vehicle base cost", + "operation": { + "+": [ + { + "source": "answers", + "identifier": "repeating-block-1-cost-base" + }, + { + "source": "answers", + "identifier": "dynamic-answer-cost-extra" + }, + { + "source": "answers", + "identifier": "finance-cost-answer" + } + ] + } + } + }, + { + "type": "Question", + "id": "base-cost-payment-breakdown", + "question": { + "type": "Calculated", + "id": "base-cost-payment-breakdown-question", + "title": { + "text": "How much of the {total} is paid by debit or credit card?", + "placeholders": [ + { + "placeholder": "total", + "transforms": [ + { + "transform": "format_currency", + "arguments": { + "number": { + "source": "calculated_summary", + "identifier": "calculated-summary-base-cost" + } + } + } + ] + } + ] + }, + "warning": "The sum of these answers must not exceed the total", + "calculations": [ + { + "calculation_type": "sum", + "value": { + "source": "calculated_summary", + "identifier": "calculated-summary-base-cost" + }, + "answers_to_calculate": ["base-credit", "base-debit"], + "conditions": ["less than", "equals"] + } + ], + "answers": [ + { + "id": "base-credit", + "label": "Credit card", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + }, + { + "id": "base-debit", + "label": "Debit card", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + } + ] + } + ] + }, + { + "id": "vehicles-section", + "title": "Vehicle Ownership", + "summary": { + "show_on_completion": true, + "items": [ + { + "type": "List", + "for_list": "vehicles", + "title": "Vehicles", + "add_link_text": "Add another vehicle", + "empty_list_text": "There are no vehicles" + } + ], + "show_non_item_answers": true + }, + "groups": [ + { + "id": "vehicles-group", + "blocks": [ + { + "type": "ListCollectorDrivingQuestion", + "id": "any-vehicle", + "for_list": "vehicles", + "question": { + "type": "General", + "id": "any-vehicle-question", + "title": "Do you own any vehicles?", + "answers": [ + { + "type": "Radio", + "id": "any-vehicle-answer", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock", + "params": { + "block_id": "add-vehicle", + "list_name": "vehicles" + } + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "routing_rules": [ + { + "section": "End", + "when": { + "==": [ + { + "source": "answers", + "identifier": "any-vehicle-answer" + }, + "No" + ] + } + }, + { + "block": "list-collector" + } + ] + }, + { + "id": "list-collector", + "type": "ListCollector", + "for_list": "vehicles", + "question": { + "id": "confirmation-question", + "type": "General", + "title": "Do you need to add more vehicles?", + "answers": [ + { + "id": "list-collector-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "add_block": { + "id": "add-vehicle", + "type": "ListAddQuestion", + "cancel_text": "Don’t need to add any other vehicles?", + "question": { + "id": "add-question", + "type": "General", + "title": "What vehicle do you own?", + "answers": [ + { + "id": "vehicle-name", + "label": "Vehicle", + "mandatory": true, + "type": "Dropdown", + "options": [ + { + "label": "Car", + "value": "Car" + }, + { + "label": "Motorbike", + "value": "Motorbike" + }, + { + "label": "Van", + "value": "Van" + } + ] + } + ] + } + }, + "edit_block": { + "id": "edit-vehicle", + "type": "ListEditQuestion", + "cancel_text": "Don’t need to change anything?", + "question": { + "id": "edit-question", + "type": "General", + "title": "What vehicle do you own?", + "answers": [ + { + "id": "vehicle-name", + "label": "Vehicle", + "mandatory": true, + "type": "Dropdown", + "options": [ + { + "label": "Car", + "value": "Car" + }, + { + "label": "Motorbike", + "value": "Motorbike" + }, + { + "label": "Van", + "value": "Van" + } + ] + } + ] + } + }, + "remove_block": { + "id": "remove-vehicle", + "type": "ListRemoveQuestion", + "cancel_text": "Don’t need to remove this vehicle?", + "question": { + "id": "remove-question", + "type": "General", + "title": "Are you sure you want to remove this vehicle?", + "warning": "All of the information about this vehicle will be deleted", + "answers": [ + { + "id": "remove-confirmation", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RemoveListItemAndAnswers" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + "summary": { + "title": "Vehicle", + "item_title": { + "text": "{vehicle_name}", + "placeholders": [ + { + "placeholder": "vehicle_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "vehicle-name" + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + } + } + } + ] + } + ] + }, + { + "id": "vehicle-details-section", + "title": "Vehicle Details", + "summary": { + "show_on_completion": true + }, + "repeat": { + "for_list": "vehicles", + "title": { + "text": "{vehicle_name} details", + "placeholders": [ + { + "placeholder": "vehicle_name", + "value": { + "source": "answers", + "identifier": "vehicle-name" + } + } + ] + } + }, + "groups": [ + { + "id": "vehicle-details-group", + "blocks": [ + { + "id": "vehicle-maintenance-block", + "type": "Question", + "question": { + "id": "vehicle-maintenance-question", + "type": "General", + "title": { + "text": "What is your monthly expenditure on maintenance for your {vehicle_name}?", + "placeholders": [ + { + "placeholder": "vehicle_name", + "value": { + "source": "answers", + "identifier": "vehicle-name" + } + } + ] + }, + "answers": [ + { + "id": "vehicle-maintenance-cost", + "label": { + "text": "{vehicle_name} maintenance costs", + "placeholders": [ + { + "placeholder": "vehicle_name", + "value": { + "source": "answers", + "identifier": "vehicle-name" + } + } + ] + }, + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "id": "vehicle-fuel-block", + "type": "Question", + "question": { + "id": "vehicle-fuel-question", + "type": "General", + "title": { + "text": "What is your monthly expenditure on fuel for your {vehicle_name}?", + "placeholders": [ + { + "placeholder": "vehicle_name", + "value": { + "source": "answers", + "identifier": "vehicle-name" + } + } + ] + }, + "answers": [ + { + "id": "vehicle-fuel-cost", + "label": { + "text": "{vehicle_name} fuel costs", + "placeholders": [ + { + "placeholder": "vehicle_name", + "value": { + "source": "answers", + "identifier": "vehicle-name" + } + } + ] + }, + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "calculated-summary-running-cost", + "title": { + "text": "We calculate the monthly running costs of your {vehicle_name} to be %(total)s. Is this correct?", + "placeholders": [ + { + "placeholder": "vehicle_name", + "value": { + "source": "answers", + "identifier": "vehicle-name" + } + } + ] + }, + "calculation": { + "title": { + "text": "Monthly {vehicle_name} costs", + "placeholders": [ + { + "placeholder": "vehicle_name", + "value": { + "source": "answers", + "identifier": "vehicle-name" + } + } + ] + }, + "operation": { + "+": [ + { + "source": "answers", + "identifier": "vehicle-maintenance-cost" + }, + { + "source": "answers", + "identifier": "vehicle-fuel-cost" + } + ] + } + } + }, + { + "type": "GrandCalculatedSummary", + "id": "grand-calculated-summary-vehicle", + "title": { + "text": "The total cost of owning and running your {vehicle_name} is calculated to be %(total)s. Is this correct?", + "placeholders": [ + { + "placeholder": "vehicle_name", + "value": { + "source": "answers", + "identifier": "vehicle-name" + } + } + ] + }, + "calculation": { + "operation": { + "+": [ + { + "source": "calculated_summary", + "identifier": "calculated-summary-base-cost" + }, + { + "source": "calculated_summary", + "identifier": "calculated-summary-running-cost" + } + ] + }, + "title": { + "text": "Grand total {vehicle_name} expenditure", + "placeholders": [ + { + "placeholder": "vehicle_name", + "value": { + "source": "answers", + "identifier": "vehicle-name" + } + } + ] + } + } + }, + { + "type": "Question", + "id": "gcs-breakdown-block", + "question": { + "id": "gcs-breakdown-question", + "guidance": { + "contents": [ + { + "description": "Currently this question is not revisited when the grand calculated summary changes. When grand calculated summary dependencies are implemented, this guidance should be removed, and this block should become incomplete upon the GCS changing." + } + ] + }, + "title": { + "text": "How do you pay for the monthly fees of {vehicle_cost}?", + "placeholders": [ + { + "placeholder": "vehicle_cost", + "transforms": [ + { + "transform": "format_currency", + "arguments": { + "number": { + "identifier": "grand-calculated-summary-vehicle", + "source": "grand_calculated_summary" + } + } + } + ] + } + ] + }, + "type": "Calculated", + "warning": "These answers must add up to the total owning and running cost", + "calculations": [ + { + "calculation_type": "sum", + "value": { + "identifier": "grand-calculated-summary-vehicle", + "source": "grand_calculated_summary" + }, + "answers_to_calculate": ["pay-debit", "pay-credit", "pay-other"], + "conditions": ["equals"] + } + ], + "answers": [ + { + "id": "pay-debit", + "label": "Amount paid by debit card", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + }, + { + "id": "pay-credit", + "label": "Amount paid by credit card", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + }, + { + "id": "pay-other", + "label": "Amount paid by other means", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "Interstitial", + "id": "gcs-piping", + "content": { + "title": { + "text": "You have provided the following information about monthly expenditure for your {vehicle_name}.", + "placeholders": [ + { + "placeholder": "vehicle_name", + "value": { + "source": "answers", + "identifier": "vehicle-name" + } + } + ] + }, + "contents": [ + { + "list": [ + { + "text": "Monthly maintenance cost: {total_maintenance}", + "placeholders": [ + { + "placeholder": "total_maintenance", + "transforms": [ + { + "transform": "format_currency", + "arguments": { + "number": { + "identifier": "vehicle-maintenance-cost", + "source": "answers" + } + } + } + ] + } + ] + }, + { + "text": "Monthly fuel cost: {total_fuel}", + "placeholders": [ + { + "placeholder": "total_fuel", + "transforms": [ + { + "transform": "format_currency", + "arguments": { + "number": { + "identifier": "vehicle-fuel-cost", + "source": "answers" + } + } + } + ] + } + ] + }, + { + "text": "Total base cost: {total_base}", + "placeholders": [ + { + "placeholder": "total_base", + "transforms": [ + { + "transform": "format_currency", + "arguments": { + "number": { + "identifier": "calculated-summary-base-cost", + "source": "calculated_summary" + } + } + } + ] + } + ] + }, + { + "text": "Total running cost: {total_running}", + "placeholders": [ + { + "placeholder": "total_running", + "transforms": [ + { + "transform": "format_currency", + "arguments": { + "number": { + "identifier": "calculated-summary-running-cost", + "source": "calculated_summary" + } + } + } + ] + } + ] + }, + { + "text": "Total owning and running cost: {total}", + "placeholders": [ + { + "placeholder": "total", + "transforms": [ + { + "transform": "format_currency", + "arguments": { + "number": { + "identifier": "grand-calculated-summary-vehicle", + "source": "grand_calculated_summary" + } + } + } + ] + } + ] + }, + { + "text": "Paid by debit card: {debit}", + "placeholders": [ + { + "placeholder": "debit", + "transforms": [ + { + "transform": "format_currency", + "arguments": { + "number": { + "identifier": "pay-debit", + "source": "answers" + } + } + } + ] + } + ] + }, + { + "text": "Paid by credit card: {credit}", + "placeholders": [ + { + "placeholder": "credit", + "transforms": [ + { + "transform": "format_currency", + "arguments": { + "number": { + "identifier": "pay-credit", + "source": "answers" + } + } + } + ] + } + ] + }, + { + "text": "Paid by other means: {other}", + "placeholders": [ + { + "placeholder": "other", + "transforms": [ + { + "transform": "format_currency", + "arguments": { + "number": { + "identifier": "pay-other", + "source": "answers" + } + } + } + ] + } + ] + } + ] + } + ] + } + } + ] + } + ] + } + ] +} diff --git a/schemas/test/en/test_grand_calculated_summary_overlapping_answers.json b/schemas/test/en/test_grand_calculated_summary_overlapping_answers.json new file mode 100644 index 0000000000..b21dbc9b6e --- /dev/null +++ b/schemas/test/en/test_grand_calculated_summary_overlapping_answers.json @@ -0,0 +1,357 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Grand Calculated Summary with overlapping answers", + "theme": "default", + "description": "A schema to showcase grand calculated summaries which include multiple calculated summaries using the same answers.", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Hub", + "options": { + "required_completed_sections": ["introduction-section"] + } + }, + "sections": [ + { + "id": "introduction-section", + "title": "Introduction", + "show_on_hub": false, + "groups": [ + { + "id": "introduction-group", + "title": "Introduction", + "blocks": [ + { + "type": "Introduction", + "id": "introduction-block", + "primary_content": [ + { + "id": "about", + "contents": [ + { + "title": "About", + "list": [ + "This survey tests that when you re-use answers between calculated summaries, the grand calculated summary still resolves to the correct value" + ] + }, + { + "title": "How to test this schema", + "list": [ + "Ensure that the grand calculated summary section does not show unless all dependent calculated summaries in section-1 have been confirmed.", + "Your answer to the third question, may unlock an additional calculated summary which re-use your answers to the first two questions", + "If you do not select to buy extra food, verify no additional calculated summary occurs, and that the grand calculated summary is correct", + "If you choose to buy any food items twice, verify that they are included twice in the grand calculated summary, one for each calculated summary", + "Verify that if you have the extra calculated summary, and change the cost of bread for example using either of the calculated summary change links which include it that you are routed to each calculated summary first, and only then the grand calculated summary" + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "id": "section-1", + "title": "Weekly shop", + "summary": { + "show_on_completion": true + }, + "groups": [ + { + "id": "group-1", + "title": "Weekly shopping", + "blocks": [ + { + "type": "Question", + "id": "block-1", + "question": { + "type": "General", + "id": "question-1", + "title": "How much do you spend on the following in a typical weekly shop?", + "answers": [ + { + "id": "q1-a1", + "label": "Money on milk", + "type": "Currency", + "currency": "GBP", + "mandatory": false, + "decimal_places": 2 + }, + { + "id": "q1-a2", + "label": "Money on eggs", + "type": "Currency", + "currency": "GBP", + "mandatory": false, + "decimal_places": 2 + } + ] + } + }, + { + "type": "Question", + "id": "block-2", + "question": { + "type": "General", + "id": "question-2", + "title": "How much do you spend on these items in a typical week?", + "answers": [ + { + "id": "q2-a1", + "label": "Money on bread", + "type": "Currency", + "currency": "GBP", + "mandatory": false, + "decimal_places": 2 + }, + { + "id": "q2-a2", + "label": "Money on cheese", + "type": "Currency", + "currency": "GBP", + "mandatory": false, + "decimal_places": 2 + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "calculated-summary-1", + "title": "Total of milk and bread is calculated to be %(total)s. Is this correct?", + "calculation": { + "title": "milk + bread", + "operation": { + "+": [ + { + "source": "answers", + "identifier": "q1-a1" + }, + { + "source": "answers", + "identifier": "q2-a1" + } + ] + } + } + }, + { + "type": "CalculatedSummary", + "id": "calculated-summary-2", + "title": "Total of eggs and cheese is calculated to be %(total)s. Is this correct?", + "calculation": { + "title": "eggs + cheese", + "operation": { + "+": [ + { + "source": "answers", + "identifier": "q1-a2" + }, + { + "source": "answers", + "identifier": "q2-a2" + } + ] + } + } + }, + { + "type": "Question", + "id": "block-3", + "question": { + "type": "General", + "id": "question-3", + "title": "Do you want to buy extra of anything this week?", + "guidance": { + "contents": [ + { + "description": "If you select the first option, all your answers so far will be reused in a new calculated summary for extra shopping. If you select the second option, only your answers for bread and cheese will be reused." + } + ] + }, + "answers": [ + { + "type": "Radio", + "id": "radio-extra", + "mandatory": true, + "options": [ + { + "label": "Yes, I am going to buy two of everything", + "value": "Yes, I am going to buy two of everything" + }, + { + "label": "Yes, extra bread and cheese", + "value": "Yes, extra bread and cheese" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + { + "skip_conditions": { + "when": { + "!=": [ + { + "source": "answers", + "identifier": "radio-extra" + }, + "Yes, I am going to buy two of everything" + ] + } + }, + "type": "CalculatedSummary", + "id": "calculated-summary-3", + "title": "Total extra items purchased is calculated to be %(total)s. Is this correct? This reuses your answers to question 1 and 2", + "calculation": { + "title": "(extra) milk + eggs + bread + cheese", + "operation": { + "+": [ + { + "source": "answers", + "identifier": "q1-a1" + }, + { + "source": "answers", + "identifier": "q1-a2" + }, + { + "source": "answers", + "identifier": "q2-a1" + }, + { + "source": "answers", + "identifier": "q2-a2" + } + ] + } + } + }, + { + "skip_conditions": { + "when": { + "!=": [ + { + "source": "answers", + "identifier": "radio-extra" + }, + "Yes, extra bread and cheese" + ] + } + }, + "type": "CalculatedSummary", + "id": "calculated-summary-4", + "title": "Total extra items cost is calculated to be %(total)s. Is this correct? This is reusing your bread and cheese answers", + "calculation": { + "title": "(extra) bread + cheese", + "operation": { + "+": [ + { + "source": "answers", + "identifier": "q2-a1" + }, + { + "source": "answers", + "identifier": "q2-a2" + } + ] + } + } + } + ] + } + ] + }, + { + "id": "section-3", + "title": "Grand calculated summary", + "enabled": { + "when": { + "==": [{ "source": "progress", "selector": "section", "identifier": "section-1" }, "COMPLETED"] + } + }, + "groups": [ + { + "id": "group-2", + "title": "Grand calculated summary", + "blocks": [ + { + "type": "GrandCalculatedSummary", + "id": "grand-calculated-summary-shopping", + "title": "Grand Calculated Summary of purchases this week comes to %(total)s. Is this correct?.", + "calculation": { + "title": "Weekly shopping cost", + "operation": { + "+": [ + { + "source": "calculated_summary", + "identifier": "calculated-summary-1" + }, + { + "source": "calculated_summary", + "identifier": "calculated-summary-2" + }, + { + "source": "calculated_summary", + "identifier": "calculated-summary-3" + }, + { + "source": "calculated_summary", + "identifier": "calculated-summary-4" + } + ] + } + } + } + ] + } + ] + }, + { + "id": "section-4", + "title": "Conditional Section", + "enabled": { + "when": { + ">": [{ "source": "grand_calculated_summary", "identifier": "grand-calculated-summary-shopping" }, 500] + } + }, + "groups": [ + { + "id": "group-3", + "title": "Conditional Group", + "blocks": [ + { + "type": "Interstitial", + "id": "grand-calculated-summary-piping", + "content": { + "title": "This section is only showing because the grand calculated summary exceeded ÂŖ500." + } + } + ] + } + ] + } + ] +} diff --git a/schemas/test/en/test_grand_calculated_summary_repeating_answers.json b/schemas/test/en/test_grand_calculated_summary_repeating_answers.json new file mode 100644 index 0000000000..1938d8263c --- /dev/null +++ b/schemas/test/en/test_grand_calculated_summary_repeating_answers.json @@ -0,0 +1,1426 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Grand Calculated Summary cross section demo", + "theme": "default", + "description": "A schema to showcase grand calculated summary across multiple sections featuring static, dynamic and list repeating block answers", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Hub", + "options": {} + }, + "sections": [ + { + "id": "section-1", + "title": "Food and clothing", + "summary": { + "show_on_completion": true + }, + "groups": [ + { + "id": "group-1", + "title": "Food", + "blocks": [ + { + "type": "Question", + "id": "block-1", + "question": { + "type": "General", + "id": "question-1", + "title": "How much do you spend per month on fruit and veg?", + "answers": [ + { + "id": "q1-a1", + "label": "Money spent on fruit", + "type": "Currency", + "currency": "GBP", + "mandatory": false, + "decimal_places": 2 + }, + { + "id": "q1-a2", + "label": "Money spent on veg", + "type": "Currency", + "currency": "GBP", + "mandatory": false, + "decimal_places": 2 + } + ] + } + }, + { + "type": "Question", + "id": "block-2", + "question": { + "type": "General", + "id": "question-2", + "title": "How much do you spend per month on other food?", + "answers": [ + { + "id": "q2-a1", + "label": "Money spent on bread", + "type": "Currency", + "currency": "GBP", + "mandatory": false, + "decimal_places": 2 + }, + { + "id": "q2-a2", + "label": "Money spent on not bread", + "type": "Currency", + "currency": "GBP", + "mandatory": false, + "decimal_places": 2 + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "calculated-summary-1", + "title": "Calculated Summary for food expenditure is calculated to be %(total)s. Is this correct?", + "calculation": { + "title": "Total monthly food expenditure", + "operation": { + "+": [ + { + "source": "answers", + "identifier": "q1-a1" + }, + { + "source": "answers", + "identifier": "q1-a2" + }, + { + "source": "answers", + "identifier": "q2-a1" + }, + { + "source": "answers", + "identifier": "q2-a2" + } + ] + } + } + } + ] + }, + { + "id": "group-2", + "title": "Clothing", + "blocks": [ + { + "type": "Question", + "id": "block-3", + "question": { + "type": "General", + "id": "question-3", + "title": "How much do you spend per month on clothes?", + "answers": [ + { + "id": "q3-a1", + "label": "Money spent on jumpers", + "type": "Currency", + "currency": "GBP", + "mandatory": false, + "decimal_places": 2 + }, + { + "id": "q3-a2", + "label": "Money spent on hats", + "type": "Currency", + "currency": "GBP", + "mandatory": false, + "decimal_places": 2 + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "calculated-summary-2", + "title": "Calculated summary for clothes expenditure is calculated to be %(total)s. Is this correct?", + "calculation": { + "title": "Total monthly clothes expenditure", + "operation": { + "+": [ + { + "source": "answers", + "identifier": "q3-a1" + }, + { + "source": "answers", + "identifier": "q3-a2" + } + ] + } + } + }, + { + "type": "CalculatedSummary", + "id": "calculated-summary-3", + "title": "Calculated summary for food and clothing is calculated to be %(total)s. Is this correct?", + "calculation": { + "title": "Total food and clothes expenditure", + "operation": { + "+": [ + { + "source": "answers", + "identifier": "q1-a1" + }, + { + "source": "answers", + "identifier": "q1-a2" + }, + { + "source": "answers", + "identifier": "q2-a1" + }, + { + "source": "answers", + "identifier": "q2-a2" + }, + { + "source": "answers", + "identifier": "q3-a1" + }, + { + "source": "answers", + "identifier": "q3-a2" + } + ] + } + } + }, + { + "type": "GrandCalculatedSummary", + "id": "grand-calculated-summary-1", + "title": "Grand Calculated Summary which should match the previous calculated summary is calculated to be %(total)s. Is this correct?", + "calculation": { + "title": "Grand calculated summary", + "operation": { + "+": [ + { + "source": "calculated_summary", + "identifier": "calculated-summary-1" + }, + { + "source": "calculated_summary", + "identifier": "calculated-summary-2" + } + ] + } + } + } + ] + } + ] + }, + { + "id": "section-2", + "title": "Entertainment", + "groups": [ + { + "id": "group-3", + "title": "Group title for questions about games", + "blocks": [ + { + "type": "Question", + "id": "block-4", + "question": { + "type": "General", + "id": "question-4", + "title": "How much do you spend per week on games?", + "guidance": { + "contents": [ + { + "description": "Note:" + }, + { + "list": [ + "The grand calculated summary section after this will only show if the total spending on games is not zero", + "You should test that if you use the change links on the grand calculated summary to come back here and set both answers to 0, that you are not routed to the grand calculated summary when you press continue on the calculated summary, but instead, taken to the Hub.", + "If you use the change links on the grand calculated summary to edit these answer values to a non-zero sum, pressing continue twice should take you back to the grand calculated summary" + ] + } + ] + }, + "answers": [ + { + "id": "q4-a1", + "label": "Video games", + "type": "Currency", + "currency": "GBP", + "mandatory": false, + "decimal_places": 2 + }, + { + "id": "q4-a2", + "label": "Board games", + "type": "Currency", + "currency": "GBP", + "mandatory": false, + "decimal_places": 2 + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "calculated-summary-4", + "title": "Calculated Summary for games expenditure is calculated to be %(total)s. Is this correct?", + "calculation": { + "title": "Total games expenditure", + "operation": { + "+": [ + { + "source": "answers", + "identifier": "q4-a1" + }, + { + "source": "answers", + "identifier": "q4-a2" + } + ] + } + } + } + ] + } + ] + }, + { + "id": "section-3", + "title": "Grand calculated summary of shopping and entertainment", + "enabled": { + "when": { + "and": [ + { + "!=": [ + 0, + { + "source": "calculated_summary", + "identifier": "calculated-summary-4" + } + ] + }, + { + "==": [ + "COMPLETED", + { + "source": "progress", + "selector": "section", + "identifier": "section-1" + } + ] + }, + { + "==": [ + "COMPLETED", + { + "source": "progress", + "selector": "section", + "identifier": "section-2" + } + ] + } + ] + } + }, + "groups": [ + { + "id": "group-4", + "title": "Group title for the grand calculated summary of both sections", + "blocks": [ + { + "type": "GrandCalculatedSummary", + "id": "grand-calculated-summary-2", + "title": "Grand Calculated Summary for shopping and entertainment is calculated to be %(total)s. Is this correct?", + "calculation": { + "title": "Total food clothes and games expenditure", + "operation": { + "+": [ + { + "source": "calculated_summary", + "identifier": "calculated-summary-1" + }, + { + "source": "calculated_summary", + "identifier": "calculated-summary-2" + }, + { + "source": "calculated_summary", + "identifier": "calculated-summary-4" + } + ] + } + } + } + ] + } + ] + }, + { + "id": "section-4", + "title": "Utility bills", + "summary": { + "show_on_completion": true, + "items": [ + { + "type": "List", + "for_list": "utility-bills", + "title": "Utility bills", + "item_anchor_answer_id": "utility-bill-name", + "item_label": "Utility bill", + "add_link_text": "Add another utility bill", + "empty_list_text": "No utility bills added" + } + ], + "show_non_item_answers": true + }, + "groups": [ + { + "id": "group-5", + "title": "Utility bills", + "blocks": [ + { + "type": "ListCollectorDrivingQuestion", + "id": "any-utility-bills", + "for_list": "utility-bills", + "question": { + "type": "General", + "id": "any-utility-bills-question", + "title": "Do you have any monthly expenditure on Utility bills?", + "answers": [ + { + "type": "Radio", + "id": "any-utility-bills-answer", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock", + "params": { + "block_id": "add-utility-bill", + "list_name": "utility-bills" + } + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "routing_rules": [ + { + "when": { + "==": [ + { + "source": "answers", + "identifier": "any-utility-bills-answer" + }, + "Yes" + ] + }, + "block": "any-other-utility-bills" + }, + { + "section": "End" + } + ] + }, + { + "id": "any-other-utility-bills", + "type": "ListCollector", + "for_list": "utility-bills", + "question": { + "id": "any-other-utility-bills-question", + "type": "General", + "title": "Do you need to add any other Utility bills?", + "answers": [ + { + "id": "any-other-utility-bills-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "add_block": { + "id": "add-utility-bill", + "type": "ListAddQuestion", + "question": { + "id": "add-utility-bill-question", + "type": "General", + "title": "What bill do you need to add expenditure for?", + "answers": [ + { + "id": "utility-bill-name", + "label": "Utility bill", + "mandatory": true, + "type": "Dropdown", + "options": [ + { + "label": "Electricity", + "value": "Electricity" + }, + { + "label": "Water", + "value": "Water" + }, + { + "label": "Gas", + "value": "Gas" + }, + { + "label": "Internet", + "value": "Internet" + } + ] + } + ] + } + }, + "edit_block": { + "id": "edit-utility-bill", + "type": "ListEditQuestion", + "question": { + "id": "edit-utility-bill-question", + "type": "General", + "title": "What is the name of the game?", + "answers": [ + { + "id": "utility-bill-name", + "label": "Name of Utility bill", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "remove_block": { + "id": "remove-utility-bill", + "type": "ListRemoveQuestion", + "question": { + "id": "remove-utility-bill-question", + "type": "General", + "title": "Are you sure you want to remove this Utility bill?", + "answers": [ + { + "id": "remove-utility-bill-confirmation", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RemoveListItemAndAnswers" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + "summary": { + "title": "Utility bills", + "item_title": { + "text": "{utility_bill}", + "placeholders": [ + { + "placeholder": "utility_bill", + "value": { + "source": "answers", + "identifier": "utility-bill-name" + } + } + ] + } + } + }, + { + "type": "Question", + "id": "dynamic-answer", + "skip_conditions": { + "when": { + "==": [ + { + "count": [ + { + "source": "list", + "identifier": "utility-bills" + } + ] + }, + 0 + ] + } + }, + "question": { + "dynamic_answers": { + "values": { + "source": "list", + "identifier": "utility-bills" + }, + "answers": [ + { + "label": { + "text": "Monthly expenditure on {utility_bill} bills", + "placeholders": [ + { + "placeholder": "utility_bill", + "value": { + "source": "answers", + "identifier": "utility-bill-name" + } + } + ] + }, + "id": "utility-bill-monthly-cost", + "type": "Currency", + "mandatory": true, + "currency": "GBP", + "decimal_places": 2 + } + ] + }, + "id": "dynamic-answer-question", + "title": "Monthly expenditure on utility bills", + "type": "General" + } + }, + { + "type": "CalculatedSummary", + "id": "calculated-summary-5", + "title": "Calculated Summary for monthly spending on utility bills is calculated to be %(total)s. Is this correct?", + "calculation": { + "title": "Total monthly expenditure on utility bills", + "operation": { + "+": [ + { + "source": "answers", + "identifier": "utility-bill-monthly-cost" + } + ] + } + } + } + ] + } + ] + }, + { + "id": "section-5", + "title": "Streaming services", + "summary": { + "show_on_completion": true, + "items": [ + { + "type": "List", + "for_list": "streaming-services", + "title": "Streaming services", + "item_anchor_answer_id": "streaming-service-name", + "item_label": "Streaming service", + "add_link_text": "Add another streaming service", + "empty_list_text": "No streaming services added" + } + ], + "show_non_item_answers": true + }, + "groups": [ + { + "id": "group-6", + "title": "Streaming services", + "blocks": [ + { + "type": "ListCollectorDrivingQuestion", + "id": "any-streaming-services", + "for_list": "streaming-services", + "question": { + "type": "General", + "id": "any-streaming-services-question", + "title": "Do you have any monthly expenditure on streaming services?", + "answers": [ + { + "type": "Radio", + "id": "any-streaming-services-answer", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock", + "params": { + "block_id": "add-streaming-service", + "list_name": "streaming-services" + } + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "routing_rules": [ + { + "when": { + "==": [ + { + "source": "answers", + "identifier": "any-streaming-services-answer" + }, + "Yes" + ] + }, + "block": "any-other-streaming-services" + }, + { + "section": "End" + } + ] + }, + { + "id": "any-other-streaming-services", + "type": "ListCollector", + "for_list": "streaming-services", + "question": { + "id": "any-other-streaming-services-question", + "type": "General", + "title": "Do you need to add any other streaming services?", + "answers": [ + { + "id": "any-other-streaming-services-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "add_block": { + "id": "add-streaming-service", + "type": "ListAddQuestion", + "question": { + "id": "add-streaming-service-question", + "type": "General", + "title": "What is the name of the Streaming service?", + "answers": [ + { + "id": "streaming-service-name", + "label": "Name of Streaming service", + "mandatory": true, + "type": "Dropdown", + "options": [ + { + "label": "Netflix", + "value": "Netflix" + }, + { + "label": "Prime video", + "value": "Prime video" + }, + { + "label": "Now TV", + "value": "Now TV" + }, + { + "label": "Apple TV", + "value": "Apple TV" + }, + { + "label": "Disney+", + "value": "Disney+" + } + ] + } + ] + } + }, + "edit_block": { + "id": "edit-streaming-service", + "type": "ListEditQuestion", + "question": { + "id": "edit-streaming-service-question", + "type": "General", + "title": "What is the name of the Streaming service?", + "answers": [ + { + "id": "streaming-service-name", + "label": "Name of Streaming service", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "remove_block": { + "id": "remove-streaming-service", + "type": "ListRemoveQuestion", + "question": { + "id": "remove-streaming-service-question", + "type": "General", + "title": "Are you sure you want to remove this Streaming service?", + "answers": [ + { + "id": "remove-streaming-service-confirmation", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RemoveListItemAndAnswers" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + "repeating_blocks": [ + { + "id": "streaming-service-repeating-block-1", + "type": "ListRepeatingQuestion", + "question": { + "id": "streaming-service-repeating-block-1-question", + "type": "General", + "title": { + "text": "What is your monthly expenditure on {streaming_service} bills?", + "placeholders": [ + { + "placeholder": "streaming_service", + "value": { + "source": "answers", + "identifier": "streaming-service-name" + } + } + ] + }, + "answers": [ + { + "id": "streaming-service-monthly-cost", + "label": { + "text": "Monthly subscription cost for {streaming_service}", + "placeholders": [ + { + "placeholder": "streaming_service", + "value": { + "source": "answers", + "identifier": "streaming-service-name" + } + } + ] + }, + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + }, + { + "id": "streaming-service-extra-cost", + "label": { + "text": "Additional monthly spending on {streaming_service}", + "placeholders": [ + { + "placeholder": "streaming_service", + "value": { + "source": "answers", + "identifier": "streaming-service-name" + } + } + ] + }, + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "id": "streaming-service-repeating-block-2", + "type": "ListRepeatingQuestion", + "question": { + "id": "streaming-service-repeating-block-2-question", + "type": "General", + "title": { + "text": "What is your average monthly internet usage for {streaming_service}?", + "placeholders": [ + { + "placeholder": "streaming_service", + "value": { + "source": "answers", + "identifier": "streaming-service-name" + } + } + ] + }, + "answers": [ + { + "id": "streaming-service-usage", + "label": { + "text": "Monthly Internet usage for {streaming_service}", + "placeholders": [ + { + "placeholder": "streaming_service", + "value": { + "source": "answers", + "identifier": "streaming-service-name" + } + } + ] + }, + "mandatory": true, + "type": "Unit", + "unit_length": "short", + "unit": "digital-gigabyte", + "decimal_places": 2 + } + ] + } + } + ], + "summary": { + "title": "Streaming services", + "item_title": { + "text": "{streaming_service_name}", + "placeholders": [ + { + "placeholder": "streaming_service_name", + "value": { + "source": "answers", + "identifier": "streaming-service-name" + } + } + ] + } + } + }, + { + "type": "CalculatedSummary", + "id": "calculated-summary-6", + "title": "Calculated Summary for monthly expenditure on streaming services is calculated to be %(total)s. Is this correct?", + "calculation": { + "title": "Total monthly expenditure on streaming services", + "operation": { + "+": [ + { + "source": "answers", + "identifier": "streaming-service-monthly-cost" + }, + { + "source": "answers", + "identifier": "streaming-service-extra-cost" + } + ] + } + } + }, + { + "type": "CalculatedSummary", + "id": "calculated-summary-7", + "title": "Total monthly internet usage on streaming services is calculated to be %(total)s. Is this correct?", + "calculation": { + "title": "Total internet usage on streaming services", + "operation": { + "+": [ + { + "source": "answers", + "identifier": "streaming-service-usage" + } + ] + } + } + }, + { + "type": "Question", + "id": "other-internet-usage", + "question": { + "type": "General", + "id": "other-internet-question", + "title": "Do you have any additional internet usage to report?", + "answers": [ + { + "id": "media-downloads", + "label": "Internet usage on media downloads", + "mandatory": false, + "type": "Unit", + "unit_length": "short", + "unit": "digital-gigabyte", + "decimal_places": 2 + }, + { + "id": "misc-internet", + "label": "Other miscellaneous internet usage", + "mandatory": false, + "type": "Unit", + "unit_length": "short", + "unit": "digital-gigabyte", + "decimal_places": 2 + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "calculated-summary-8", + "title": "Total monthly internet usage on other services is calculated to be %(total)s. Is this correct?", + "calculation": { + "title": "Total internet usage on other services", + "operation": { + "+": [ + { + "source": "answers", + "identifier": "media-downloads" + }, + { + "source": "answers", + "identifier": "misc-internet" + } + ] + } + } + } + ] + } + ] + }, + { + "id": "section-6", + "title": "Grand calculated summary of household expenditure", + "enabled": { + "when": { + "and": [ + { + "==": [ + "COMPLETED", + { + "source": "progress", + "selector": "section", + "identifier": "section-1" + } + ] + }, + { + "==": [ + "COMPLETED", + { + "source": "progress", + "selector": "section", + "identifier": "section-2" + } + ] + }, + { + "==": [ + "COMPLETED", + { + "source": "progress", + "selector": "section", + "identifier": "section-4" + } + ] + }, + { + "==": [ + "COMPLETED", + { + "source": "progress", + "selector": "section", + "identifier": "section-5" + } + ] + } + ] + } + }, + "summary": { + "show_on_completion": true + }, + "groups": [ + { + "id": "group-7", + "title": "Expenditure grand totals", + "blocks": [ + { + "type": "GrandCalculatedSummary", + "id": "grand-calculated-summary-3", + "title": "Grand Calculated Summary for monthly spending on bills and services is calculated to be %(total)s. Is this correct?", + "calculation": { + "title": "Total monthly expenditure on bills and streaming services", + "operation": { + "+": [ + { + "source": "calculated_summary", + "identifier": "calculated-summary-5" + }, + { + "source": "calculated_summary", + "identifier": "calculated-summary-6" + } + ] + } + } + }, + { + "type": "GrandCalculatedSummary", + "id": "grand-calculated-summary-4", + "title": "Grand Calculated Summary for internet usage is calculated to be %(total)s. Is this correct?", + "calculation": { + "title": "Total internet usage", + "operation": { + "+": [ + { + "source": "calculated_summary", + "identifier": "calculated-summary-7" + }, + { + "source": "calculated_summary", + "identifier": "calculated-summary-8" + } + ] + } + } + }, + { + "type": "GrandCalculatedSummary", + "id": "grand-calculated-summary-5", + "title": "Grand Calculated Summary for total monthly household expenditure is calculated to be %(total)s. Is this correct?", + "calculation": { + "title": "Total monthly expenditure", + "operation": { + "+": [ + { + "source": "calculated_summary", + "identifier": "calculated-summary-1" + }, + { + "source": "calculated_summary", + "identifier": "calculated-summary-2" + }, + { + "source": "calculated_summary", + "identifier": "calculated-summary-4" + }, + { + "source": "calculated_summary", + "identifier": "calculated-summary-5" + }, + { + "source": "calculated_summary", + "identifier": "calculated-summary-6" + } + ] + } + } + }, + { + "type": "Question", + "id": "internet-breakdown-block", + "question": { + "id": "internet-breakdown-question", + "title": { + "text": "How did you use the {internet_usage} across your devices?", + "placeholders": [ + { + "placeholder": "internet_usage", + "transforms": [ + { + "transform": "format_unit", + "arguments": { + "value": { + "source": "grand_calculated_summary", + "identifier": "grand-calculated-summary-4" + }, + "unit": "digital-gigabyte", + "unit_length": "short" + } + } + ] + } + ] + }, + "type": "Calculated", + "warning": "These answers must add up to the total internet usage", + "calculations": [ + { + "calculation_type": "sum", + "value": { + "identifier": "grand-calculated-summary-4", + "source": "grand_calculated_summary" + }, + "answers_to_calculate": ["internet-pc", "internet-phone"], + "conditions": ["equals"] + } + ], + "answers": [ + { + "id": "internet-pc", + "label": "Amount of internet usage via PC", + "mandatory": true, + "type": "Unit", + "unit_length": "short", + "unit": "digital-gigabyte", + "decimal_places": 2 + }, + { + "id": "internet-phone", + "label": "Amount of internet usage via Phone", + "mandatory": true, + "type": "Unit", + "unit_length": "short", + "unit": "digital-gigabyte", + "decimal_places": 2 + } + ] + } + } + ] + } + ] + }, + { + "id": "section-7", + "title": "Personal Expenditure", + "enabled": { + "when": { + "==": [ + "COMPLETED", + { + "source": "progress", + "selector": "section", + "identifier": "section-6" + } + ] + } + }, + "summary": { + "show_on_completion": true + }, + "groups": [ + { + "id": "group-8", + "blocks": [ + { + "type": "Question", + "id": "personal-expenditure-block", + "question": { + "id": "personal-expenditure-question", + "title": { + "text": "How much of the {total_expenditure} household expenditure do you contribute personally?", + "placeholders": [ + { + "placeholder": "total_expenditure", + "transforms": [ + { + "transform": "format_currency", + "arguments": { + "number": { + "identifier": "grand-calculated-summary-5", + "source": "grand_calculated_summary" + } + } + } + ] + } + ] + }, + "type": "General", + "answers": [ + { + "id": "personal-expenditure-answer", + "label": "Personal contribution", + "mandatory": true, + "description": "Cannot exceed the total expenditure from section 6", + "type": "Currency", + "currency": "GBP", + "decimal_places": 2, + "maximum": { + "value": { + "identifier": "grand-calculated-summary-5", + "source": "grand_calculated_summary" + } + } + } + ] + } + }, + { + "type": "Interstitial", + "id": "grand-calculated-summary-piping", + "content": { + "title": "You have provided the following information about household expenditure and internet use.", + "contents": [ + { + "list": [ + { + "text": "Total household expenditure: {total_expenditure}", + "placeholders": [ + { + "placeholder": "total_expenditure", + "transforms": [ + { + "transform": "format_currency", + "arguments": { + "number": { + "identifier": "grand-calculated-summary-5", + "source": "grand_calculated_summary" + } + } + } + ] + } + ] + }, + { + "text": "Personal contribution: {personal_contribution}", + "placeholders": [ + { + "placeholder": "personal_contribution", + "transforms": [ + { + "transform": "format_currency", + "arguments": { + "number": { + "identifier": "personal-expenditure-answer", + "source": "answers" + } + } + } + ] + } + ] + }, + { + "text": "Total internet usage: {internet_usage}", + "placeholders": [ + { + "placeholder": "internet_usage", + "transforms": [ + { + "transform": "format_unit", + "arguments": { + "value": { + "source": "grand_calculated_summary", + "identifier": "grand-calculated-summary-4" + }, + "unit": "digital-gigabyte", + "unit_length": "short" + } + } + ] + } + ] + }, + { + "text": "Usage by phone: {internet_phone}", + "placeholders": [ + { + "placeholder": "internet_phone", + "transforms": [ + { + "transform": "format_unit", + "arguments": { + "value": { + "source": "answers", + "identifier": "internet-phone" + }, + "unit": "digital-gigabyte", + "unit_length": "short" + } + } + ] + } + ] + }, + { + "text": "Usage by PC: {internet_pc}", + "placeholders": [ + { + "placeholder": "internet_pc", + "transforms": [ + { + "transform": "format_unit", + "arguments": { + "value": { + "source": "answers", + "identifier": "internet-pc" + }, + "unit": "digital-gigabyte", + "unit_length": "short" + } + } + ] + } + ] + } + ] + } + ] + } + } + ] + } + ] + } + ] +} diff --git a/schemas/test/en/test_hub_and_spoke.json b/schemas/test/en/test_hub_and_spoke.json index fa5920796f..7535553170 100644 --- a/schemas/test/en/test_hub_and_spoke.json +++ b/schemas/test/en/test_hub_and_spoke.json @@ -92,20 +92,19 @@ }, "routing_rules": [ { - "goto": { - "group": "checkboxes", - "when": [ + "group": "checkboxes", + "when": { + "!=": [ { - "id": "employment-status-answer", - "condition": "set" - } + "identifier": "employment-status-answer", + "source": "answers" + }, + null ] } }, { - "goto": { - "block": "employment-type" - } + "block": "employment-type" } ] }, @@ -155,7 +154,9 @@ { "id": "accommodation-section", "title": "Accommodation", - "summary": { "show_on_completion": true }, + "summary": { + "show_on_completion": true + }, "groups": [ { "blocks": [ @@ -192,7 +193,9 @@ ] }, { - "summary": { "show_on_completion": true }, + "summary": { + "show_on_completion": true + }, "groups": [ { "blocks": [ @@ -217,7 +220,7 @@ } ], "id": "does-anyone-live-here-question", - "title": "Does anyone live here? ", + "title": "Does anyone live here?", "type": "General" }, "type": "Question" @@ -251,17 +254,17 @@ "type": "General" }, "type": "Question", - "skip_conditions": [ - { - "when": [ + "skip_conditions": { + "when": { + "==": [ { - "id": "does-anyone-live-here-answer", - "condition": "equals", - "value": "No" - } + "source": "answers", + "identifier": "does-anyone-live-here-answer" + }, + "No" ] } - ] + } } ], "id": "household-question-group", @@ -275,7 +278,9 @@ "id": "relationships-section", "title": "Relationships", "show_on_hub": false, - "summary": { "show_on_completion": true }, + "summary": { + "show_on_completion": true + }, "groups": [ { "blocks": [ diff --git a/schemas/test/en/test_hub_and_spoke_custom_content.json b/schemas/test/en/test_hub_and_spoke_custom_content.json index ca9a17bbee..b951e23cba 100644 --- a/schemas/test/en/test_hub_and_spoke_custom_content.json +++ b/schemas/test/en/test_hub_and_spoke_custom_content.json @@ -33,7 +33,9 @@ }, "sections": [ { - "summary": { "show_on_completion": true }, + "summary": { + "show_on_completion": true + }, "groups": [ { "blocks": [ @@ -58,7 +60,7 @@ } ], "id": "does-anyone-live-here-question", - "title": "Does anyone live here? ", + "title": "Does anyone live here?", "type": "General" }, "type": "Question" @@ -92,17 +94,17 @@ "type": "General" }, "type": "Question", - "skip_conditions": [ - { - "when": [ + "skip_conditions": { + "when": { + "==": [ { - "id": "does-anyone-live-here-answer", - "condition": "equals", - "value": "No" - } + "source": "answers", + "identifier": "does-anyone-live-here-answer" + }, + "No" ] } - ] + } } ], "id": "household-question-group", diff --git a/schemas/test/en/test_hub_complete_sections.json b/schemas/test/en/test_hub_complete_sections.json index e19f7c53db..e0691427e9 100644 --- a/schemas/test/en/test_hub_complete_sections.json +++ b/schemas/test/en/test_hub_complete_sections.json @@ -23,7 +23,9 @@ ], "questionnaire_flow": { "type": "Hub", - "options": { "required_completed_sections": ["employment-section"] } + "options": { + "required_completed_sections": ["employment-section"] + } }, "sections": [ { @@ -92,20 +94,19 @@ }, "routing_rules": [ { - "goto": { - "group": "checkboxes", - "when": [ + "group": "checkboxes", + "when": { + "!=": [ { - "id": "employment-status-answer", - "condition": "set" - } + "identifier": "employment-status-answer", + "source": "answers" + }, + null ] } }, { - "goto": { - "block": "employment-type" - } + "block": "employment-type" } ] }, @@ -155,7 +156,9 @@ { "id": "accommodation-section", "title": "Accommodation", - "summary": { "show_on_completion": true }, + "summary": { + "show_on_completion": true + }, "groups": [ { "blocks": [ diff --git a/schemas/test/en/test_hub_section_required_and_enabled.json b/schemas/test/en/test_hub_section_required_and_enabled.json index 8da0b88f61..6320062ce6 100644 --- a/schemas/test/en/test_hub_section_required_and_enabled.json +++ b/schemas/test/en/test_hub_section_required_and_enabled.json @@ -106,17 +106,17 @@ "title": "Relationships count" } ], - "enabled": [ - { - "when": [ + "enabled": { + "when": { + "==": [ + "Yes", { - "id": "household-relationships-answer", - "condition": "equals", - "value": "Yes" + "source": "answers", + "identifier": "household-relationships-answer" } ] } - ] + } } ] } diff --git a/schemas/test/en/test_new_variants_first_item_in_list.json b/schemas/test/en/test_hub_section_required_with_repeat.json similarity index 53% rename from schemas/test/en/test_new_variants_first_item_in_list.json rename to schemas/test/en/test_hub_section_required_with_repeat.json index 0e3f8f366f..aaff2a9ed3 100644 --- a/schemas/test/en/test_new_variants_first_item_in_list.json +++ b/schemas/test/en/test_hub_section_required_with_repeat.json @@ -4,9 +4,9 @@ "schema_version": "0.0.1", "data_version": "0.0.3", "survey_id": "0", - "title": "Test New Question Variants Using List", + "title": "Hub & Spoke Enabled with Required Repeating Sections", "theme": "default", - "description": "A questionnaire to test new question variants using the first item in a list", + "description": "A questionnaire to demo hub and spoke functionality with required repeating sections", "metadata": [ { "name": "user_id", @@ -23,17 +23,84 @@ ], "questionnaire_flow": { "type": "Hub", - "options": {} + "options": { + "required_completed_sections": ["list-collector-section", "personal-details-section"] + } }, "sections": [ { - "id": "section", + "summary": { + "show_on_completion": true, + "items": [ + { + "type": "List", + "for_list": "people", + "title": "Household members", + "add_link_text": "Add someone to this household", + "empty_list_text": "There are no householders" + } + ] + }, + "id": "list-collector-section", "title": "Household", "groups": [ { "id": "group", "title": "List", "blocks": [ + { + "id": "primary-person-list-collector", + "type": "PrimaryPersonListCollector", + "for_list": "people", + "add_or_edit_block": { + "id": "add-or-edit-primary-person", + "type": "PrimaryPersonListAddOrEditQuestion", + "question": { + "id": "primary-person-add-or-edit-question", + "type": "General", + "title": "What is your name?", + "answers": [ + { + "id": "first-name", + "label": "First name", + "mandatory": true, + "type": "TextField" + }, + { + "id": "last-name", + "label": "Last name", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "question": { + "id": "primary-confirmation-question", + "type": "General", + "title": "Do you live here?", + "answers": [ + { + "id": "you-live-here", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, { "id": "list-collector", "type": "ListCollector", @@ -175,7 +242,9 @@ { "id": "personal-details-section", "title": "Personal Details", - "summary": { "show_on_completion": true }, + "summary": { + "show_on_completion": true + }, "repeat": { "for_list": "people", "title": { @@ -211,42 +280,71 @@ "title": "Personal Details", "blocks": [ { - "id": "list-status", + "id": "proxy", + "question": { + "answers": [ + { + "default": "Yes", + "id": "proxy-answer", + "mandatory": false, + "options": [ + { + "label": "No, I’m answering for myself", + "value": "No, I’m answering for myself" + }, + { + "label": "Yes", + "value": "Yes" + } + ], + "type": "Radio" + } + ], + "id": "proxy-question", + "title": "Are you answering the questions on behalf of someone else?", + "type": "General" + }, + "type": "Question" + }, + { + "id": "date-of-birth", "question_variants": [ { "question": { "answers": [ { - "id": "list-status-answer", - "mandatory": false, - "options": [ - { - "label": "Yes", - "value": "Yes" + "id": "date-of-birth-answer", + "mandatory": true, + "maximum": { + "value": "now" + }, + "minimum": { + "offset_by": { + "years": -115 }, - { - "label": "No", - "value": "No" - } - ], - "type": "Radio" + "value": "2019-10-13" + }, + "type": "Date" } ], - "id": "list-status-question", - "title": "You are the first person in the list", + "guidance": { + "contents": [ + { + "description": "For example 31 12 1970" + } + ] + }, + "id": "date-of-birth-question", + "title": "What is your date of birth?", "type": "General" }, "when": { "==": [ { - "source": "list", - "identifier": "people", - "selector": "first" + "source": "answers", + "identifier": "proxy-answer" }, - { - "source": "location", - "identifier": "list_item_id" - } + "No, I’m answering for myself" ] } }, @@ -254,36 +352,71 @@ "question": { "answers": [ { - "id": "list-status-answer", - "mandatory": false, - "options": [ - { - "label": "Yes", - "value": "Yes" + "id": "date-of-birth-answer", + "mandatory": true, + "maximum": { + "value": "now" + }, + "minimum": { + "offset_by": { + "years": -115 }, - { - "label": "No", - "value": "No" - } - ], - "type": "Radio" + "value": "2019-10-13" + }, + "type": "Date" } ], - "id": "list-status-question", - "title": "You are not the first person in the list", + "guidance": { + "contents": [ + { + "description": "For example 31 12 1970" + } + ] + }, + "id": "date-of-birth-question", + "title": { + "placeholders": [ + { + "placeholder": "person_name_possessive", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "first-name" + }, + { + "source": "answers", + "identifier": "last-name" + } + ] + }, + "transform": "concatenate_list" + }, + { + "arguments": { + "string_to_format": { + "source": "previous_transform" + } + }, + "transform": "format_possessive" + } + ] + } + ], + "text": "What is {person_name_possessive} date of birth?" + }, "type": "General" }, "when": { - "!=": [ + "==": [ { - "source": "list", - "identifier": "people", - "selector": "first" + "source": "answers", + "identifier": "proxy-answer" }, - { - "source": "location", - "identifier": "list_item_id" - } + "Yes" ] } } diff --git a/schemas/test/en/test_hub_section_required_with_repeat_supplementary.json b/schemas/test/en/test_hub_section_required_with_repeat_supplementary.json new file mode 100644 index 0000000000..a61369a87a --- /dev/null +++ b/schemas/test/en/test_hub_section_required_with_repeat_supplementary.json @@ -0,0 +1,414 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "123", + "title": "Test Hub Enabled with Repeats Using Supplementary Data", + "theme": "default", + "description": "A questionnaire to demo the Hub enabled when repeating sections using Supplementary data are complete.", + "metadata": [ + { + "name": "survey_id", + "type": "string" + }, + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + }, + { + "name": "sds_dataset_id", + "type": "string" + } + ], + "supplementary_data": { + "lists": ["employees"] + }, + "questionnaire_flow": { + "type": "Hub", + "options": { + "required_completed_sections": ["introduction-section", "section-2", "section-3"] + } + }, + "post_submission": { + "view_response": true + }, + "sections": [ + { + "id": "introduction-section", + "title": "Introduction", + "groups": [ + { + "id": "introduction-group", + "title": "Introduction Group", + "blocks": [ + { + "id": "loaded-successfully-block", + "type": "Interstitial", + "content": { + "title": "Supplementary Data", + "contents": [ + { + "title": "You have successfully loaded Supplementary data", + "description": "Press continue to proceed to the introduction", + "guidance": { + "contents": [ + { + "description": "The purpose of this block, is to test that supplementary data loads successfully, separate to using the supplementary data" + } + ] + } + } + ] + } + }, + { + "id": "introduction-block", + "type": "Introduction", + "primary_content": [ + { + "id": "business-details", + "title": { + "text": "You are completing this survey for {company_name}", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "supplementary_data", + "identifier": "company_name" + } + } + ] + }, + "contents": [ + { + "description": { + "text": "If the company details or structure have changed contact us on {telephone_number_link}", + "placeholders": [ + { + "placeholder": "telephone_number_link", + "value": { + "source": "supplementary_data", + "identifier": "company_details", + "selectors": ["telephone_number"] + } + } + ] + } + }, + { + "guidance": { + "contents": [ + { + "title": "Guidance for completing this survey", + "list": [ + "The company name, telephone number all come from supplementary data", + "if you picked the supplementary dataset with guidance, there will be a 3rd bullet point below this one, with the supplementary guidance.", + { + "text": "{survey_guidance}", + "placeholders": [ + { + "placeholder": "survey_guidance", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "source": "supplementary_data", + "identifier": "guidance" + } + ] + } + } + ] + } + ] + } + ] + } + ] + } + } + ] + } + ] + } + ] + } + ] + }, + { + "id": "section-2", + "title": "Employees", + "groups": [ + { + "id": "employee-reporting", + "blocks": [ + { + "id": "list-collector-employees", + "type": "ListCollectorContent", + "page_title": "Employees", + "for_list": "employees", + "content": { + "title": "Employees", + "contents": [ + { + "definition": { + "title": "Company employees", + "contents": [ + { + "description": "List of previously reported employees." + } + ] + } + }, + { + "description": "You have previously reported on the above employees. Press continue to proceed to the next section where you can add any additional employees." + } + ] + }, + "summary": { + "title": "employees", + "item_title": { + "text": "{employee_name}", + "placeholders": [ + { + "placeholder": "employee_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "supplementary_data", + "identifier": "employees", + "selectors": ["personal_details", "forename"] + }, + { + "source": "supplementary_data", + "identifier": "employees", + "selectors": ["personal_details", "surname"] + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + } + } + } + ] + } + ] + }, + { + "id": "section-3", + "title": "Employee Details", + "summary": { + "show_on_completion": true + }, + "repeat": { + "for_list": "employees", + "title": { + "text": "{employee_name}", + "placeholders": [ + { + "placeholder": "employee_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "supplementary_data", + "identifier": "employees", + "selectors": ["personal_details", "forename"] + }, + { + "source": "supplementary_data", + "identifier": "employees", + "selectors": ["personal_details", "surname"] + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + } + }, + "groups": [ + { + "id": "employee-detail-questions", + "blocks": [ + { + "type": "Question", + "id": "length-of-employment", + "question": { + "id": "length-employment-question", + "type": "General", + "title": { + "text": "When did {employee_name} start working for {company_name}?", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "supplementary_data", + "identifier": "company_name" + } + }, + { + "placeholder": "employee_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "supplementary_data", + "identifier": "employees", + "selectors": ["personal_details", "forename"] + }, + { + "source": "supplementary_data", + "identifier": "employees", + "selectors": ["personal_details", "surname"] + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + }, + "answers": [ + { + "id": "employment-start", + "label": { + "text": "Start date at {company_name}", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "supplementary_data", + "identifier": "company_name" + } + } + ] + }, + "mandatory": true, + "type": "Date", + "maximum": { + "value": "now" + }, + "minimum": { + "value": { + "source": "supplementary_data", + "identifier": "incorporation_date" + } + } + } + ] + } + }, + { + "id": "conditional-employee-block", + "type": "Question", + "skip_conditions": { + "when": { + "!=": [ + { + "count": [ + { + "source": "list", + "identifier": "employees" + } + ] + }, + 3 + ] + } + }, + "question": { + "id": "conditional-employee-question", + "guidance": { + "contents": [ + { + "description": "This block is enabled because there are 3 employees in the supplementary dataset" + } + ] + }, + "type": "General", + "title": { + "text": "Has {employee_name} been promoted since starting at {company_name}?", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "supplementary_data", + "identifier": "company_name" + } + }, + { + "placeholder": "employee_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "supplementary_data", + "identifier": "employees", + "selectors": ["personal_details", "forename"] + }, + { + "source": "supplementary_data", + "identifier": "employees", + "selectors": ["personal_details", "surname"] + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + }, + "answers": [ + { + "id": "promoted-yes-no-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + } + ] + } + ] + } + ] +} diff --git a/schemas/test/en/test_individual_response.json b/schemas/test/en/test_individual_response.json index 60a2bf6e01..447c3b0915 100644 --- a/schemas/test/en/test_individual_response.json +++ b/schemas/test/en/test_individual_response.json @@ -5,7 +5,7 @@ "data_version": "0.0.3", "survey_id": "census", "title": "Test Individual Response", - "theme": "census", + "theme": "default", "description": "A questionnaire to test individual response", "metadata": [ { @@ -576,7 +576,7 @@ ] } ], - "text": "Are you {person_name}?" + "text": "Are you {person_name}?" }, "type": "General" }, diff --git a/schemas/test/en/test_individual_response_on_hub_disabled.json b/schemas/test/en/test_individual_response_on_hub_disabled.json index b8616d5757..8e64f1967b 100644 --- a/schemas/test/en/test_individual_response_on_hub_disabled.json +++ b/schemas/test/en/test_individual_response_on_hub_disabled.json @@ -5,7 +5,7 @@ "data_version": "0.0.3", "survey_id": "census", "title": "Test Individual Response", - "theme": "census", + "theme": "default", "description": "A questionnaire to test individual response", "metadata": [ { @@ -398,7 +398,7 @@ ] } ], - "text": "Are you {person_name}?" + "text": "Are you {person_name}?" }, "type": "General" }, diff --git a/schemas/test/en/test_instructions.json b/schemas/test/en/test_instructions.json index 73accd7825..d80841ce84 100644 --- a/schemas/test/en/test_instructions.json +++ b/schemas/test/en/test_instructions.json @@ -63,7 +63,6 @@ "id": "favourite-lunch", "label": "What is your favourite lunchtime food", "mandatory": false, - "q_code": "0", "type": "TextField" } ], diff --git a/schemas/test/en/test_interstitial_definition.json b/schemas/test/en/test_interstitial_definition.json index a8f9659189..27ae0408ee 100644 --- a/schemas/test/en/test_interstitial_definition.json +++ b/schemas/test/en/test_interstitial_definition.json @@ -53,16 +53,6 @@ ] } }, - { - "definition": { - "title": "Questionnaire", - "contents": [ - { - "description": "A set of printed or written questions with a choice of answers, devised for the purposes of a survey or statistical study" - } - ] - } - }, { "description": "You can now continue." } @@ -91,7 +81,6 @@ "type": "Radio" } ], - "id": "content-variant-definition-question", "type": "General", "title": "What would you like to see a definition about?" @@ -130,13 +119,15 @@ } ] }, - "when": [ - { - "id": "content-variant-definition-answer", - "condition": "equals", - "value": "Answer" - } - ] + "when": { + "==": [ + { + "source": "answers", + "identifier": "content-variant-definition-answer" + }, + "Answer" + ] + } }, { "content": { @@ -157,13 +148,15 @@ } ] }, - "when": [ - { - "id": "content-variant-definition-answer", - "condition": "equals", - "value": "Question" - } - ] + "when": { + "==": [ + { + "source": "answers", + "identifier": "content-variant-definition-answer" + }, + "Question" + ] + } } ] } diff --git a/schemas/test/en/test_interstitial_page.json b/schemas/test/en/test_interstitial_page.json index fb41492557..18b59c4440 100644 --- a/schemas/test/en/test_interstitial_page.json +++ b/schemas/test/en/test_interstitial_page.json @@ -49,7 +49,6 @@ "id": "favourite-breakfast", "label": "What is your favourite breakfast food", "mandatory": false, - "q_code": "0", "type": "TextField" } ], @@ -79,7 +78,6 @@ "id": "favourite-lunch", "label": "What is your favourite lunchtime food", "mandatory": false, - "q_code": "0", "type": "TextField" } ], diff --git a/schemas/test/en/test_interstitial_page_title.json b/schemas/test/en/test_interstitial_page_title.json index e9d57c6b8c..12f9b9b766 100644 --- a/schemas/test/en/test_interstitial_page_title.json +++ b/schemas/test/en/test_interstitial_page_title.json @@ -7,11 +7,6 @@ "title": "Interstitial Page Titles", "theme": "default", "description": "A questionnaire to demo interstitial pages titles.", - "messages": { - "NUMBER_TOO_LARGE": "Number is too large", - "NUMBER_TOO_SMALL": "Number cannot be less than zero", - "INVALID_NUMBER": "Please enter an integer" - }, "metadata": [ { "name": "user_id", @@ -24,10 +19,6 @@ { "name": "ru_name", "type": "string" - }, - { - "name": "case_id", - "type": "string" } ], "questionnaire_flow": { @@ -41,31 +32,31 @@ { "blocks": [ { - "id": "breakfast-interstitial", + "id": "interstitial-page", "content": { "title": { "placeholders": [ { - "placeholder": "case_id", + "placeholder": "ru_name", "value": { - "identifier": "case_id", + "identifier": "ru_name", "source": "metadata" } } ], - "text": "This is the content title {case_id}" + "text": "Your RU name: {ru_name}" }, "contents": [ { - "description": "You have successfully completed the breakfast section. Next we want to know about your lunch." + "description": "You have successfully completed the section." } ] }, "type": "Interstitial" } ], - "id": "favourite-foods", - "title": "Favourite food" + "id": "interstitial-page-titles", + "title": "Interstitial page titles" } ] } diff --git a/schemas/test/en/test_interviewer_note.json b/schemas/test/en/test_interviewer_note.json index 5141486394..3d67b35043 100644 --- a/schemas/test/en/test_interviewer_note.json +++ b/schemas/test/en/test_interviewer_note.json @@ -54,7 +54,6 @@ "id": "favourite-team-answer", "label": "Favourite team", "mandatory": false, - "q_code": "0", "type": "TextField" } ], diff --git a/schemas/test/en/test_introduction.json b/schemas/test/en/test_introduction.json index 14ac1d5b99..c2ab5babf1 100644 --- a/schemas/test/en/test_introduction.json +++ b/schemas/test/en/test_introduction.json @@ -5,6 +5,7 @@ "data_version": "0.0.3", "survey_id": "144", "theme": "default", + "preview_questions": true, "title": "Test introduction", "legal_basis": "Notice is given under section 999 of the Test Act 2000", "metadata": [ @@ -28,6 +29,18 @@ "name": "trad_as", "type": "string", "optional": true + }, + { + "name": "ref_p_start_date", + "type": "date" + }, + { + "name": "ref_p_end_date", + "type": "date" + }, + { + "name": "display_address", + "type": "string" } ], "questionnaire_flow": { @@ -37,7 +50,7 @@ "sections": [ { "id": "introduction-section", - "title": "Introduction", + "title": "Main section", "groups": [ { "id": "introduction-group", @@ -239,6 +252,454 @@ } ] }, + { + "type": "Question", + "id": "report-radio", + "question": { + "guidance": { + "contents": [ + { + "description": "Please provide figures for the period in which you were trading." + } + ] + }, + "description": ["

Your return should relate to the calendar year 2021.

"], + "instruction": ["Select your answer"], + "answers": [ + { + "id": "report-radio-answer", + "mandatory": true, + "description": "Select your answer", + "guidance": { + "show_guidance": "Additional guidance", + "hide_guidance": "Additional guidance", + "contents": [ + { + "description": "For example select `yes` if you can report for this period" + } + ] + }, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ], + "id": "report-radio-question", + "title": { + "text": "Are you able to report for the calendar month {start_date} to {end_date}?", + "placeholders": [ + { + "placeholder": "start_date", + "transforms": [ + { + "transform": "format_date", + "arguments": { + "date_to_format": { + "source": "metadata", + "identifier": "ref_p_start_date" + }, + "date_format": "d MMMM yyyy" + } + } + ] + }, + { + "placeholder": "end_date", + "transforms": [ + { + "transform": "format_date", + "arguments": { + "date_to_format": { + "source": "metadata", + "identifier": "ref_p_end_date" + }, + "date_format": "d MMMM yyyy" + } + } + ] + } + ] + }, + "type": "General" + } + }, + { + "type": "Question", + "id": "reporting-date", + "question": { + "id": "projects-checkbox-question", + "title": "What dates will you be reporting for?", + "description": [ + "

If figures are not available for the calendar year 2021, your return should relate to a 12 month business year that ends between 6 April 2021 and 5 April 2022.

" + ], + "type": "DateRange", + "answers": [ + { + "id": "answer-from", + "type": "Date", + "mandatory": true, + "label": "Period from", + "minimum": { + "value": { + "source": "metadata", + "identifier": "ref_p_start_date" + }, + "offset_by": { + "days": -31 + } + } + }, + { + "id": "answer-to", + "type": "Date", + "mandatory": true, + "label": "Period to", + "maximum": { + "value": { + "source": "metadata", + "identifier": "ref_p_end_date" + }, + "offset_by": { + "days": 31 + } + } + } + ], + "guidance": { + "contents": [ + { + "description": "

Only traded for a part of the year?

" + }, + { + "description": "

Please provide figures for the period in which you were trading.

" + }, + { + "description": "

Only commenced trading during 2021?

" + }, + { + "description": "

Your return should cover the period from the commencement of your business until 31 December 2021 or, alternatively, any date up to 5 April 2022.

" + }, + { + "description": "

Ceased trading during 2021?

" + }, + { + "description": "

Your return should cover the period 1 January 2021 to the date you ceased to trade or, alternatively, from the beginning of your last business year up to the cessation date.

" + } + ] + } + } + }, + { + "type": "Question", + "id": "report-radio-second", + "question": { + "description": ["

Your return should relate to the calendar year 2021.

"], + "instruction": ["Select your answer"], + "answers": [ + { + "id": "report-radio-second-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ], + "id": "report-radio-second-question", + "title": { + "text": "Are you sure you are able to report for the calendar month {calendar_start_date} to {calendar_end_date}?", + "placeholders": [ + { + "placeholder": "calendar_start_date", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "identifier": "answer-from", + "source": "answers" + } + ] + } + }, + { + "transform": "format_date", + "arguments": { + "date_to_format": { + "source": "previous_transform" + }, + "date_format": "d MMMM yyyy" + } + } + ] + }, + { + "placeholder": "calendar_end_date", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "identifier": "answer-to", + "source": "answers" + } + ] + } + }, + { + "transform": "format_date", + "arguments": { + "date_to_format": { + "source": "previous_transform" + }, + "date_format": "d MMMM yyyy" + } + } + ] + } + ] + }, + "type": "General" + } + }, + { + "type": "Question", + "id": "projects-checkbox", + "question": { + "answers": [ + { + "id": "projects-checkbox-answer", + "instruction": "Select any answers that apply", + "mandatory": true, + "options": [ + { + "label": "Public sector projects", + "value": "Public sector projects", + "description": "This includes public housing and government owned organisations such as local, regional and national public authorities and agencies" + }, + { + "label": "Private sector projects", + "value": "Private sector projects", + "description": "This refers to the part of the economy that is for profit and is owned by private organisations. For example privately owned businesses, housing associations, partnerships and sole traders, joint ventures and privately owned housing" + } + ], + "type": "Checkbox" + } + ], + "id": "projects-checkbox-question-2", + "title": { + "text": "Which sector did {ru_name} carry out work for?", + "placeholders": [ + { + "placeholder": "ru_name", + "value": { + "source": "metadata", + "identifier": "ru_name" + } + } + ] + }, + "type": "General", + "guidance": { + "contents": [ + { + "description": "Include:" + }, + { + "list": ["Local public authorities and agencies", "Regional and national authorities and agencies"] + } + ] + } + } + }, + { + "type": "Question", + "id": "turnover-variants-block", + "question_variants": [ + { + "question": { + "guidance": { + "contents": [ + { + "description": "Include:" + }, + { + "list": [ + "exports", + "payments for work in progress", + "costs incurred and passed on to customers", + "income from sub-contracted activities", + "commission", + "sales of goods purchased for resale", + "revenue earned from other parts of the business not named, please supply at fair value" + ] + }, + { + "description": "Exclude:" + }, + { + "list": [ + "VAT", + "income from the sale of fixed capital assets", + "grants and subsidies", + "insurance claims", + "interest received" + ] + } + ] + }, + "id": "turnover-variants-question", + "title": "What was your total turnover", + "type": "General", + "answers": [ + { + "id": "turnover-variants-answer", + "mandatory": false, + "type": "TextField", + "label": "Total turnover" + } + ] + }, + "when": { + "==": [ + { + "source": "answers", + "identifier": "report-radio-answer" + }, + "Yes" + ] + } + }, + { + "question": { + "id": "turnover-variants-question", + "title": "Why are you not able to report?", + "type": "General", + "answers": [ + { + "id": "turnover-variants-answer", + "mandatory": false, + "type": "TextField", + "label": "Details" + } + ] + }, + "when": { + "==": [ + { + "source": "answers", + "identifier": "report-radio-answer" + }, + "No" + ] + } + } + ] + }, + { + "type": "Question", + "id": "address-mutually-exclusive-checkbox", + "question": { + "id": "address-mutually-exclusive-checkbox-question", + "type": "MutuallyExclusive", + "title": "Were your company based at any of the following addresses?", + "mandatory": true, + "answers": [ + { + "id": "address-checkbox-answer", + "instruction": "Select an answer", + "type": "Checkbox", + "mandatory": false, + "options": [ + { + "label": { + "placeholders": [ + { + "placeholder": "company_address", + "value": { + "identifier": "display_address", + "source": "metadata" + } + } + ], + "text": "{company_address}" + }, + "value": "{company_address}" + }, + { + "label": "7 Evelyn Street, Barry", + "value": "7 Evelyn Street, Barry" + }, + { + "label": "251 Argae Lane, Barry", + "value": "251 Argae Lane, Barry" + } + ] + }, + { + "id": "address-checkbox-exclusive-answer", + "mandatory": false, + "type": "Checkbox", + "options": [ + { + "label": "I prefer not to say", + "description": "Some description", + "value": "I prefer not to say" + } + ] + } + ] + } + }, + { + "id": "further-details-text-area", + "type": "Question", + "question": { + "id": "further-details-text-area-question", + "title": "Please provide any further details", + "type": "General", + "description": [ + { + "text": "

Answer for {ru_name}

", + "placeholders": [ + { + "placeholder": "ru_name", + "value": { + "source": "metadata", + "identifier": "ru_name" + } + } + ] + } + ], + "answers": [ + { + "id": "further-details-text-area-answer", + "mandatory": false, + "type": "TextArea", + "label": "Comments", + "max_length": 2000 + } + ] + } + }, { "type": "Interstitial", "id": "general-business-information-completed", @@ -246,7 +707,7 @@ "title": "Section complete", "contents": [ { - "description": "

You have successfully completed this section

The next section covers changes in business strategy and practices, for example, implementing changes to marketing concepts or strategies.

" + "description": "

You have successfully completed this section

The next section covers changes in business strategy and practices, for example, implementing changes to marketing concepts or strategies.

" } ] } diff --git a/schemas/test/en/test_introduction_hub.json b/schemas/test/en/test_introduction_hub.json new file mode 100644 index 0000000000..d0e761c80c --- /dev/null +++ b/schemas/test/en/test_introduction_hub.json @@ -0,0 +1,636 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "144", + "theme": "default", + "preview_questions": true, + "title": "Test introduction preview questions with hub", + "legal_basis": "Notice is given under section 999 of the Test Act 2000", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + }, + { + "name": "ru_ref", + "type": "string" + }, + { + "name": "trad_as", + "type": "string", + "optional": true + }, + { + "name": "ref_p_start_date", + "type": "date" + }, + { + "name": "ref_p_end_date", + "type": "date" + }, + { + "name": "display_address", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Hub", + "options": { + "required_completed_sections": ["introduction-section"] + } + }, + "sections": [ + { + "id": "introduction-section", + "title": "Main section", + "summary": { + "page_title": "Summary title", + "show_on_completion": true + }, + "show_on_hub": true, + "groups": [ + { + "id": "introduction-group", + "title": "General Business Information", + "blocks": [ + { + "id": "introduction", + "type": "Introduction", + "primary_content": [ + { + "id": "business-details", + "title": { + "text": "You are completing this for {company_name}", + "placeholders": [ + { + "placeholder": "company_name", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "source": "metadata", + "identifier": "trad_as" + }, + { + "source": "metadata", + "identifier": "ru_name" + } + ] + } + } + ] + } + ] + }, + "contents": [ + { + "description": { + "text": "If the company details or structure have changed contact us on {telephone_number_link} or email {email_link}", + "placeholders": [ + { + "placeholder": "telephone_number_link", + "transforms": [ + { + "transform": "telephone_number_link", + "arguments": { + "telephone_number": "0300 1234 931" + } + } + ] + }, + { + "placeholder": "email_link", + "transforms": [ + { + "transform": "email_link", + "arguments": { + "email_address": "surveys@ons.gov.uk", + "email_subject": "Change of details reference", + "email_subject_append": { + "identifier": "ru_ref", + "source": "metadata" + } + } + } + ] + } + ] + } + }, + { + "guidance": { + "contents": [ + { + "title": "Coronavirus (COVID-19) guidance", + "description": "Explain your figures in the comment section to minimise us contacting you and to help us tell an industry story" + } + ] + } + } + ] + }, + { + "id": "use-of-information", + "contents": [ + { + "list": [ + "Data should relate to all sites in England, Scotland and Wales unless otherwise stated.", + "You can provide informed estimates if actual figures aren’t available.", + "We will treat your data securely and confidentially." + ] + }, + { + "description": "To take part, all you need to do is check that you have the information you need to answer the survey questions." + } + ] + } + ], + "secondary_content": [ + { + "id": "how-we-use-your-data", + "contents": [ + { + "title": "How we use your data", + "list": [ + "You cannot appeal your selection. Your business was selected to give us a comprehensive view of the UK economy", + "The data from you business is essential is it helps us calculate the GDP of the UK", + "Our surveys inform government decisions. For example, past statistics from our surveys led to the introduction of business grants" + ] + } + ] + } + ] + }, + { + "type": "Question", + "id": "report-radio", + "question": { + "guidance": { + "contents": [ + { + "description": "Please provide figures for the period in which you were trading." + } + ] + }, + "description": ["

Your return should relate to the calendar year 2021.

"], + "instruction": ["Select your answer"], + "answers": [ + { + "id": "report-radio-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ], + "id": "report-radio-question", + "title": { + "text": "Are you able to report for the calendar month {start_date} to {end_date}?", + "placeholders": [ + { + "placeholder": "start_date", + "transforms": [ + { + "transform": "format_date", + "arguments": { + "date_to_format": { + "source": "metadata", + "identifier": "ref_p_start_date" + }, + "date_format": "d MMMM yyyy" + } + } + ] + }, + { + "placeholder": "end_date", + "transforms": [ + { + "transform": "format_date", + "arguments": { + "date_to_format": { + "source": "metadata", + "identifier": "ref_p_end_date" + }, + "date_format": "d MMMM yyyy" + } + } + ] + } + ] + }, + "type": "General" + } + }, + { + "type": "Question", + "id": "reporting-date", + "question": { + "id": "projects-checkbox-question", + "title": "What dates will you be reporting for?", + "description": [ + "

If figures are not available for the calendar year 2021, your return should relate to a 12 month business year that ends between 6 April 2021 and 5 April 2022.

" + ], + "type": "DateRange", + "answers": [ + { + "id": "answer-from", + "type": "Date", + "mandatory": true, + "label": "Period from", + "minimum": { + "value": { + "source": "metadata", + "identifier": "ref_p_start_date" + }, + "offset_by": { + "days": -31 + } + } + }, + { + "id": "answer-to", + "type": "Date", + "mandatory": true, + "label": "Period to", + "maximum": { + "value": { + "source": "metadata", + "identifier": "ref_p_end_date" + }, + "offset_by": { + "days": 31 + } + } + } + ], + "guidance": { + "contents": [ + { + "description": "

Only traded for a part of the year?

" + }, + { + "description": "

Please provide figures for the period in which you were trading.

" + }, + { + "description": "

Only commenced trading during 2021?

" + }, + { + "description": "

Your return should cover the period from the commencement of your business until 31 December 2021 or, alternatively, any date up to 5 April 2022.

" + }, + { + "description": "

Ceased trading during 2021?

" + }, + { + "description": "

Your return should cover the period 1 January 2021 to the date you ceased to trade or, alternatively, from the beginning of your last business year up to the cessation date.

" + } + ] + } + } + }, + { + "type": "Question", + "id": "report-radio-second", + "question": { + "description": ["

Your return should relate to the calendar year 2021.

"], + "instruction": ["Select your answer"], + "answers": [ + { + "id": "report-radio-second-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ], + "id": "report-radio-second-question", + "title": { + "text": "Are you sure you are able to report for the calendar month {calendar_start_date} to {calendar_end_date}?", + "placeholders": [ + { + "placeholder": "calendar_start_date", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "identifier": "answer-from", + "source": "answers" + } + ] + } + }, + { + "transform": "format_date", + "arguments": { + "date_to_format": { + "source": "previous_transform" + }, + "date_format": "d MMMM yyyy" + } + } + ] + }, + { + "placeholder": "calendar_end_date", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "identifier": "answer-to", + "source": "answers" + } + ] + } + }, + { + "transform": "format_date", + "arguments": { + "date_to_format": { + "source": "previous_transform" + }, + "date_format": "d MMMM yyyy" + } + } + ] + } + ] + }, + "type": "General" + } + }, + { + "type": "Question", + "id": "projects-checkbox", + "question": { + "answers": [ + { + "id": "projects-checkbox-answer", + "instruction": "Select any answers that apply", + "mandatory": true, + "options": [ + { + "label": "Public sector projects", + "value": "Public sector projects", + "description": "This includes public housing and government owned organisations such as local, regional and national public authorities and agencies" + }, + { + "label": "Private sector projects", + "value": "Private sector projects", + "description": "This refers to the part of the economy that is for profit and is owned by private organisations. For example privately owned businesses, housing associations, partnerships and sole traders, joint ventures and privately owned housing" + } + ], + "type": "Checkbox" + } + ], + "id": "projects-checkbox-question-2", + "title": { + "text": "Which sector did {ru_name} carry out work for?", + "placeholders": [ + { + "placeholder": "ru_name", + "value": { + "source": "metadata", + "identifier": "ru_name" + } + } + ] + }, + "type": "General", + "guidance": { + "contents": [ + { + "description": "Include:" + }, + { + "list": ["Local public authorities and agencies", "Regional and national authorities and agencies"] + } + ] + } + } + }, + { + "type": "Question", + "id": "turnover-variants-block", + "question_variants": [ + { + "question": { + "guidance": { + "contents": [ + { + "description": "Include:" + }, + { + "list": [ + "exports", + "payments for work in progress", + "costs incurred and passed on to customers", + "income from sub-contracted activities", + "commission", + "sales of goods purchased for resale", + "revenue earned from other parts of the business not named, please supply at fair value" + ] + }, + { + "description": "Exclude:" + }, + { + "list": [ + "VAT", + "income from the sale of fixed capital assets", + "grants and subsidies", + "insurance claims", + "interest received" + ] + } + ] + }, + "id": "turnover-variants-question", + "title": "What was your total turnover", + "type": "General", + "answers": [ + { + "id": "turnover-variants-answer", + "mandatory": false, + "type": "TextField", + "label": "Total turnover" + } + ] + }, + "when": { + "==": [ + { + "source": "answers", + "identifier": "report-radio-answer" + }, + "Yes" + ] + } + }, + { + "question": { + "id": "turnover-variants-question", + "title": "Why are you not able to report?", + "type": "General", + "answers": [ + { + "id": "turnover-variants-answer", + "mandatory": false, + "type": "TextField", + "label": "Details" + } + ] + }, + "when": { + "==": [ + { + "source": "answers", + "identifier": "report-radio-answer" + }, + "No" + ] + } + } + ] + }, + { + "type": "Question", + "id": "address-mutually-exclusive-checkbox", + "question": { + "id": "address-mutually-exclusive-checkbox-question", + "type": "MutuallyExclusive", + "title": "Were your company based at any of the following addresses?", + "mandatory": true, + "answers": [ + { + "id": "address-checkbox-answer", + "instruction": "Select an answer", + "type": "Checkbox", + "mandatory": false, + "options": [ + { + "label": { + "placeholders": [ + { + "placeholder": "company_address", + "value": { + "identifier": "display_address", + "source": "metadata" + } + } + ], + "text": "{company_address}" + }, + "value": "{company_address}" + }, + { + "label": "7 Evelyn Street, Barry", + "value": "7 Evelyn Street, Barry" + }, + { + "label": "251 Argae Lane, Barry", + "value": "251 Argae Lane, Barry" + } + ] + }, + { + "id": "address-checkbox-exclusive-answer", + "mandatory": false, + "type": "Checkbox", + "options": [ + { + "label": "I prefer not to say", + "description": "Some description", + "value": "I prefer not to say" + } + ] + } + ] + } + } + ] + } + ] + }, + { + "id": "additional-section", + "title": "Additional section", + "summary": { + "page_title": "Summary title", + "show_on_completion": true + }, + "show_on_hub": true, + "groups": [ + { + "id": "additional-group", + "title": "Additional Business Information", + "blocks": [ + { + "id": "further-details-text-area", + "type": "Question", + "question": { + "id": "further-details-text-area-question", + "title": "Please provide any further details", + "type": "General", + "description": [ + { + "text": "

Answer for {ru_name}

", + "placeholders": [ + { + "placeholder": "ru_name", + "value": { + "source": "metadata", + "identifier": "ru_name" + } + } + ] + } + ], + "answers": [ + { + "id": "further-details-text-area-answer", + "mandatory": false, + "type": "TextArea", + "label": "Comments", + "max_length": 2000 + } + ] + } + } + ] + } + ] + } + ] +} diff --git a/schemas/test/en/test_last_viewed_question_guidance.json b/schemas/test/en/test_last_viewed_question_guidance.json index 478e1db2ed..7b6b183db1 100644 --- a/schemas/test/en/test_last_viewed_question_guidance.json +++ b/schemas/test/en/test_last_viewed_question_guidance.json @@ -171,39 +171,46 @@ }, "routing_rules": [ { - "goto": { - "section": "End", - "when": [ + "section": "End", + "when": { + "and": [ { - "condition": "equals", - "id": "anyone-usually-live-at-answer", - "value": "No" + "==": [ + { + "identifier": "anyone-usually-live-at-answer", + "source": "answers" + }, + "No" + ] }, { - "condition": "less than", - "list": "people", - "value": 1 + "<": [ + { + "source": "list", + "identifier": "people", + "selector": "count" + }, + 1 + ] } ] } }, { - "goto": { - "block": "list-collector" - } + "block": "list-collector" } ], - "skip_conditions": [ - { - "when": [ + "skip_conditions": { + "when": { + "==": [ { - "condition": "equals", - "id": "you-live-here", - "value": "Yes" - } + "identifier": "you-live-here", + "source": "answers" + }, + "Yes" ] } - ], + }, "type": "Question" }, { @@ -388,7 +395,7 @@ "id": "relationship-question", "type": "General", "title": { - "text": "Thinking of {first_person_name}, {second_person_name} is their â€Ļ", + "text": "Thinking of {first_person_name}, {second_person_name} is their â€Ļ", "placeholders": [ { "placeholder": "first_person_name", @@ -456,7 +463,7 @@ "mandatory": true, "type": "Relationship", "playback": { - "text": "{second_person_name} is {first_person_name_possessive} â€Ļ", + "text": "{second_person_name} is {first_person_name_possessive} â€Ļ", "placeholders": [ { "placeholder": "first_person_name_possessive", @@ -531,7 +538,7 @@ "label": "Husband or Wife", "value": "Husband or Wife", "title": { - "text": "Thinking of {first_person_name}, {second_person_name} is their husband or wife", + "text": "Thinking of {first_person_name}, {second_person_name} is their husband or wife", "placeholders": [ { "placeholder": "first_person_name", @@ -594,7 +601,7 @@ ] }, "playback": { - "text": "{second_person_name} is {first_person_name_possessive} husband or wife", + "text": "{second_person_name} is {first_person_name_possessive} husband or wife", "placeholders": [ { "placeholder": "first_person_name_possessive", @@ -669,7 +676,7 @@ "label": "Son or daughter", "value": "Son or daughter", "title": { - "text": "Thinking of {first_person_name}, {second_person_name} is their son or daughter", + "text": "Thinking of {first_person_name}, {second_person_name} is their son or daughter", "placeholders": [ { "placeholder": "first_person_name", @@ -732,7 +739,7 @@ ] }, "playback": { - "text": "{second_person_name} is {first_person_name_possessive} son or daughter", + "text": "{second_person_name} is {first_person_name_possessive} son or daughter", "placeholders": [ { "placeholder": "first_person_name_possessive", @@ -807,7 +814,7 @@ "label": "Brother or sister", "value": "Brother or sister", "title": { - "text": "Thinking of {first_person_name}, {second_person_name} is their brother or sister", + "text": "Thinking of {first_person_name}, {second_person_name} is their brother or sister", "placeholders": [ { "placeholder": "first_person_name", @@ -870,7 +877,7 @@ ] }, "playback": { - "text": "{second_person_name} is {first_person_name_possessive} brother or sister", + "text": "{second_person_name} is {first_person_name_possessive} brother or sister", "placeholders": [ { "placeholder": "first_person_name_possessive", @@ -946,17 +953,18 @@ } ] }, - "skip_conditions": [ - { - "when": [ + "skip_conditions": { + "when": { + "<": [ { - "list": "people", - "condition": "less than", - "value": 2 - } + "source": "list", + "identifier": "people", + "selector": "count" + }, + 2 ] } - ] + } } ], "id": "relationship-group", diff --git a/schemas/test/en/test_list_change_evaluates_sections.json b/schemas/test/en/test_list_change_evaluates_sections.json index 17c1f926bc..fe038d1578 100644 --- a/schemas/test/en/test_list_change_evaluates_sections.json +++ b/schemas/test/en/test_list_change_evaluates_sections.json @@ -238,7 +238,9 @@ ] }, { - "summary": { "show_on_completion": true }, + "summary": { + "show_on_completion": true + }, "groups": [ { "blocks": [ @@ -295,21 +297,20 @@ }, "routing_rules": [ { - "goto": { - "section": "End", - "when": [ + "section": "End", + "when": { + "==": [ { - "condition": "equals", - "list": "people", - "value": 0 - } + "source": "list", + "identifier": "people", + "selector": "count" + }, + 0 ] } }, { - "goto": { - "block": "own-or-rent" - } + "block": "own-or-rent" } ], "type": "Question" diff --git a/schemas/test/en/test_list_collector_content_page.json b/schemas/test/en/test_list_collector_content_page.json new file mode 100644 index 0000000000..d31345c3ef --- /dev/null +++ b/schemas/test/en/test_list_collector_content_page.json @@ -0,0 +1,550 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Test List Collector Section Summary Items", + "theme": "default", + "description": "A questionnaire to test list collector section summary items", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Hub", + "options": { + "required_completed_sections": ["section-companies"] + } + }, + "post_submission": { + "view_response": true + }, + "sections": [ + { + "id": "section-companies", + "title": "General insurance business", + "summary": { + "show_on_completion": true, + "items": [ + { + "type": "List", + "for_list": "companies", + "title": "Companies or UK branches", + "item_anchor_answer_id": "company-or-branch-name", + "item_label": "Name of UK company or branch", + "add_link_text": "Add another UK company or branch", + "empty_list_text": "No UK company or branch added", + "related_answers": [ + { + "source": "answers", + "identifier": "registration-number" + }, + { + "source": "answers", + "identifier": "authorised-insurer-radio" + } + ] + } + ], + "show_non_item_answers": true + }, + "groups": [ + { + "id": "group-companies", + "blocks": [ + { + "type": "ListCollectorDrivingQuestion", + "id": "any-companies-or-branches", + "for_list": "companies", + "question": { + "type": "General", + "id": "any-companies-or-branches-question", + "title": "Do any companies or branches within your United Kingdom group undertake general insurance business?", + "answers": [ + { + "type": "Radio", + "id": "any-companies-or-branches-answer", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock", + "params": { + "block_id": "add-company", + "list_name": "companies" + } + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "routing_rules": [ + { + "when": { + "==": [ + { + "source": "answers", + "identifier": "any-companies-or-branches-answer" + }, + "No" + ] + }, + "block": "confirmation-checkbox" + }, + { + "block": "any-other-companies-or-branches" + } + ] + }, + { + "id": "any-other-companies-or-branches", + "type": "ListCollector", + "for_list": "companies", + "question": { + "id": "any-other-companies-or-branches-question", + "type": "General", + "title": "Do you need to add any other UK companies or branches that undertake general insurance business?", + "answers": [ + { + "id": "any-other-companies-or-branches-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "add_block": { + "id": "add-company", + "type": "ListAddQuestion", + "question": { + "id": "add-question-companies", + "type": "General", + "title": "Give details about the company or branch that undertakes general insurance business", + "answers": [ + { + "id": "company-or-branch-name", + "label": "Name of UK company or branch", + "mandatory": true, + "type": "TextField" + }, + { + "id": "registration-number", + "label": "Registration number", + "mandatory": true, + "type": "Number", + "maximum": { + "value": 999, + "exclusive": false + }, + "decimal_places": 0 + }, + { + "type": "Radio", + "label": "Is this UK company or branch an authorised insurer?", + "id": "authorised-insurer-radio", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + "edit_block": { + "id": "edit-company", + "type": "ListEditQuestion", + "question": { + "id": "edit-question-companies", + "type": "General", + "title": "What is the name of the company?", + "answers": [ + { + "id": "company-or-branch-name", + "label": "Name of UK company or branch", + "mandatory": true, + "type": "TextField" + }, + { + "id": "registration-number", + "label": "Registration number", + "mandatory": true, + "type": "Number" + }, + { + "type": "Radio", + "label": "Is this UK company or branch an authorised insurer?", + "id": "authorised-insurer-radio", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + "remove_block": { + "id": "remove-company", + "type": "ListRemoveQuestion", + "question": { + "id": "remove-question-companies", + "type": "General", + "title": "Are you sure you want to remove this company or UK branch?", + "answers": [ + { + "id": "remove-confirmation", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RemoveListItemAndAnswers" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + "summary": { + "title": "Companies or UK branches", + "item_title": { + "text": "{company_name}", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "answers", + "identifier": "company-or-branch-name" + } + } + ] + } + } + }, + { + "type": "Question", + "id": "confirmation-checkbox", + "question": { + "answers": [ + { + "id": "confirmation-checkbox-answer", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ], + "type": "Radio" + } + ], + "id": "confirmation-checkbox-question", + "title": "Are all companies or branches based in UK?", + "type": "General" + }, + "skip_conditions": { + "when": { + "!=": [ + { + "count": [ + { + "source": "list", + "identifier": "companies" + } + ] + }, + 3 + ] + } + } + } + ] + } + ] + }, + { + "id": "section-list-collector-contents", + "title": "List Collector Contents", + "summary": { + "show_on_completion": true, + "items": [ + { + "type": "List", + "for_list": "companies", + "title": "Companies or UK branches", + "item_label": "Name of UK company or branch" + } + ], + "show_non_item_answers": true + }, + "groups": [ + { + "id": "group-list-collector-contents", + "title": "Companies", + "blocks": [ + { + "type": "Question", + "id": "responsible-party", + "question": { + "type": "General", + "id": "responsible-party-question", + "title": "Are you the responsible party for reporting trading details for a company of branch?", + "answers": [ + { + "type": "Radio", + "id": "responsible-party-answer", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "routing_rules": [ + { + "block": "list-collector-content", + "when": { + "==": [ + "Yes", + { + "source": "answers", + "identifier": "responsible-party-answer" + } + ] + } + }, + { + "section": "End" + } + ] + }, + { + "id": "list-collector-content", + "type": "ListCollectorContent", + "page_title": "Companies", + "for_list": "companies", + "summary": { + "title": "Companies or UK branches", + "item_title": { + "text": "{company_name}", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "answers", + "identifier": "company-or-branch-name" + } + } + ] + } + }, + "content": { + "title": "Companies", + "contents": [ + { + "guidance": { + "contents": [ + { + "description": "Include all companies" + } + ] + } + }, + { + "definition": { + "title": "Companies definition", + "contents": [ + { + "description": "Legal entities formed by a group of individuals to engage in and operate a business enterprise in a commercial or industrial capacity." + } + ] + } + }, + { + "description": "You have previously reported the following companies. Press continue to updated registration and trading information." + } + ] + }, + "repeating_blocks": [ + { + "id": "companies-repeating-block-1", + "type": "ListRepeatingQuestion", + "question": { + "id": "companies-repeating-block-1-question", + "type": "General", + "title": { + "text": "Give details about {company_name}", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "answers", + "identifier": "company-or-branch-name" + } + } + ] + }, + "answers": [ + { + "id": "registration-number-repeating-block", + "label": "Registration number (Mandatory)", + "mandatory": true, + "type": "Number", + "maximum": { + "value": 999, + "exclusive": false + }, + "decimal_places": 0 + }, + { + "id": "registration-date-repeating-block", + "label": "Date of Registration (Mandatory)", + "mandatory": true, + "type": "Date", + "maximum": { + "value": "now" + } + } + ] + } + }, + { + "id": "companies-repeating-block-2", + "type": "ListRepeatingQuestion", + "question": { + "id": "companies-repeating-block-2-question", + "type": "General", + "title": { + "text": "Give details about how {company_name} has been trading over the past {date_difference}.", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "answers", + "identifier": "company-or-branch-name" + } + }, + { + "placeholder": "date_difference", + "transforms": [ + { + "transform": "calculate_date_difference", + "arguments": { + "first_date": { + "source": "answers", + "identifier": "registration-date-repeating-block" + }, + "second_date": { + "value": "now" + } + } + } + ] + } + ] + }, + "answers": [ + { + "type": "Radio", + "label": "Has this company been trading in the UK? (Mandatory)", + "id": "authorised-trader-uk-radio-repeating-block", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + }, + { + "type": "Radio", + "label": "Has this company been trading in the EU? (Not mandatory)", + "id": "authorised-trader-eu-radio-repeating-block", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + } + ] + } + ] + } + ] + } + ] +} diff --git a/schemas/test/en/test_list_collector_driving_checkbox.json b/schemas/test/en/test_list_collector_driving_checkbox.json index d10bb8e63c..6637a42efb 100644 --- a/schemas/test/en/test_list_collector_driving_checkbox.json +++ b/schemas/test/en/test_list_collector_driving_checkbox.json @@ -154,13 +154,15 @@ } ] }, - "when": [ - { - "id": "you-live-here", - "condition": "equals", - "value": "Yes, I usually live here" - } - ] + "when": { + "==": [ + { + "identifier": "you-live-here", + "source": "answers" + }, + "Yes, I usually live here" + ] + } }, { "question": { @@ -212,44 +214,44 @@ } ] }, - "when": [ - { - "id": "you-live-here", - "condition": "equals", - "value": "No, I don’t usually live here" - } - ] + "when": { + "==": [ + { + "identifier": "you-live-here", + "source": "answers" + }, + "No, I don’t usually live here" + ] + } } ], "routing_rules": [ { - "goto": { - "block": "list-collector-temporary-away-stay", - "when": [ + "block": "list-collector-temporary-away-stay", + "when": { + "in": [ + "None of these apply, no-one usually lives here", { - "id": "anyone-usually-live-at-answer-exclusive", - "condition": "contains", - "value": "None of these apply, no-one usually lives here" + "identifier": "anyone-usually-live-at-answer-exclusive", + "source": "answers" } ] } }, { - "goto": { - "block": "list-collector-temporary-away-stay", - "when": [ + "block": "list-collector-temporary-away-stay", + "when": { + "in": [ + "None of the these apply, I am the only person who usually lives here", { - "id": "anyone-usually-live-at-answer-exclusive", - "condition": "contains", - "value": "None of the these apply, I am the only person who usually lives here" + "identifier": "anyone-usually-live-at-answer-exclusive", + "source": "answers" } ] } }, { - "goto": { - "block": "list-collector" - } + "block": "list-collector" } ] }, @@ -306,13 +308,16 @@ } ] }, - "when": [ - { - "condition": "equals", - "list": "people", - "value": 0 - } - ] + "when": { + "==": [ + { + "source": "list", + "identifier": "people", + "selector": "count" + }, + 0 + ] + } }, { "question": { @@ -334,13 +339,16 @@ } ] }, - "when": [ - { - "condition": "greater than", - "list": "people", - "value": 0 - } - ] + "when": { + ">": [ + { + "source": "list", + "identifier": "people", + "selector": "count" + }, + 0 + ] + } } ] }, @@ -539,13 +547,16 @@ } ] }, - "when": [ - { - "condition": "equals", - "list": "people", - "value": 0 - } - ] + "when": { + "==": [ + { + "source": "list", + "identifier": "people", + "selector": "count" + }, + 0 + ] + } }, { "question": { @@ -567,13 +578,16 @@ } ] }, - "when": [ - { - "condition": "greater than", - "list": "people", - "value": 0 - } - ] + "when": { + ">": [ + { + "source": "list", + "identifier": "people", + "selector": "count" + }, + 0 + ] + } } ] }, diff --git a/schemas/test/en/test_list_collector_driving_question.json b/schemas/test/en/test_list_collector_driving_question.json index 20514e00f7..9046699d67 100644 --- a/schemas/test/en/test_list_collector_driving_question.json +++ b/schemas/test/en/test_list_collector_driving_question.json @@ -93,21 +93,19 @@ }, "routing_rules": [ { - "goto": { - "section": "End", - "when": [ + "section": "End", + "when": { + "==": [ { - "id": "anyone-usually-live-at-answer", - "condition": "equals", - "value": "No" - } + "source": "answers", + "identifier": "anyone-usually-live-at-answer" + }, + "No" ] } }, { - "goto": { - "block": "anyone-else-live-at" - } + "block": "anyone-else-live-at" } ] }, diff --git a/schemas/test/en/test_list_collector_list_summary.json b/schemas/test/en/test_list_collector_list_summary.json new file mode 100644 index 0000000000..b07504fe6e --- /dev/null +++ b/schemas/test/en/test_list_collector_list_summary.json @@ -0,0 +1,528 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Test ListCollector", + "preview_questions": true, + "theme": "default", + "description": "A questionnaire to test ListCollector", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + }, + { + "name": "display_address", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Linear", + "options": {} + }, + "individual_response": { + "for_list": "people", + "individual_section_id": "section" + }, + "sections": [ + { + "id": "section", + "title": "People who live here and overnight visitors", + "summary": { + "show_on_completion": true, + "items": [ + { + "type": "List", + "for_list": "people", + "title": { + "text": "Household members staying overnight on {date} at {household_address}", + "placeholders": [ + { + "placeholder": "date", + "transforms": [ + { + "arguments": { + "date_format": "d MMMM yyyy", + "date_to_format": { + "value": "2019-10-13" + } + }, + "transform": "format_date" + } + ] + }, + { + "placeholder": "household_address", + "value": { + "identifier": "display_address", + "source": "metadata" + } + } + ] + }, + "add_link_text": "Add someone to this household", + "empty_list_text": "There are no householders" + }, + { + "type": "List", + "for_list": "visitors", + "title": { + "text": "Visitors staying overnight on {date} at {household_address}", + "placeholders": [ + { + "placeholder": "date", + "transforms": [ + { + "arguments": { + "date_format": "d MMMM yyyy", + "date_to_format": { + "value": "2019-10-13" + } + }, + "transform": "format_date" + } + ] + }, + { + "placeholder": "household_address", + "value": { + "identifier": "display_address", + "source": "metadata" + } + } + ] + }, + "add_link_text": "Add another visitor to this household", + "empty_list_text": "There are no visitors" + } + ] + }, + "groups": [ + { + "id": "group", + "title": "Questions", + "blocks": [ + { + "id": "introduction", + "type": "Introduction" + }, + { + "id": "primary-person-list-collector", + "type": "PrimaryPersonListCollector", + "for_list": "people", + "add_or_edit_block": { + "id": "add-or-edit-primary-person", + "type": "PrimaryPersonListAddOrEditQuestion", + "question": { + "id": "primary-person-add-or-edit-question", + "type": "General", + "title": "What is your name?", + "answers": [ + { + "id": "first-name", + "label": "First name", + "mandatory": true, + "type": "TextField" + }, + { + "id": "last-name", + "label": "Last name", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "question": { + "id": "primary-confirmation-question", + "type": "General", + "title": { + "placeholders": [ + { + "placeholder": "household_address", + "value": { + "identifier": "display_address", + "source": "metadata" + } + } + ], + "text": "Do you live at {household_address}?" + }, + "answers": [ + { + "id": "you-live-here", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + { + "id": "list-collector", + "type": "ListCollector", + "for_list": "people", + "question": { + "id": "confirmation-question", + "type": "General", + "title": { + "placeholders": [ + { + "placeholder": "household_address", + "value": { + "identifier": "display_address", + "source": "metadata" + } + } + ], + "text": "Does anyone else live at {household_address}?" + }, + "answers": [ + { + "id": "anyone-else", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "add_block": { + "id": "add-person", + "type": "ListAddQuestion", + "question": { + "id": "add-question", + "type": "General", + "title": "What is the name of the person?", + "answers": [ + { + "id": "first-name", + "label": "First name", + "mandatory": true, + "type": "TextField" + }, + { + "id": "last-name", + "label": "Last name", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "edit_block": { + "id": "edit-person", + "type": "ListEditQuestion", + "question": { + "id": "edit-question", + "type": "General", + "title": "What is the name of the person?", + "answers": [ + { + "id": "first-name", + "label": "First name", + "mandatory": true, + "type": "TextField" + }, + { + "id": "last-name", + "label": "Last name", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "remove_block": { + "id": "remove-person", + "type": "ListRemoveQuestion", + "question": { + "id": "remove-question", + "type": "General", + "title": "Are you sure you want to remove this person?", + "answers": [ + { + "id": "remove-confirmation", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RemoveListItemAndAnswers" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + "summary": { + "title": { + "text": "Household members staying overnight on {date} at {household_address}", + "placeholders": [ + { + "placeholder": "date", + "transforms": [ + { + "arguments": { + "date_format": "d MMMM yyyy", + "date_to_format": { + "value": "2019-10-13" + } + }, + "transform": "format_date" + } + ] + }, + { + "placeholder": "household_address", + "value": { + "identifier": "display_address", + "source": "metadata" + } + } + ] + }, + "item_title": { + "text": "{person_name}", + "placeholders": [ + { + "placeholder": "person_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "first-name" + }, + { + "source": "answers", + "identifier": "last-name" + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + } + } + }, + { + "id": "visitor-list-collector", + "type": "ListCollector", + "for_list": "visitors", + "question": { + "id": "confirmation-visitor-question", + "type": "General", + "title": { + "placeholders": [ + { + "placeholder": "household_address", + "value": { + "identifier": "display_address", + "source": "metadata" + } + } + ], + "text": "Are there any other visitors staying overnight at {household_address}?" + }, + "answers": [ + { + "id": "any-more-visitors", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "add_block": { + "id": "add-visitor", + "type": "ListAddQuestion", + "question": { + "id": "add-visitor-question", + "type": "General", + "title": "What is the name of the visitor?", + "answers": [ + { + "id": "first-name-visitor", + "label": "First name", + "mandatory": true, + "type": "TextField" + }, + { + "id": "last-name-visitor", + "label": "Last name", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "edit_block": { + "id": "edit-visitor-person", + "type": "ListEditQuestion", + "question": { + "id": "edit-visitor-question", + "type": "General", + "title": "What is the name of the person?", + "answers": [ + { + "id": "first-name-visitor", + "label": "First name", + "mandatory": true, + "type": "TextField" + }, + { + "id": "last-name-visitor", + "label": "Last name", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "remove_block": { + "id": "remove-visitor", + "type": "ListRemoveQuestion", + "question": { + "id": "remove-visitor-question", + "type": "General", + "title": "Are you sure you want to remove this person?", + "answers": [ + { + "id": "remove-visitor-confirmation", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RemoveListItemAndAnswers" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + "summary": { + "title": { + "text": "Visitors staying overnight on {date} at {household_address}", + "placeholders": [ + { + "placeholder": "date", + "transforms": [ + { + "arguments": { + "date_format": "d MMMM yyyy", + "date_to_format": { + "value": "2019-10-13" + } + }, + "transform": "format_date" + } + ] + }, + { + "placeholder": "household_address", + "value": { + "identifier": "display_address", + "source": "metadata" + } + } + ] + }, + "item_title": { + "text": "{visitor_name}", + "placeholders": [ + { + "placeholder": "visitor_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "first-name-visitor" + }, + { + "source": "answers", + "identifier": "last-name-visitor" + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + } + } + } + ] + } + ] + } + ] +} diff --git a/schemas/test/en/test_list_collector_primary_and_collector_with_driving_question.json b/schemas/test/en/test_list_collector_primary_and_collector_with_driving_question.json index af119748a6..5b47d6e0d5 100644 --- a/schemas/test/en/test_list_collector_primary_and_collector_with_driving_question.json +++ b/schemas/test/en/test_list_collector_primary_and_collector_with_driving_question.json @@ -146,21 +146,19 @@ }, "routing_rules": [ { - "goto": { - "section": "End", - "when": [ + "section": "End", + "when": { + "==": [ { - "id": "anyone-else-usually-live-at-answer", - "condition": "equals", - "value": "No" - } + "source": "answers", + "identifier": "anyone-else-usually-live-at-answer" + }, + "No" ] } }, { - "goto": { - "block": "anyone-else-live-at" - } + "block": "anyone-else-live-at" } ] }, diff --git a/schemas/test/en/test_list_collector_primary_person.json b/schemas/test/en/test_list_collector_primary_person.json index dcf177c54c..1ada4c69aa 100644 --- a/schemas/test/en/test_list_collector_primary_person.json +++ b/schemas/test/en/test_list_collector_primary_person.json @@ -139,39 +139,46 @@ }, "routing_rules": [ { - "goto": { - "section": "End", - "when": [ + "section": "End", + "when": { + "and": [ { - "condition": "equals", - "id": "anyone-usually-live-at-answer", - "value": "No" + "==": [ + { + "source": "answers", + "identifier": "anyone-usually-live-at-answer" + }, + "No" + ] }, { - "condition": "less than", - "list": "people", - "value": 1 + "<": [ + { + "source": "list", + "identifier": "people", + "selector": "count" + }, + 1 + ] } ] } }, { - "goto": { - "block": "list-collector" - } + "block": "list-collector" } ], - "skip_conditions": [ - { - "when": [ + "skip_conditions": { + "when": { + "==": [ { - "condition": "equals", - "id": "you-live-here", - "value": "Yes" - } + "source": "answers", + "identifier": "you-live-here" + }, + "Yes" ] } - ], + }, "type": "Question" }, { diff --git a/schemas/test/en/test_list_collector_repeating_blocks_section_summary.json b/schemas/test/en/test_list_collector_repeating_blocks_section_summary.json new file mode 100644 index 0000000000..c43fe8b93e --- /dev/null +++ b/schemas/test/en/test_list_collector_repeating_blocks_section_summary.json @@ -0,0 +1,432 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Test a List Collector with Repeating Blocks and Section Summary Items", + "theme": "default", + "description": "A questionnaire to test a list collector with repeating blocks and section summary items", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "answer_codes": [ + { + "answer_id": "responsible-party-answer", + "code": "1" + }, + { + "answer_id": "any-companies-or-branches-answer", + "code": "2" + }, + { + "answer_id": "company-or-branch-name", + "code": "2a" + }, + { + "answer_id": "registration-number", + "code": "2b" + }, + { + "answer_id": "registration-date", + "code": "2c" + }, + { + "answer_id": "authorised-trader-uk-radio", + "code": "2d" + }, + { + "answer_id": "authorised-trader-eu-radio", + "code": "2e" + }, + { + "answer_id": "any-other-companies-or-branches-answer", + "code": "3" + }, + { + "answer_id": "any-other-trading-details-answer", + "code": "4" + } + ], + "questionnaire_flow": { + "type": "Linear", + "options": { + "summary": { + "collapsible": false + } + } + }, + "sections": [ + { + "id": "section-companies", + "title": "General insurance business", + "summary": { + "show_on_completion": true, + "items": [ + { + "type": "List", + "for_list": "companies", + "title": "Companies or UK branches", + "item_anchor_answer_id": "company-or-branch-name", + "item_label": "Name of UK company or branch", + "add_link_text": "Add another UK company or branch", + "empty_list_text": "No UK company or branch added" + } + ], + "show_non_item_answers": true + }, + "groups": [ + { + "id": "group-companies", + "blocks": [ + { + "type": "Question", + "id": "responsible-party", + "question": { + "type": "General", + "id": "responsible-party-question", + "title": "Are you the responsible party for reporting trading details for a company of branch?", + "answers": [ + { + "type": "Radio", + "id": "responsible-party-answer", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "routing_rules": [ + { + "block": "any-companies-or-branches", + "when": { + "==": [ + "Yes", + { + "source": "answers", + "identifier": "responsible-party-answer" + } + ] + } + }, + { + "section": "End" + } + ] + }, + { + "type": "ListCollectorDrivingQuestion", + "id": "any-companies-or-branches", + "for_list": "companies", + "question": { + "type": "General", + "id": "any-companies-or-branches-question", + "title": "Do any companies or branches within your United Kingdom group undertake general insurance business?", + "answers": [ + { + "type": "Radio", + "id": "any-companies-or-branches-answer", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock", + "params": { + "block_id": "add-company", + "list_name": "companies" + } + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "routing_rules": [ + { + "when": { + "==": [ + { + "source": "answers", + "identifier": "any-companies-or-branches-answer" + }, + "Yes" + ] + }, + "block": "any-other-companies-or-branches" + }, + { + "section": "End" + } + ] + }, + { + "id": "any-other-companies-or-branches", + "type": "ListCollector", + "for_list": "companies", + "question": { + "id": "any-other-companies-or-branches-question", + "type": "General", + "title": "Do you need to add any other UK companies or branches that undertake general insurance business?", + "answers": [ + { + "id": "any-other-companies-or-branches-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "add_block": { + "id": "add-company", + "type": "ListAddQuestion", + "question": { + "id": "add-question-companies", + "type": "General", + "title": "What is the name and registration number of the company?", + "answers": [ + { + "id": "company-or-branch-name", + "label": "Name of UK company or branch (Mandatory)", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "repeating_blocks": [ + { + "id": "companies-repeating-block-1", + "type": "ListRepeatingQuestion", + "question": { + "id": "companies-repeating-block-1-question", + "type": "General", + "title": { + "text": "Give details about {company_name}", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "answers", + "identifier": "company-or-branch-name" + } + } + ] + }, + "answers": [ + { + "id": "registration-number", + "label": "Registration number (Mandatory)", + "mandatory": true, + "type": "Number", + "maximum": { + "value": 999, + "exclusive": false + }, + "decimal_places": 0 + }, + { + "id": "registration-date", + "label": "Date of Registration (Mandatory)", + "mandatory": true, + "type": "Date", + "maximum": { + "value": "now" + } + } + ] + } + }, + { + "id": "companies-repeating-block-2", + "type": "ListRepeatingQuestion", + "question": { + "id": "companies-repeating-block-2-question", + "type": "General", + "title": { + "text": "Give details about how {company_name} has been trading over the past {date_difference}.", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "answers", + "identifier": "company-or-branch-name" + } + }, + { + "placeholder": "date_difference", + "transforms": [ + { + "transform": "calculate_date_difference", + "arguments": { + "first_date": { + "source": "answers", + "identifier": "registration-date" + }, + "second_date": { + "value": "now" + } + } + } + ] + } + ] + }, + "answers": [ + { + "type": "Radio", + "label": "Has this company been trading in the UK? (Mandatory)", + "id": "authorised-trader-uk-radio", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + }, + { + "type": "Radio", + "label": "Has this company been trading in the EU? (Not mandatory)", + "id": "authorised-trader-eu-radio", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + } + ], + "edit_block": { + "id": "edit-company", + "type": "ListEditQuestion", + "question": { + "id": "edit-question-companies", + "type": "General", + "title": "What is the name and registration number of the company?", + "answers": [ + { + "id": "company-or-branch-name", + "label": "Name of UK company or branch (Mandatory)", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "remove_block": { + "id": "remove-company", + "type": "ListRemoveQuestion", + "question": { + "id": "remove-question-companies", + "type": "General", + "title": "Are you sure you want to remove this company or UK branch?", + "answers": [ + { + "id": "remove-confirmation", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RemoveListItemAndAnswers" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + "summary": { + "title": "Companies or UK branches", + "item_title": { + "text": "{company_name}", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "answers", + "identifier": "company-or-branch-name" + } + } + ] + } + } + }, + { + "id": "any-other-trading-details", + "type": "Question", + "question": { + "id": "any-other-trading-details-question", + "type": "General", + "title": "Do you have any other details about the trading you have reported for?", + "answers": [ + { + "id": "any-other-trading-details-answer", + "label": "Additional details", + "mandatory": false, + "type": "TextField" + } + ] + } + } + ] + } + ] + } + ] +} diff --git a/schemas/test/en/test_list_collector_repeating_blocks_with_hub.json b/schemas/test/en/test_list_collector_repeating_blocks_with_hub.json new file mode 100644 index 0000000000..9eff68c1a7 --- /dev/null +++ b/schemas/test/en/test_list_collector_repeating_blocks_with_hub.json @@ -0,0 +1,826 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Test a List Collector with Repeating Blocks and Section Summary Items", + "theme": "default", + "description": "A questionnaire to test a list collector with repeating blocks and section summary items", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "answer_codes": [ + { + "answer_id": "responsible-party-answer", + "code": "1" + }, + { + "answer_id": "any-companies-or-branches-answer", + "code": "2" + }, + { + "answer_id": "company-or-branch-name", + "code": "2a" + }, + { + "answer_id": "registration-number", + "code": "2b" + }, + { + "answer_id": "registration-date", + "code": "2c" + }, + { + "answer_id": "authorised-trader-uk-radio", + "code": "2d" + }, + { + "answer_id": "authorised-trader-eu-radio", + "code": "2e" + }, + { + "answer_id": "any-other-companies-or-branches-answer", + "code": "3" + }, + { + "answer_id": "any-other-trading-details-answer", + "code": "4" + }, + { + "answer_id": "responsible-party-business-answer", + "code": "5" + }, + { + "answer_id": "any-businesses-or-branches-answer", + "code": "6" + }, + { + "answer_id": "business-or-branch-name", + "code": "6a" + }, + { + "answer_id": "registration-business-number", + "code": "6b" + }, + { + "answer_id": "registration-business-date", + "code": "6c" + }, + { + "answer_id": "authorised-business-trader-uk-radio", + "code": "6d" + }, + { + "answer_id": "authorised-business-trader-eu-radio", + "code": "6e" + }, + { + "answer_id": "any-other-business-businesses-or-branches-answer", + "code": "7" + }, + { + "answer_id": "any-other-business-trading-details-answer", + "code": "8" + } + ], + "questionnaire_flow": { + "type": "Hub", + "options": { + "required_completed_sections": ["section-companies"] + } + }, + "sections": [ + { + "id": "section-companies", + "title": "General insurance companies", + "summary": { + "show_on_completion": true, + "items": [ + { + "type": "List", + "for_list": "companies", + "title": "Companies or UK branches", + "item_anchor_answer_id": "company-or-branch-name", + "item_label": "Name of UK company or branch", + "add_link_text": "Add another UK company or branch", + "empty_list_text": "No UK company or branch added" + } + ], + "show_non_item_answers": true + }, + "groups": [ + { + "id": "group-companies", + "blocks": [ + { + "type": "Question", + "id": "responsible-party", + "question": { + "type": "General", + "id": "responsible-party-question", + "title": "Are you the responsible party for reporting trading details for a company of branch?", + "answers": [ + { + "type": "Radio", + "id": "responsible-party-answer", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "routing_rules": [ + { + "block": "any-companies-or-branches", + "when": { + "==": [ + "Yes", + { + "source": "answers", + "identifier": "responsible-party-answer" + } + ] + } + }, + { + "section": "End" + } + ] + }, + { + "type": "ListCollectorDrivingQuestion", + "id": "any-companies-or-branches", + "for_list": "companies", + "question": { + "type": "General", + "id": "any-companies-or-branches-question", + "title": "Do any companies or branches within your United Kingdom group undertake general insurance business?", + "answers": [ + { + "type": "Radio", + "id": "any-companies-or-branches-answer", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock", + "params": { + "block_id": "add-company", + "list_name": "companies" + } + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "routing_rules": [ + { + "when": { + "==": [ + { + "source": "answers", + "identifier": "any-companies-or-branches-answer" + }, + "Yes" + ] + }, + "block": "any-other-companies-or-branches" + }, + { + "section": "End" + } + ] + }, + { + "id": "any-other-companies-or-branches", + "type": "ListCollector", + "for_list": "companies", + "question": { + "id": "any-other-companies-or-branches-question", + "type": "General", + "title": "Do you need to add any other UK companies or branches that undertake general insurance business?", + "answers": [ + { + "id": "any-other-companies-or-branches-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "add_block": { + "id": "add-company", + "type": "ListAddQuestion", + "question": { + "id": "add-question-companies", + "type": "General", + "title": "What is the name and registration number of the company?", + "answers": [ + { + "id": "company-or-branch-name", + "label": "Name of UK company or branch (Mandatory)", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "repeating_blocks": [ + { + "id": "companies-repeating-block-1", + "type": "ListRepeatingQuestion", + "question": { + "id": "companies-repeating-block-1-question", + "type": "General", + "title": { + "text": "Give details about {company_name}", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "answers", + "identifier": "company-or-branch-name" + } + } + ] + }, + "answers": [ + { + "id": "registration-number", + "label": "Registration number (Mandatory)", + "mandatory": true, + "type": "Number", + "maximum": { + "value": 999, + "exclusive": false + }, + "decimal_places": 0 + }, + { + "id": "registration-date", + "label": "Date of Registration (Mandatory)", + "mandatory": true, + "type": "Date", + "maximum": { + "value": "now" + } + } + ] + } + }, + { + "id": "companies-repeating-block-2", + "type": "ListRepeatingQuestion", + "question": { + "id": "companies-repeating-block-2-question", + "type": "General", + "title": { + "text": "Give details about how {company_name} has been trading over the past {date_difference}.", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "answers", + "identifier": "company-or-branch-name" + } + }, + { + "placeholder": "date_difference", + "transforms": [ + { + "transform": "calculate_date_difference", + "arguments": { + "first_date": { + "source": "answers", + "identifier": "registration-date" + }, + "second_date": { + "value": "now" + } + } + } + ] + } + ] + }, + "answers": [ + { + "type": "Radio", + "label": "Has this company been trading in the UK? (Mandatory)", + "id": "authorised-trader-uk-radio", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + }, + { + "type": "Radio", + "label": "Has this company been trading in the EU? (Not mandatory)", + "id": "authorised-trader-eu-radio", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + } + ], + "edit_block": { + "id": "edit-company", + "type": "ListEditQuestion", + "question": { + "id": "edit-question-companies", + "type": "General", + "title": "What is the name and registration number of the company?", + "answers": [ + { + "id": "company-or-branch-name", + "label": "Name of UK company or branch (Mandatory)", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "remove_block": { + "id": "remove-company", + "type": "ListRemoveQuestion", + "question": { + "id": "remove-question-companies", + "type": "General", + "title": "Are you sure you want to remove this company or UK branch?", + "answers": [ + { + "id": "remove-confirmation", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RemoveListItemAndAnswers" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + "summary": { + "title": "Companies or UK branches", + "item_title": { + "text": "{company_name}", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "answers", + "identifier": "company-or-branch-name" + } + } + ] + } + } + }, + { + "id": "any-other-trading-details", + "type": "Question", + "question": { + "id": "any-other-trading-details-question", + "type": "General", + "title": "Do you have any other details about the trading you have reported for?", + "answers": [ + { + "id": "any-other-trading-details-answer", + "label": "Additional details", + "mandatory": false, + "type": "TextField" + } + ] + } + } + ] + } + ] + }, + { + "id": "section-businesses", + "title": "General insurance business", + "summary": { + "show_on_completion": true, + "items": [ + { + "type": "List", + "for_list": "businesses", + "title": "Businesses or UK branches", + "item_anchor_answer_id": "business-or-branch-name", + "item_label": "Name of UK business or branch", + "add_link_text": "Add another UK business or branch", + "empty_list_text": "No UK business or branch added" + } + ], + "show_non_item_answers": true + }, + "groups": [ + { + "id": "group-businesses", + "blocks": [ + { + "type": "Question", + "id": "responsible-party-business", + "question": { + "type": "General", + "id": "responsible-party-business-question", + "title": "Are you the responsible party for reporting trading details for a business of branch?", + "answers": [ + { + "type": "Radio", + "id": "responsible-party-business-answer", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "routing_rules": [ + { + "block": "any-businesses-or-branches", + "when": { + "==": [ + "Yes", + { + "source": "answers", + "identifier": "responsible-party-business-answer" + } + ] + } + }, + { + "section": "End" + } + ] + }, + { + "type": "ListCollectorDrivingQuestion", + "id": "any-businesses-or-branches", + "for_list": "businesses", + "question": { + "type": "General", + "id": "any-businesses-or-branches-question", + "title": "Do any businesses or branches within your United Kingdom group undertake general insurance business?", + "answers": [ + { + "type": "Radio", + "id": "any-businesses-or-branches-answer", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock", + "params": { + "block_id": "add-business", + "list_name": "businesses" + } + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "routing_rules": [ + { + "when": { + "==": [ + { + "source": "answers", + "identifier": "any-businesses-or-branches-answer" + }, + "Yes" + ] + }, + "block": "any-other-business-businesses-or-branches" + }, + { + "section": "End" + } + ] + }, + { + "id": "any-other-business-businesses-or-branches", + "type": "ListCollector", + "for_list": "businesses", + "question": { + "id": "any-other-business-businesses-or-branches-question", + "type": "General", + "title": "Do you need to add any other UK businesses or branches that undertake general insurance business?", + "answers": [ + { + "id": "any-other-business-businesses-or-branches-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "add_block": { + "id": "add-business", + "type": "ListAddQuestion", + "question": { + "id": "add-question-businesses", + "type": "General", + "title": "What is the name and registration number of the business?", + "answers": [ + { + "id": "business-or-branch-name", + "label": "Name of UK business or branch (Mandatory)", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "repeating_blocks": [ + { + "id": "businesses-repeating-block-1", + "type": "ListRepeatingQuestion", + "question": { + "id": "businesses-repeating-block-1-question", + "type": "General", + "title": { + "text": "Give details about {business_name}", + "placeholders": [ + { + "placeholder": "business_name", + "value": { + "source": "answers", + "identifier": "business-or-branch-name" + } + } + ] + }, + "answers": [ + { + "id": "registration-business-number", + "label": "Registration number (Mandatory)", + "mandatory": true, + "type": "Number", + "maximum": { + "value": 999, + "exclusive": false + }, + "decimal_places": 0 + }, + { + "id": "registration-business-date", + "label": "Date of Registration (Mandatory)", + "mandatory": true, + "type": "Date", + "maximum": { + "value": "now" + } + } + ] + } + }, + { + "id": "businesses-repeating-block-2", + "type": "ListRepeatingQuestion", + "question": { + "id": "businesses-repeating-block-2-question", + "type": "General", + "title": { + "text": "Give details about how {business_name} has been trading over the past {date_difference}.", + "placeholders": [ + { + "placeholder": "business_name", + "value": { + "source": "answers", + "identifier": "business-or-branch-name" + } + }, + { + "placeholder": "date_difference", + "transforms": [ + { + "transform": "calculate_date_difference", + "arguments": { + "first_date": { + "source": "answers", + "identifier": "registration-business-date" + }, + "second_date": { + "value": "now" + } + } + } + ] + } + ] + }, + "answers": [ + { + "type": "Radio", + "label": "Has this business been trading in the UK? (Mandatory)", + "id": "authorised-business-trader-uk-radio", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + }, + { + "type": "Radio", + "label": "Has this business been trading in the EU? (Not mandatory)", + "id": "authorised-business-trader-eu-radio", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + } + ], + "edit_block": { + "id": "edit-business", + "type": "ListEditQuestion", + "question": { + "id": "edit-question-businesses", + "type": "General", + "title": "What is the name and registration number of the business?", + "answers": [ + { + "id": "business-or-branch-name", + "label": "Name of UK business or branch (Mandatory)", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "remove_block": { + "id": "remove-business", + "type": "ListRemoveQuestion", + "question": { + "id": "remove-question-businesses", + "type": "General", + "title": "Are you sure you want to remove this business or UK branch?", + "answers": [ + { + "id": "remove-confirmation-business", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RemoveListItemAndAnswers" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + "summary": { + "title": "Businesses or UK branches", + "item_title": { + "text": "{business_name}", + "placeholders": [ + { + "placeholder": "business_name", + "value": { + "source": "answers", + "identifier": "business-or-branch-name" + } + } + ] + } + } + }, + { + "id": "any-other-business-trading-details", + "type": "Question", + "question": { + "id": "any-other-business-trading-details-question", + "type": "General", + "title": "Do you have any other details about the trading you have reported for?", + "answers": [ + { + "id": "any-other-business-trading-details-answer", + "label": "Additional details", + "mandatory": false, + "type": "TextField" + } + ] + } + } + ] + } + ] + } + ] +} diff --git a/schemas/test/en/test_list_collector_same_name_items.json b/schemas/test/en/test_list_collector_same_name_items.json index 864fec0323..18109e2242 100644 --- a/schemas/test/en/test_list_collector_same_name_items.json +++ b/schemas/test/en/test_list_collector_same_name_items.json @@ -140,39 +140,46 @@ }, "routing_rules": [ { - "goto": { - "section": "End", - "when": [ + "section": "End", + "when": { + "and": [ { - "condition": "equals", - "id": "anyone-usually-live-at-answer", - "value": "No" + "==": [ + { + "identifier": "anyone-usually-live-at-answer", + "source": "answers" + }, + "No" + ] }, { - "condition": "less than", - "list": "people", - "value": 1 + "<": [ + { + "source": "list", + "identifier": "people", + "selector": "count" + }, + 1 + ] } ] } }, { - "goto": { - "block": "list-collector" - } + "block": "list-collector" } ], - "skip_conditions": [ - { - "when": [ + "skip_conditions": { + "when": { + "==": [ { - "condition": "equals", - "id": "you-live-here", - "value": "Yes" - } + "source": "answers", + "identifier": "you-live-here" + }, + "Yes" ] } - ], + }, "type": "Question" }, { @@ -263,7 +270,9 @@ { "transform": "format_name", "arguments": { - "include_middle_names": { "source": "previous_transform" }, + "include_middle_names": { + "source": "previous_transform" + }, "first_name": { "source": "answers", "identifier": "first-name" @@ -354,7 +363,9 @@ { "transform": "format_name", "arguments": { - "include_middle_names": { "source": "previous_transform" }, + "include_middle_names": { + "source": "previous_transform" + }, "first_name": { "source": "answers", "identifier": "first-name" diff --git a/schemas/test/en/test_list_collector_section_summary.json b/schemas/test/en/test_list_collector_section_summary.json index a7637c7bf7..f739e7583d 100644 --- a/schemas/test/en/test_list_collector_section_summary.json +++ b/schemas/test/en/test_list_collector_section_summary.json @@ -4,9 +4,9 @@ "schema_version": "0.0.1", "data_version": "0.0.3", "survey_id": "0", - "title": "Test ListCollector", + "title": "Test List Collector Section Summary Items", "theme": "default", - "description": "A questionnaire to test ListCollector", + "description": "A questionnaire to test list collector section summary items", "metadata": [ { "name": "user_id", @@ -19,151 +19,117 @@ { "name": "ru_name", "type": "string" - }, - { - "name": "display_address", - "type": "string" } ], "questionnaire_flow": { "type": "Linear", - "options": {} + "options": { + "summary": { + "collapsible": false + } + } }, - "individual_response": { - "for_list": "people", - "individual_section_id": "section" + "post_submission": { + "view_response": true }, + "answer_codes": [ + { + "answer_id": "any-companies-or-branches-answer", + "code": "1" + }, + { + "answer_id": "company-or-branch-name", + "code": "1a" + }, + { + "answer_id": "registration-number", + "code": "1b" + }, + { + "answer_id": "authorised-insurer-radio", + "code": "1c" + }, + { + "answer_id": "any-other-companies-or-branches-answer", + "code": "2" + }, + { + "answer_id": "confirmation-checkbox-answer", + "code": "3" + }, + { + "answer_id": "anyone-else", + "code": "4" + }, + { + "answer_id": "householder-checkbox-answer", + "code": "5" + }, + { + "answer_id": "first-name", + "code": "6" + }, + { + "answer_id": "last-name", + "code": "7" + } + ], "sections": [ { - "id": "section", - "title": "People who live here and overnight visitors", + "id": "section-companies", + "title": "General insurance business", "summary": { "show_on_completion": true, "items": [ { "type": "List", - "for_list": "people", - "title": { - "text": "Household members staying overnight on {census_date} at {household_address}", - "placeholders": [ - { - "placeholder": "census_date", - "transforms": [ - { - "arguments": { - "date_format": "d MMMM yyyy", - "date_to_format": { - "value": "2019-10-13" - } - }, - "transform": "format_date" - } - ] - }, - { - "placeholder": "household_address", - "value": { - "identifier": "display_address", - "source": "metadata" - } - } - ] - }, - "add_link_text": "Add someone to this household", - "empty_list_text": "There are no householders" - }, - { - "type": "List", - "for_list": "visitors", - "title": { - "text": "Visitors staying overnight on {census_date} at {household_address}", - "placeholders": [ - { - "placeholder": "census_date", - "transforms": [ - { - "arguments": { - "date_format": "d MMMM yyyy", - "date_to_format": { - "value": "2019-10-13" - } - }, - "transform": "format_date" - } - ] - }, - { - "placeholder": "household_address", - "value": { - "identifier": "display_address", - "source": "metadata" - } - } - ] - }, - "add_link_text": "Add another visitor to this household", - "empty_list_text": "There are no visitors" + "for_list": "companies", + "title": "Companies or UK branches", + "item_anchor_answer_id": "company-or-branch-name", + "item_label": "Name of UK company or branch", + "add_link_text": "Add another UK company or branch", + "empty_list_text": "No UK company or branch added", + "related_answers": [ + { + "source": "answers", + "identifier": "registration-number" + }, + { + "source": "answers", + "identifier": "authorised-insurer-radio" + } + ] } - ] + ], + "show_non_item_answers": true }, "groups": [ { - "id": "group", - "title": "Questions", + "id": "group-companies", "blocks": [ { - "id": "primary-person-list-collector", - "type": "PrimaryPersonListCollector", - "for_list": "people", - "add_or_edit_block": { - "id": "add-or-edit-primary-person", - "type": "PrimaryPersonListAddOrEditQuestion", - "question": { - "id": "primary-person-add-or-edit-question", - "type": "General", - "title": "What is your name?", - "answers": [ - { - "id": "first-name", - "label": "First name", - "mandatory": true, - "type": "TextField" - }, - { - "id": "last-name", - "label": "Last name", - "mandatory": true, - "type": "TextField" - } - ] - } - }, + "type": "ListCollectorDrivingQuestion", + "id": "any-companies-or-branches", + "for_list": "companies", "question": { - "id": "primary-confirmation-question", "type": "General", - "title": { - "placeholders": [ - { - "placeholder": "household_address", - "value": { - "identifier": "display_address", - "source": "metadata" - } - } - ], - "text": "Do you live at {household_address}?" - }, + "id": "any-companies-or-branches-question", + "title": "Do any companies or branches within your United Kingdom group undertake general insurance business?", "answers": [ { - "id": "you-live-here", - "mandatory": true, "type": "Radio", + "id": "any-companies-or-branches-answer", + "mandatory": true, "options": [ { "label": "Yes", "value": "Yes", "action": { - "type": "RedirectToListAddBlock" + "type": "RedirectToListAddBlock", + "params": { + "block_id": "add-company", + "list_name": "companies" + } } }, { @@ -173,30 +139,30 @@ ] } ] - } + }, + "routing_rules": [ + { + "when": { + "==": [{ "source": "answers", "identifier": "any-companies-or-branches-answer" }, "No"] + }, + "block": "confirmation-checkbox" + }, + { + "block": "any-other-companies-or-branches" + } + ] }, { - "id": "list-collector", + "id": "any-other-companies-or-branches", "type": "ListCollector", - "for_list": "people", + "for_list": "companies", "question": { - "id": "confirmation-question", + "id": "any-other-companies-or-branches-question", "type": "General", - "title": { - "placeholders": [ - { - "placeholder": "household_address", - "value": { - "identifier": "display_address", - "source": "metadata" - } - } - ], - "text": "Does anyone else live at {household_address}?" - }, + "title": "Do you need to add any other UK companies or branches that undertake general insurance business?", "answers": [ { - "id": "anyone-else", + "id": "any-other-companies-or-branches-answer", "mandatory": true, "type": "Radio", "options": [ @@ -216,61 +182,98 @@ ] }, "add_block": { - "id": "add-person", + "id": "add-company", "type": "ListAddQuestion", "question": { - "id": "add-question", + "id": "add-question-companies", "type": "General", - "title": "What is the name of the person?", + "title": "Give details about the company or branch that undertakes general insurance business", "answers": [ { - "id": "first-name", - "label": "First name", + "id": "company-or-branch-name", + "label": "Name of UK company or branch", "mandatory": true, "type": "TextField" }, { - "id": "last-name", - "label": "Last name", + "id": "registration-number", + "label": "Registration number", "mandatory": true, - "type": "TextField" + "type": "Number", + "maximum": { + "value": 999, + "exclusive": false + }, + "decimal_places": 0 + }, + { + "type": "Radio", + "label": "Is this UK company or branch an authorised insurer?", + "id": "authorised-insurer-radio", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] } ] } }, "edit_block": { - "id": "edit-person", + "id": "edit-company", "type": "ListEditQuestion", "question": { - "id": "edit-question", + "id": "edit-question-companies", "type": "General", - "title": "What is the name of the person?", + "title": "What is the name of the company?", "answers": [ { - "id": "first-name", - "label": "First name", + "id": "company-or-branch-name", + "label": "Name of UK company or branch", "mandatory": true, "type": "TextField" }, { - "id": "last-name", - "label": "Last name", + "id": "registration-number", + "label": "Registration number", "mandatory": true, - "type": "TextField" + "type": "Number" + }, + { + "type": "Radio", + "label": "Is this UK company or branch an authorised insurer?", + "id": "authorised-insurer-radio", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] } ] } }, "remove_block": { - "id": "remove-person", + "id": "remove-company", "type": "ListRemoveQuestion", "question": { - "id": "remove-question", + "id": "remove-question-companies", "type": "General", - "title": "Are you sure you want to remove this person?", + "title": "Are you sure you want to remove this company or UK branch?", "answers": [ { - "id": "remove-confirmation", + "id": "remove-confirmation-company", "mandatory": true, "type": "Radio", "options": [ @@ -291,82 +294,100 @@ } }, "summary": { - "title": { - "text": "Household members staying overnight on {census_date} at {household_address}", + "title": "Companies or UK branches", + "item_title": { + "text": "{company_name}", "placeholders": [ { - "placeholder": "census_date", - "transforms": [ - { - "arguments": { - "date_format": "d MMMM yyyy", - "date_to_format": { - "value": "2019-10-13" - } - }, - "transform": "format_date" - } - ] - }, - { - "placeholder": "household_address", + "placeholder": "company_name", "value": { - "identifier": "display_address", - "source": "metadata" + "source": "answers", + "identifier": "company-or-branch-name" } } ] - }, - "item_title": { - "text": "{person_name}", - "placeholders": [ + } + } + }, + { + "type": "Question", + "id": "confirmation-checkbox", + "question": { + "answers": [ + { + "id": "confirmation-checkbox-answer", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ], + "type": "Radio" + } + ], + "id": "confirmation-checkbox-question", + "title": "Are all companies or branches based in UK?", + "type": "General" + }, + "skip_conditions": { + "when": { + "!=": [ { - "placeholder": "person_name", - "transforms": [ + "count": [ { - "arguments": { - "delimiter": " ", - "list_to_concatenate": [ - { - "source": "answers", - "identifier": "first-name" - }, - { - "source": "answers", - "identifier": "last-name" - } - ] - }, - "transform": "concatenate_list" + "source": "list", + "identifier": "companies" } ] - } + }, + 3 ] } } - }, + } + ] + } + ] + }, + { + "id": "section-household", + "title": "Household Section", + "summary": { + "show_on_completion": true, + "items": [ + { + "type": "List", + "for_list": "people", + "title": "Who lives here", + "item_anchor_answer_id": "first-name", + "item_label": "Name of householder", + "add_link_text": "Add someone to this household", + "empty_list_text": "There are no householders" + } + ], + "show_non_item_answers": true + }, + "groups": [ + { + "id": "group-household", + "title": "Household Questions", + "blocks": [ { - "id": "visitor-list-collector", + "id": "list-collector", "type": "ListCollector", - "for_list": "visitors", + "for_list": "people", "question": { - "id": "confirmation-visitor-question", + "id": "confirmation-question", "type": "General", - "title": { - "placeholders": [ - { - "placeholder": "household_address", - "value": { - "identifier": "display_address", - "source": "metadata" - } - } - ], - "text": "Are there any other visitors staying overnight at {household_address}?" - }, + "title": "Does anyone else live here?", "answers": [ { - "id": "any-more-visitors", + "id": "anyone-else", "mandatory": true, "type": "Radio", "options": [ @@ -386,21 +407,22 @@ ] }, "add_block": { - "id": "add-visitor", + "id": "add-person", "type": "ListAddQuestion", + "cancel_text": "Don’t need to add anyone else?", "question": { - "id": "add-visitor-question", + "id": "add-question-people", "type": "General", - "title": "What is the name of the visitor?", + "title": "What is the name of the person?", "answers": [ { - "id": "first-name-visitor", + "id": "first-name", "label": "First name", "mandatory": true, "type": "TextField" }, { - "id": "last-name-visitor", + "id": "last-name", "label": "Last name", "mandatory": true, "type": "TextField" @@ -409,21 +431,22 @@ } }, "edit_block": { - "id": "edit-visitor-person", + "id": "edit-person", "type": "ListEditQuestion", + "cancel_text": "Don’t need to change anything?", "question": { - "id": "edit-visitor-question", + "id": "edit-question-people", "type": "General", "title": "What is the name of the person?", "answers": [ { - "id": "first-name-visitor", + "id": "first-name", "label": "First name", "mandatory": true, "type": "TextField" }, { - "id": "last-name-visitor", + "id": "last-name", "label": "Last name", "mandatory": true, "type": "TextField" @@ -432,15 +455,17 @@ } }, "remove_block": { - "id": "remove-visitor", + "id": "remove-person", "type": "ListRemoveQuestion", + "cancel_text": "Don’t need to remove this person?", "question": { - "id": "remove-visitor-question", + "id": "remove-question-people", "type": "General", "title": "Are you sure you want to remove this person?", + "warning": "All of the information about this person will be deleted", "answers": [ { - "id": "remove-visitor-confirmation", + "id": "remove-confirmation", "mandatory": true, "type": "Radio", "options": [ @@ -461,37 +486,12 @@ } }, "summary": { - "title": { - "text": "Visitors staying overnight on {census_date} at {household_address}", - "placeholders": [ - { - "placeholder": "census_date", - "transforms": [ - { - "arguments": { - "date_format": "d MMMM yyyy", - "date_to_format": { - "value": "2019-10-13" - } - }, - "transform": "format_date" - } - ] - }, - { - "placeholder": "household_address", - "value": { - "identifier": "display_address", - "source": "metadata" - } - } - ] - }, + "title": "Household members", "item_title": { - "text": "{visitor_name}", + "text": "{person_name}", "placeholders": [ { - "placeholder": "visitor_name", + "placeholder": "person_name", "transforms": [ { "arguments": { @@ -499,11 +499,11 @@ "list_to_concatenate": [ { "source": "answers", - "identifier": "first-name-visitor" + "identifier": "first-name" }, { "source": "answers", - "identifier": "last-name-visitor" + "identifier": "last-name" } ] }, @@ -514,6 +514,32 @@ ] } } + }, + { + "type": "Question", + "id": "householder-checkbox", + "question": { + "answers": [ + { + "id": "householder-checkbox-answer", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ], + "type": "Radio" + } + ], + "id": "householder-checkbox-question", + "title": "Are all these people based in the UK?", + "type": "General" + } } ] } diff --git a/schemas/test/en/test_list_collector_two_list_collectors.json b/schemas/test/en/test_list_collector_two_list_collectors.json index 001faf7cf4..8f6c798d43 100644 --- a/schemas/test/en/test_list_collector_two_list_collectors.json +++ b/schemas/test/en/test_list_collector_two_list_collectors.json @@ -81,9 +81,7 @@ }, "routing_rules": [ { - "goto": { - "block": "list-collector" - } + "block": "list-collector" } ] }, @@ -273,9 +271,7 @@ }, "routing_rules": [ { - "goto": { - "block": "another-list-collector" - } + "block": "another-list-collector" } ], "type": "Question" diff --git a/schemas/test/en/test_list_collector_variants.json b/schemas/test/en/test_list_collector_variants.json index db15f07f79..44d1975b70 100644 --- a/schemas/test/en/test_list_collector_variants.json +++ b/schemas/test/en/test_list_collector_variants.json @@ -90,13 +90,15 @@ } ] }, - "when": [ - { - "id": "you-live-here-answer", - "condition": "equals", - "value": "Yes" - } - ] + "when": { + "==": [ + { + "source": "answers", + "identifier": "you-live-here-answer" + }, + "Yes" + ] + } }, { "question": { @@ -124,13 +126,15 @@ } ] }, - "when": [ - { - "id": "you-live-here-answer", - "condition": "equals", - "value": "No" - } - ] + "when": { + "==": [ + { + "source": "answers", + "identifier": "you-live-here-answer" + }, + "No" + ] + } } ], "add_block": { @@ -157,13 +161,15 @@ } ] }, - "when": [ - { - "id": "you-live-here-answer", - "condition": "equals", - "value": "No" - } - ] + "when": { + "==": [ + { + "source": "answers", + "identifier": "you-live-here-answer" + }, + "No" + ] + } }, { "question": { @@ -185,13 +191,15 @@ } ] }, - "when": [ - { - "id": "you-live-here-answer", - "condition": "equals", - "value": "Yes" - } - ] + "when": { + "==": [ + { + "source": "answers", + "identifier": "you-live-here-answer" + }, + "Yes" + ] + } } ] }, @@ -219,13 +227,15 @@ } ] }, - "when": [ - { - "id": "you-live-here-answer", - "condition": "equals", - "value": "Yes" - } - ] + "when": { + "==": [ + { + "source": "answers", + "identifier": "you-live-here-answer" + }, + "Yes" + ] + } }, { "question": { @@ -247,13 +257,15 @@ } ] }, - "when": [ - { - "id": "you-live-here-answer", - "condition": "equals", - "value": "No" - } - ] + "when": { + "==": [ + { + "source": "answers", + "identifier": "you-live-here-answer" + }, + "No" + ] + } } ] }, @@ -287,13 +299,15 @@ } ] }, - "when": [ - { - "id": "you-live-here-answer", - "condition": "equals", - "value": "Yes" - } - ] + "when": { + "==": [ + { + "source": "answers", + "identifier": "you-live-here-answer" + }, + "Yes" + ] + } }, { "question": { @@ -321,13 +335,15 @@ } ] }, - "when": [ - { - "id": "you-live-here-answer", - "condition": "equals", - "value": "No" - } - ] + "when": { + "==": [ + { + "source": "answers", + "identifier": "you-live-here-answer" + }, + "No" + ] + } } ] }, diff --git a/schemas/test/en/test_list_collector_variants_primary_person.json b/schemas/test/en/test_list_collector_variants_primary_person.json index 582d8e9164..d4b7246a6d 100644 --- a/schemas/test/en/test_list_collector_variants_primary_person.json +++ b/schemas/test/en/test_list_collector_variants_primary_person.json @@ -91,13 +91,15 @@ } ] }, - "when": [ - { - "id": "variant-answer", - "condition": "equals", - "value": "No" - } - ] + "when": { + "==": [ + { + "source": "answers", + "identifier": "variant-answer" + }, + "No" + ] + } }, { "question": { @@ -119,13 +121,15 @@ } ] }, - "when": [ - { - "id": "variant-answer", - "condition": "equals", - "value": "Yes" - } - ] + "when": { + "==": [ + { + "source": "answers", + "identifier": "variant-answer" + }, + "Yes" + ] + } } ] }, @@ -156,13 +160,15 @@ } ] }, - "when": [ - { - "id": "variant-answer", - "condition": "equals", - "value": "Yes" - } - ] + "when": { + "==": [ + { + "source": "answers", + "identifier": "variant-answer" + }, + "Yes" + ] + } }, { "question": { @@ -190,13 +196,15 @@ } ] }, - "when": [ - { - "id": "variant-answer", - "condition": "equals", - "value": "No" - } - ] + "when": { + "==": [ + { + "source": "answers", + "identifier": "variant-answer" + }, + "No" + ] + } } ] }, diff --git a/schemas/test/en/test_list_collector_variants_section_summary.json b/schemas/test/en/test_list_collector_variants_section_summary.json new file mode 100644 index 0000000000..5b8ac29666 --- /dev/null +++ b/schemas/test/en/test_list_collector_variants_section_summary.json @@ -0,0 +1,551 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Test List Collector Variants Section Summary Items", + "theme": "default", + "description": "A questionnaire to test list collector section summary items for variants", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Linear", + "options": { + "summary": { + "collapsible": false + } + } + }, + "sections": [ + { + "id": "section-companies", + "title": "General insurance business", + "summary": { + "show_on_completion": true, + "items": [ + { + "type": "List", + "for_list": "companies", + "title": "Companies or UK branches", + "item_anchor_answer_id": "company-or-branch-name", + "item_label": "Name of UK or non-UK company or branch", + "add_link_text": "Add another UK company or branch", + "empty_list_text": "No UK company or branch added", + "related_answers": [ + { + "source": "answers", + "identifier": "registration-number" + }, + { + "source": "answers", + "identifier": "authorised-insurer-radio" + } + ] + } + ], + "show_non_item_answers": true + }, + "groups": [ + { + "id": "group-companies", + "blocks": [ + { + "type": "Question", + "id": "uk-based-block", + "question": { + "type": "General", + "id": "uk-based-question", + "title": "Are the companies UK based?", + "answers": [ + { + "type": "Radio", + "id": "uk-based-answer", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + { + "type": "ListCollectorDrivingQuestion", + "id": "any-companies-or-branches", + "for_list": "companies", + "question": { + "type": "General", + "id": "any-companies-or-branches-question", + "title": "Do any companies or branches undertake general insurance business?", + "answers": [ + { + "type": "Radio", + "id": "any-companies-or-branches-answer", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock", + "params": { + "block_id": "add-company", + "list_name": "companies" + } + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "routing_rules": [ + { + "block": "confirmation-checkbox", + "when": { + "==": [ + { + "source": "answers", + "identifier": "any-companies-or-branches-answer" + }, + "No" + ] + } + }, + { + "block": "any-other-companies-or-branches" + } + ] + }, + { + "id": "any-other-companies-or-branches", + "type": "ListCollector", + "for_list": "companies", + "question_variants": [ + { + "question": { + "id": "any-other-companies-or-branches-question", + "type": "General", + "title": "Do you need to add any other UK companies or branches that undertake general insurance business?", + "answers": [ + { + "id": "any-other-companies-or-branches-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "when": { + "==": [ + { + "source": "answers", + "identifier": "uk-based-answer" + }, + "Yes" + ] + } + }, + { + "question": { + "id": "any-other-companies-or-branches-question", + "type": "General", + "title": "Do you need to add any other non-UK companies or branches that undertake general insurance business?", + "answers": [ + { + "id": "any-other-companies-or-branches-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "when": { + "==": [ + { + "source": "answers", + "identifier": "uk-based-answer" + }, + "No" + ] + } + } + ], + "add_block": { + "id": "add-company", + "type": "ListAddQuestion", + "question_variants": [ + { + "question": { + "id": "add-question-companies", + "type": "General", + "title": "Give details about the company or branch that undertakes general insurance business", + "answers": [ + { + "id": "company-or-branch-name", + "label": "Name of UK company or branch", + "mandatory": true, + "type": "TextField" + }, + { + "id": "registration-number", + "label": "UK Registration number", + "mandatory": true, + "type": "Number", + "maximum": { + "value": 999, + "exclusive": false + }, + "decimal_places": 0 + }, + { + "type": "Radio", + "label": "Is this UK company or branch an authorised insurer?", + "id": "authorised-insurer-radio", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "when": { + "==": [ + { + "source": "answers", + "identifier": "uk-based-answer" + }, + "Yes" + ] + } + }, + { + "question": { + "id": "add-question-companies", + "type": "General", + "title": "Give details about the company or branch that undertakes general insurance business", + "answers": [ + { + "id": "company-or-branch-name", + "label": "Name of non-UK company or branch", + "mandatory": true, + "type": "TextField" + }, + { + "id": "registration-number", + "label": "Non-UK Registration number", + "mandatory": true, + "type": "Number", + "maximum": { + "value": 999, + "exclusive": false + }, + "decimal_places": 0 + }, + { + "type": "Radio", + "label": "Is this non-UK company or branch an authorised insurer?", + "id": "authorised-insurer-radio", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "when": { + "==": [ + { + "source": "answers", + "identifier": "uk-based-answer" + }, + "No" + ] + } + } + ] + }, + "edit_block": { + "id": "edit-company", + "type": "ListEditQuestion", + "question_variants": [ + { + "question": { + "id": "edit-question-companies", + "type": "General", + "title": "What is the name of the company?", + "answers": [ + { + "id": "company-or-branch-name", + "label": "Name of UK company or branch", + "mandatory": true, + "type": "TextField" + }, + { + "id": "registration-number", + "label": "UK Registration number", + "mandatory": true, + "type": "Number" + }, + { + "type": "Radio", + "label": "Is this UK company or branch an authorised insurer?", + "id": "authorised-insurer-radio", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "when": { + "==": [ + { + "source": "answers", + "identifier": "uk-based-answer" + }, + "Yes" + ] + } + }, + { + "question": { + "id": "edit-question-companies", + "type": "General", + "title": "What is the name of the company?", + "answers": [ + { + "id": "company-or-branch-name", + "label": "Name of non-UK company or branch", + "mandatory": true, + "type": "TextField" + }, + { + "id": "registration-number", + "label": "Non-UK Registration number", + "mandatory": true, + "type": "Number" + }, + { + "type": "Radio", + "label": "Is this non-UK company or branch an authorised insurer?", + "id": "authorised-insurer-radio", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "when": { + "==": [ + { + "source": "answers", + "identifier": "uk-based-answer" + }, + "No" + ] + } + } + ] + }, + "remove_block": { + "id": "remove-company", + "type": "ListRemoveQuestion", + "question_variants": [ + { + "question": { + "id": "remove-question-companies", + "type": "General", + "title": "Are you sure you want to remove this company or non-UK branch?", + "answers": [ + { + "id": "remove-confirmation", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RemoveListItemAndAnswers" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "when": { + "==": [ + { + "source": "answers", + "identifier": "uk-based-answer" + }, + "No" + ] + } + }, + { + "question": { + "id": "remove-question-companies", + "type": "General", + "title": "Are you sure you want to remove this company or non-UK branch?", + "answers": [ + { + "id": "remove-confirmation", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RemoveListItemAndAnswers" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "when": { + "==": [ + { + "source": "answers", + "identifier": "uk-based-answer" + }, + "Yes" + ] + } + } + ] + }, + "summary": { + "title": "Companies or UK branches", + "item_title": { + "text": "{company_name}", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "answers", + "identifier": "company-or-branch-name" + } + } + ] + } + } + }, + { + "type": "Question", + "id": "confirmation-checkbox", + "question": { + "answers": [ + { + "id": "confirmation-checkbox-answer", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ], + "type": "Radio" + } + ], + "id": "confirmation-checkbox-question", + "title": "Are all companies or branches based in UK?", + "type": "General" + } + } + ] + } + ] + } + ] +} diff --git a/schemas/test/en/test_markup.json b/schemas/test/en/test_markup.json index 59a5c2cd3e..98bf348532 100644 --- a/schemas/test/en/test_markup.json +++ b/schemas/test/en/test_markup.json @@ -48,22 +48,21 @@ "hide_guidance": "hide lorem ipsum guidance", "contents": [ { - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla vitae elit libero, a pharetra augue. Vestibulum id ligula porta felis euismod semper. Integer posuere erat a ante venenatis dapibus posuere velit aliquet." + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla vitae elit libero, a pharetra augue. Vestibulum id ligula porta felis euismod semper. Integer posuere erat a ante venenatis dapibus posuere velit aliquet." } ] }, "id": "answer", "label": "What is the thing?", "mandatory": false, - "q_code": "0", "type": "TextField" } ], "description": [ - "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Curabitur blandit tempus porttitor." + "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Curabitur blandit tempus porttitor." ], "id": "question", - "title": "This is a title with emphasis", + "title": "This is a title with emphasis", "type": "General" } } diff --git a/schemas/test/en/test_metadata_routing.json b/schemas/test/en/test_metadata_routing.json index dccb4c2157..1142d57f9e 100644 --- a/schemas/test/en/test_metadata_routing.json +++ b/schemas/test/en/test_metadata_routing.json @@ -4,9 +4,9 @@ "schema_version": "0.0.1", "data_version": "0.0.3", "survey_id": "0", - "title": "Census England Household Schema", - "description": "Census England Household Schema", - "theme": "census", + "title": "Household Schema", + "description": "Household Schema", + "theme": "default", "metadata": [ { "name": "user_id", @@ -17,8 +17,12 @@ "type": "string" }, { - "name": "flag_1", + "name": "boolean_flag", "type": "boolean" + }, + { + "name": "ru_name", + "type": "string" } ], "questionnaire_flow": { @@ -48,7 +52,6 @@ { "id": "block1-answer", "mandatory": false, - "q_code": "20", "type": "TextField", "label": "Question 1" } @@ -56,21 +59,19 @@ }, "routing_rules": [ { - "goto": { - "block": "block3", - "when": [ + "block": "block3", + "when": { + "==": [ { - "meta": "flag_1", - "condition": "equals", - "value": true - } + "identifier": "boolean_flag", + "source": "metadata" + }, + true ] } }, { - "goto": { - "block": "block2" - } + "block": "block2" } ] }, @@ -85,7 +86,6 @@ { "id": "block2-answer", "mandatory": false, - "q_code": "20", "type": "TextField", "label": "Question 2" } @@ -103,7 +103,6 @@ { "id": "block3-answer", "mandatory": false, - "q_code": "20", "type": "TextField", "label": "Question 3" } diff --git a/schemas/test/en/test_multiple_answers.json b/schemas/test/en/test_multiple_answers.json index b8d4d4e260..6f1832cdac 100644 --- a/schemas/test/en/test_multiple_answers.json +++ b/schemas/test/en/test_multiple_answers.json @@ -102,7 +102,8 @@ "type": "Currency", "currency": "GBP", "mandatory": true, - "label": "What is your budget?" + "label": "What is your budget?", + "decimal_places": 2 }, { "id": "month-year-date-answer", diff --git a/schemas/test/en/test_multiple_piping.json b/schemas/test/en/test_multiple_piping.json index d1dce19adb..a7e56dbb94 100644 --- a/schemas/test/en/test_multiple_piping.json +++ b/schemas/test/en/test_multiple_piping.json @@ -135,7 +135,7 @@ "question": { "id": "multiple-piping-question", "title": { - "text": "Does {person} live at {address}", + "text": "Does {person} live at {address}", "placeholders": [ { "placeholder": "person", diff --git a/schemas/test/en/test_mutually_exclusive.json b/schemas/test/en/test_mutually_exclusive.json index 581df9e89f..e691163842 100644 --- a/schemas/test/en/test_mutually_exclusive.json +++ b/schemas/test/en/test_mutually_exclusive.json @@ -65,7 +65,7 @@ }, { "label": "Other", - "description": "Choose any other topping", + "description": "Enter another Nationality", "value": "Other", "detail_answer": { "mandatory": false, diff --git a/schemas/test/en/test_mutually_exclusive_multiple.json b/schemas/test/en/test_mutually_exclusive_multiple.json new file mode 100644 index 0000000000..a4603cc5a2 --- /dev/null +++ b/schemas/test/en/test_mutually_exclusive_multiple.json @@ -0,0 +1,622 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Mutually Exclusive Multiple", + "theme": "default", + "description": "A questionnaire to demo mutually exclusive answers with multiple radio override", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Linear", + "options": { + "summary": { + "collapsible": true + } + } + }, + "sections": [ + { + "id": "mutually-exclusive-checkbox-section", + "title": "Checkbox", + "summary": { "show_on_completion": true }, + "groups": [ + { + "id": "mutually-exclusive-checkbox-mandatory-group", + "title": "Mutually Exclusive With Multiple Radio Override - Mandatory", + "blocks": [ + { + "type": "Question", + "id": "mutually-exclusive-checkbox", + "question": { + "id": "mutually-exclusive-checkbox-question", + "type": "MutuallyExclusive", + "title": "What is your nationality?", + "warning": "This is important", + "mandatory": true, + "answers": [ + { + "id": "checkbox-answer", + "instruction": "Select an answer", + "type": "Checkbox", + "mandatory": false, + "options": [ + { + "label": "British", + "value": "British" + }, + { + "label": "Irish", + "value": "Irish" + }, + { + "label": "Other", + "description": "Enter another Nationality", + "value": "Other", + "detail_answer": { + "mandatory": false, + "id": "checkbox-child-other-answer", + "label": "Please specify other", + "type": "TextField" + } + } + ] + }, + { + "id": "checkbox-exclusive-answer", + "mandatory": false, + "type": "Radio", + "options": [ + { + "label": "I prefer not to say", + "description": "Some description", + "value": "I prefer not to say" + }, + { + "label": "I am an alien", + "description": "Some description", + "value": "I am an alien" + } + ] + } + ] + } + } + ] + } + ] + }, + { + "id": "mutually-exclusive-mandatory-date-section", + "title": "Date", + "summary": { "show_on_completion": true }, + "groups": [ + { + "id": "mutually-exclusive-mandatory-date-group", + "title": "Mutually Exclusive With Multiple Radio Override - Mandatory", + "blocks": [ + { + "type": "Question", + "id": "mutually-exclusive-mandatory-date", + "question": { + "id": "mutually-exclusive-mandatory-date-question", + "type": "MutuallyExclusive", + "title": "When did you leave your last paid job?", + "mandatory": true, + "answers": [ + { + "id": "mandatory-date-answer", + "label": "Enter a date", + "mandatory": false, + "type": "Date" + }, + { + "id": "mandatory-date-exclusive-answer", + "mandatory": false, + "type": "Radio", + "options": [ + { + "label": "I prefer not to say", + "value": "I prefer not to say" + }, + { + "label": "I have never worked", + "value": "I have never worked" + } + ] + } + ] + } + } + ] + } + ] + }, + { + "id": "mutually-exclusive-date-section", + "title": "Date", + "summary": { "show_on_completion": true }, + "groups": [ + { + "id": "mutually-exclusive-date-group", + "title": "Mutually Exclusive With Multiple Radio Override - Optional", + "blocks": [ + { + "type": "Question", + "id": "mutually-exclusive-date", + "question": { + "id": "mutually-exclusive-date-question", + "type": "MutuallyExclusive", + "title": "When did you leave your last paid job?", + "mandatory": false, + "answers": [ + { + "id": "date-answer", + "label": "Enter a date", + "mandatory": false, + "type": "Date" + }, + { + "id": "date-exclusive-answer", + "mandatory": false, + "type": "Radio", + "options": [ + { + "label": "I prefer not to say", + "value": "I prefer not to say" + }, + { + "label": "I have never worked", + "value": "I have never worked" + } + ] + } + ] + } + } + ] + } + ] + }, + { + "id": "mutually-exclusive-currency-section", + "title": "Currency", + "summary": { "show_on_completion": true }, + "groups": [ + { + "id": "mutually-exclusive-currency-group", + "title": "Mutually Exclusive With Multiple Radio Override - Optional", + "blocks": [ + { + "type": "Question", + "id": "mutually-exclusive-currency", + "question": { + "id": "mutually-exclusive-currency-question", + "type": "MutuallyExclusive", + "title": "What is your annual income before tax?", + "mandatory": false, + "answers": [ + { + "id": "currency-answer", + "label": "Enter your income", + "mandatory": false, + "type": "Currency", + "currency": "GBP" + }, + { + "id": "currency-exclusive-answer", + "mandatory": false, + "type": "Radio", + "options": [ + { + "label": "I prefer not to say", + "value": "I prefer not to say" + }, + { + "label": "I have no income", + "value": "I have no income" + } + ] + } + ] + } + } + ] + } + ] + }, + { + "id": "mutually-exclusive-number-section", + "title": "Number", + "summary": { "show_on_completion": true }, + "groups": [ + { + "id": "mutually-exclusive-number-group", + "title": "Mutually Exclusive With Multiple Radio Override - Optional", + "blocks": [ + { + "type": "Question", + "id": "mutually-exclusive-number", + "question": { + "id": "mutually-exclusive-number-question", + "type": "MutuallyExclusive", + "title": "What is your favourite number?", + "mandatory": false, + "answers": [ + { + "id": "number-answer", + "label": "Enter your favourite number", + "mandatory": false, + "type": "Number", + "decimal_places": 2 + }, + { + "id": "number-exclusive-answer", + "mandatory": false, + "type": "Radio", + "options": [ + { + "label": "I prefer not to say", + "value": "I prefer not to say" + }, + { + "label": "I dont have a favourite number", + "value": "I dont have a favourite number" + } + ] + } + ] + } + } + ] + } + ] + }, + { + "id": "mutually-exclusive-percentage-section", + "title": "Percentage", + "summary": { "show_on_completion": true }, + "groups": [ + { + "id": "mutually-exclusive-percentage-group", + "title": "Mutually Exclusive With Multiple Radio Override - Optional", + "blocks": [ + { + "type": "Question", + "id": "mutually-exclusive-percentage", + "question": { + "id": "mutually-exclusive-percentage-question", + "type": "MutuallyExclusive", + "title": "What was the percentage increase in your annual income this tax year?", + "mandatory": false, + "answers": [ + { + "id": "percentage-answer", + "label": "Enter the percentage increase of your income", + "mandatory": false, + "type": "Percentage", + "maximum": { + "value": 100 + } + }, + { + "id": "percentage-exclusive-answer", + "mandatory": false, + "type": "Radio", + "options": [ + { + "label": "I prefer not to say", + "value": "I prefer not to say" + }, + { + "label": "No income change", + "value": "No income change" + } + ] + } + ] + } + } + ] + } + ] + }, + { + "id": "mutually-exclusive-textfield-section", + "title": "Textfield", + "summary": { "show_on_completion": true }, + "groups": [ + { + "id": "mutually-exclusive-textfield-group", + "title": "Mutually Exclusive With Multiple Radio Override - Optional", + "blocks": [ + { + "type": "Question", + "id": "mutually-exclusive-textfield", + "question": { + "id": "mutually-exclusive-textfield-question", + "type": "MutuallyExclusive", + "title": "What is your favourite colour?", + "mandatory": false, + "answers": [ + { + "id": "textfield-answer", + "label": "Enter your favourite colour", + "mandatory": false, + "type": "TextField" + }, + { + "id": "textfield-exclusive-answer", + "mandatory": false, + "type": "Radio", + "options": [ + { + "label": "I prefer not to say", + "value": "I prefer not to say" + }, + { + "label": "I dont have a favorite colour", + "value": "I dont have a favorite colour" + } + ] + } + ] + } + } + ] + } + ] + }, + { + "id": "mutually-exclusive-month-year-date-section", + "title": "Month Year Date", + "summary": { "show_on_completion": true }, + "groups": [ + { + "id": "mutually-exclusive-month-year-date-group", + "title": "Mutually Exclusive With Multiple Radio Override - Optional", + "blocks": [ + { + "type": "Question", + "id": "mutually-exclusive-month-year-date", + "question": { + "id": "mutually-exclusive-month-year-date-question", + "type": "MutuallyExclusive", + "title": "When did you leave your last paid job?", + "mandatory": false, + "answers": [ + { + "id": "month-year-date-answer", + "label": "Enter a date", + "mandatory": false, + "type": "MonthYearDate", + "maximum": { + "value": "now" + } + }, + { + "id": "month-year-date-exclusive-answer", + "mandatory": false, + "type": "Radio", + "options": [ + { + "label": "I prefer not to say", + "value": "I prefer not to say" + }, + { + "label": "I have never worked", + "value": "I have never worked" + } + ] + } + ] + } + } + ] + } + ] + }, + { + "id": "mutually-exclusive-year-date-section", + "title": "Year Date", + "summary": { "show_on_completion": true }, + "groups": [ + { + "id": "mutually-exclusive-year-date-group", + "title": "Mutually Exclusive With Multiple Radio Override - Optional", + "blocks": [ + { + "type": "Question", + "id": "mutually-exclusive-year-date", + "question": { + "id": "mutually-exclusive-year-date-question", + "type": "MutuallyExclusive", + "title": "When did you leave your last paid job?", + "mandatory": false, + "answers": [ + { + "id": "year-date-answer", + "label": "Enter a date", + "mandatory": false, + "type": "YearDate", + "maximum": { + "value": "now" + } + }, + { + "id": "year-date-exclusive-answer", + "mandatory": false, + "type": "Radio", + "options": [ + { + "label": "I prefer not to say", + "value": "I prefer not to say" + }, + { + "label": "I have never worked", + "value": "I have never worked" + } + ] + } + ] + } + } + ] + } + ] + }, + { + "id": "mutually-exclusive-unit-section", + "title": "Unit", + "summary": { "show_on_completion": true }, + "groups": [ + { + "id": "mutually-exclusive-unit-group", + "title": "Mutually Exclusive With Multiple Radio Override - Optional", + "blocks": [ + { + "type": "Question", + "id": "mutually-exclusive-unit", + "question": { + "id": "mutually-exclusive-unit-question", + "type": "MutuallyExclusive", + "title": "How many years have you been in the UK?", + "mandatory": false, + "answers": [ + { + "id": "unit-answer", + "label": "Enter the number of years you have lived in the UK", + "unit": "duration-year", + "type": "Unit", + "unit_length": "long", + "mandatory": false + }, + { + "id": "unit-exclusive-answer", + "mandatory": false, + "type": "Radio", + "options": [ + { + "label": "I prefer not to say", + "value": "I prefer not to say" + }, + { + "label": "I have never lived in the UK", + "value": "I have never lived in the UK" + } + ] + } + ] + } + } + ] + } + ] + }, + { + "id": "mutually-exclusive-duration-section", + "title": "Duration", + "summary": { "show_on_completion": true }, + "groups": [ + { + "id": "mutually-exclusive-duration-group", + "title": "Mutually Exclusive With Multiple Radio Override - Optional", + "blocks": [ + { + "type": "Question", + "id": "mutually-exclusive-duration", + "question": { + "id": "mutually-exclusive-duration-question", + "type": "MutuallyExclusive", + "title": "How long have you been employed for?", + "mandatory": false, + "answers": [ + { + "id": "duration-answer", + "mandatory": false, + "units": ["years", "months"], + "type": "Duration" + }, + { + "id": "duration-exclusive-answer", + "mandatory": false, + "type": "Radio", + "options": [ + { + "label": "I prefer not to say", + "value": "I prefer not to say" + }, + { + "label": "I have never worked", + "value": "I have never worked" + } + ] + } + ] + } + } + ] + } + ] + }, + { + "id": "mutually-exclusive-textarea-section", + "title": "TextArea", + "summary": { "show_on_completion": true }, + "groups": [ + { + "id": "mutually-exclusive-textarea-group", + "title": "Mutually Exclusive With Multiple Radio Override - Optional", + "blocks": [ + { + "type": "Question", + "id": "mutually-exclusive-textarea", + "question": { + "id": "mutually-exclusive-textarea-question", + "type": "MutuallyExclusive", + "title": "Why did you leave your last job?", + "mandatory": false, + "answers": [ + { + "id": "textarea-answer", + "mandatory": false, + "type": "TextArea" + }, + { + "id": "textarea-exclusive-answer", + "mandatory": false, + "type": "Radio", + "options": [ + { + "label": "I prefer not to say", + "value": "I prefer not to say" + }, + { + "label": "I have never worked", + "value": "I have never worked" + } + ] + } + ] + } + } + ] + } + ] + } + ] +} diff --git a/schemas/test/en/test_new_calculated_summary.json b/schemas/test/en/test_new_calculated_summary.json new file mode 100644 index 0000000000..9e10f5cd04 --- /dev/null +++ b/schemas/test/en/test_new_calculated_summary.json @@ -0,0 +1,571 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "A test schema to demo Calculated Summary", + "theme": "default", + "description": "A schema to showcase Calculated Summary pages and usage in value source.", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Linear", + "options": { + "summary": { + "collapsible": false + } + } + }, + "sections": [ + { + "id": "default-section", + "groups": [ + { + "id": "group", + "title": "Total a range of values", + "blocks": [ + { + "type": "Question", + "id": "first-number-block", + "question": { + "id": "first-number-question", + "title": "First Number Question Title", + "type": "General", + "answers": [ + { + "id": "first-number-answer", + "label": "First answer label", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2, + "minimum": { + "value": -1000, + "exclusive": false + } + } + ] + } + }, + { + "type": "Question", + "id": "second-number-block", + "question": { + "id": "second-number-question", + "title": "Second Number Question Title", + "type": "General", + "answers": [ + { + "id": "second-number-answer", + "label": "Second answer in currency label", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2, + "minimum": { + "value": -1000, + "exclusive": false + } + }, + { + "id": "second-number-answer-unit-total", + "label": "Second answer label in unit total", + "mandatory": true, + "type": "Unit", + "unit_length": "short", + "unit": "length-centimeter" + }, + { + "id": "second-number-answer-also-in-total", + "label": "Second answer label also in currency total (optional)", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2, + "minimum": { + "value": -1000, + "exclusive": false + } + } + ] + } + }, + { + "type": "Question", + "id": "third-number-block", + "question": { + "id": "third-number-question", + "title": "Third Number Question Title", + "type": "General", + "answers": [ + { + "id": "third-number-answer", + "label": "Third answer label", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2, + "minimum": { + "value": -1000, + "exclusive": false + } + } + ] + } + }, + { + "type": "Question", + "id": "third-and-a-half-number-block", + "question": { + "id": "third-and-a-half-number-question-unit-total", + "title": "Third Number Question Title Unit Total", + "type": "General", + "answers": [ + { + "id": "third-and-a-half-number-answer-unit-total", + "label": "Third answer label in unit total", + "mandatory": true, + "type": "Unit", + "unit_length": "short", + "unit": "length-centimeter" + } + ] + } + }, + { + "type": "Question", + "id": "skip-fourth-block", + "question": { + "type": "General", + "id": "skip-fourth-block-question", + "title": "Skip Fourth Block so it doesn’t appear in Total?", + "answers": [ + { + "type": "Radio", + "id": "skip-fourth-block-answer", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + { + "skip_conditions": { + "when": { + "==": [ + { + "identifier": "skip-fourth-block-answer", + "source": "answers" + }, + "Yes" + ] + } + }, + "type": "Question", + "id": "fourth-number-block", + "question": { + "id": "fourth-number-question", + "title": "Fourth Number Question Title", + "type": "General", + "answers": [ + { + "id": "fourth-number-answer", + "label": "Fourth answer label (optional)", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "skip_conditions": { + "when": { + "==": [ + { + "identifier": "skip-fourth-block-answer", + "source": "answers" + }, + "Yes" + ] + } + }, + "type": "Question", + "id": "fourth-and-a-half-number-block", + "question": { + "id": "fourth-and-a-half-number-question-also-in-total", + "title": "Fourth Number Additional Question Title", + "type": "General", + "answers": [ + { + "id": "fourth-and-a-half-number-answer-also-in-total", + "label": "Fourth answer label also in total (optional)", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "Question", + "id": "fifth-number-block", + "question": { + "id": "fifth-number-question", + "title": "Fifth Number Question Title Percentage", + "type": "General", + "answers": [ + { + "id": "fifth-percent-answer", + "label": "Fifth answer label percentage total", + "mandatory": true, + "type": "Percentage", + "maximum": { + "value": 100 + } + }, + { + "id": "fifth-number-answer", + "label": "Fifth answer label number total", + "mandatory": false, + "type": "Number", + "decimal_places": 2 + } + ] + } + }, + { + "type": "Question", + "id": "sixth-number-block", + "question": { + "id": "sixth-number-question", + "title": "Sixth Number Question Title Percentage", + "type": "General", + "answers": [ + { + "id": "sixth-percent-answer", + "label": "Sixth answer label percentage total", + "mandatory": true, + "type": "Percentage", + "maximum": { + "value": 100 + } + }, + { + "id": "sixth-number-answer", + "label": "Sixth answer label number total", + "mandatory": false, + "type": "Number", + "decimal_places": 2 + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "currency-total-playback", + "title": "We calculate the total of currency values entered to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "answers", + "identifier": "first-number-answer" + }, + { + "source": "answers", + "identifier": "second-number-answer" + }, + { + "source": "answers", + "identifier": "second-number-answer-also-in-total" + }, + { + "source": "answers", + "identifier": "third-number-answer" + }, + { + "source": "answers", + "identifier": "fourth-number-answer" + }, + { + "source": "answers", + "identifier": "fourth-and-a-half-number-answer-also-in-total" + } + ] + }, + "title": "Grand total of previous values" + } + }, + { + "type": "CalculatedSummary", + "id": "unit-total-playback", + "title": "We calculate the total of unit values entered to be %(total)s. Is this correct?", + "page_title": "Total Unit Values", + "calculation": { + "operation": { + "+": [ + { + "source": "answers", + "identifier": "second-number-answer-unit-total" + }, + { + "source": "answers", + "identifier": "third-and-a-half-number-answer-unit-total" + } + ] + }, + "title": "Grand total of previous values" + } + }, + { + "type": "CalculatedSummary", + "id": "percentage-total-playback", + "title": "We calculate the total of percentage values entered to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "answers", + "identifier": "fifth-percent-answer" + }, + { + "source": "answers", + "identifier": "sixth-percent-answer" + } + ] + }, + "title": "Grand total of previous values" + } + }, + { + "type": "CalculatedSummary", + "id": "number-total-playback", + "title": "We calculate the total of number values entered to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "answers", + "identifier": "fifth-number-answer" + }, + { + "source": "answers", + "identifier": "sixth-number-answer" + } + ] + }, + "title": "Grand total of previous values" + } + }, + { + "type": "Interstitial", + "id": "calculated-summary-total-confirmation", + "content": { + "title": "You have provided the following grand totals.", + "contents": [ + { + "list": [ + { + "text": "Total currency values: {currency_total}", + "placeholders": [ + { + "placeholder": "currency_total", + "transforms": [ + { + "transform": "format_currency", + "arguments": { + "number": { + "source": "calculated_summary", + "identifier": "currency-total-playback" + } + } + } + ] + } + ] + }, + { + "text": "Total unformatted unit values: {unit_total}", + "placeholders": [ + { + "placeholder": "unit_total", + "transforms": [ + { + "transform": "format_number", + "arguments": { + "number": { + "source": "calculated_summary", + "identifier": "unit-total-playback" + } + } + } + ] + } + ] + }, + { + "text": "Total formatted unit values: {unit_total}", + "placeholders": [ + { + "placeholder": "unit_total", + "transforms": [ + { + "transform": "format_unit", + "arguments": { + "value": { + "source": "calculated_summary", + "identifier": "unit-total-playback" + }, + "unit": "length-centimeter", + "unit_length": "short" + } + } + ] + } + ] + }, + { + "text": "Total unformatted percentage values: {percentage_total}", + "placeholders": [ + { + "placeholder": "percentage_total", + "transforms": [ + { + "transform": "format_number", + "arguments": { + "number": { + "source": "calculated_summary", + "identifier": "percentage-total-playback" + } + } + } + ] + } + ] + }, + { + "text": "Total formatted percentage values: {percentage_total}", + "placeholders": [ + { + "placeholder": "percentage_total", + "transforms": [ + { + "transform": "format_percentage", + "arguments": { + "value": { + "source": "calculated_summary", + "identifier": "percentage-total-playback" + } + } + } + ] + } + ] + }, + { + "text": "Total number values: {number_total}", + "placeholders": [ + { + "placeholder": "number_total", + "transforms": [ + { + "transform": "format_number", + "arguments": { + "number": { + "source": "calculated_summary", + "identifier": "number-total-playback" + } + } + } + ] + } + ] + } + ] + } + ] + } + }, + { + "type": "Question", + "id": "set-min-max-block", + "question": { + "answers": [ + { + "id": "set-minimum-answer", + "label": "Set a value greater than the total above", + "mandatory": true, + "description": "This is a description of the minimum value", + "type": "Currency", + "currency": "GBP", + "decimal_places": 2, + "minimum": { + "value": { + "source": "calculated_summary", + "identifier": "currency-total-playback" + } + } + }, + { + "id": "set-maximum-answer", + "description": "This is a description of the maximum value", + "label": "Set a value less than the total above", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2, + "maximum": { + "value": { + "source": "calculated_summary", + "identifier": "currency-total-playback" + } + } + } + ], + "id": "set-min-question", + "title": { + "placeholders": [ + { + "placeholder": "calculated_summary_answer", + "value": { + "identifier": "currency-total-playback", + "source": "calculated_summary" + } + } + ], + "text": "Set minimum and maximum values based on your calculated summary total of ÂŖ{calculated_summary_answer}" + }, + "type": "General" + } + } + ] + } + ] + } + ] +} diff --git a/schemas/test/en/test_new_calculated_summary_cross_section_dependencies.json b/schemas/test/en/test_new_calculated_summary_cross_section_dependencies.json new file mode 100644 index 0000000000..d79882b3d1 --- /dev/null +++ b/schemas/test/en/test_new_calculated_summary_cross_section_dependencies.json @@ -0,0 +1,375 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Calculated Summary Cross Section Dependencies", + "theme": "default", + "description": "A questionnaire to demo resolution of calculated summary values across sections", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Hub", + "options": {} + }, + "sections": [ + { + "id": "questions-section", + "title": "Questions", + "summary": { "show_on_completion": true }, + "groups": [ + { + "id": "radio", + "title": "Questions", + "blocks": [ + { + "type": "Question", + "id": "skip-first-block", + "question": { + "type": "General", + "id": "skip-first-block-question", + "title": "Skip First Block so it doesn’t appear in Total?", + "answers": [ + { + "type": "Radio", + "id": "skip-first-block-answer", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + { + "skip_conditions": { + "when": { + "==": [ + { + "identifier": "skip-first-block-answer", + "source": "answers" + }, + "Yes" + ] + } + }, + "type": "Question", + "id": "first-number-block", + "question": { + "id": "first-number-question", + "title": "First Number Question Title", + "type": "General", + "answers": [ + { + "id": "first-number-answer", + "label": "First answer label (optional)", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "skip_conditions": { + "when": { + "==": [ + { + "identifier": "skip-first-block-answer", + "source": "answers" + }, + "Yes" + ] + } + }, + "type": "Question", + "id": "first-and-a-half-number-block", + "question": { + "id": "first-and-a-half-number-question-also-in-total", + "title": "First Number Additional Question Title", + "type": "General", + "answers": [ + { + "id": "first-and-a-half-number-answer-also-in-total", + "label": "First answer label also in total (optional)", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "Question", + "id": "second-number-block", + "question": { + "id": "second-number-question-also-in-total", + "title": "Second Number Additional Question Title", + "type": "General", + "answers": [ + { + "id": "second-number-answer-also-in-total", + "label": "Second answer label also in total (optional)", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "currency-total-playback-1", + "title": "We calculate the total of currency values entered to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "answers", + "identifier": "first-number-answer" + }, + { + "source": "answers", + "identifier": "first-and-a-half-number-answer-also-in-total" + }, + { + "source": "answers", + "identifier": "second-number-answer-also-in-total" + } + ] + }, + "title": "Grand total of previous values" + } + } + ] + } + ] + }, + { + "id": "calculated-summary-section", + "title": "Calculated Summary", + "summary": { "show_on_completion": true }, + "groups": [ + { + "blocks": [ + { + "type": "Question", + "id": "third-number-block", + "question": { + "id": "third-number-question", + "title": "Third Number Question Title", + "type": "General", + "answers": [ + { + "id": "third-number-answer", + "label": "Third answer in currency label", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + }, + { + "id": "third-number-answer-also-in-total", + "label": "Third answer label also in currency total (optional)", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "currency-total-playback-2", + "title": "We calculate the total of currency values entered to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "answers", + "identifier": "third-number-answer" + }, + { + "source": "answers", + "identifier": "third-number-answer-also-in-total" + } + ] + }, + "title": "Grand total of previous values" + } + }, + { + "type": "Question", + "id": "mutually-exclusive-checkbox", + "question": { + "id": "mutually-exclusive-checkbox-question", + "type": "MutuallyExclusive", + "title": "Which answer did you give to question 4 and a half?", + "mandatory": true, + "answers": [ + { + "id": "checkbox-answer", + "instruction": "Select an answer", + "type": "Checkbox", + "mandatory": false, + "options": [ + { + "label": { + "placeholders": [ + { + "placeholder": "answer_value_1", + "value": { + "identifier": "first-and-a-half-number-answer-also-in-total", + "source": "answers" + } + } + ], + "text": "{answer_value_1} - first and a half answer" + }, + "value": "{answer_value_1}" + }, + { + "label": { + "placeholders": [ + { + "placeholder": "calc_value_1", + "value": { + "identifier": "currency-total-playback-1", + "source": "calculated_summary" + } + } + ], + "text": "{calc_value_1} - calculated summary answer (previous section)" + }, + "value": "{calc_value_1}" + }, + { + "label": { + "placeholders": [ + { + "placeholder": "calc_value_2", + "value": { + "identifier": "currency-total-playback-2", + "source": "calculated_summary" + } + } + ], + "text": "{calc_value_2} - calculated summary answer (current section)" + }, + "value": "{calc_value_2}" + }, + { + "label": { + "placeholders": [ + { + "placeholder": "third_answer_value", + "value": { + "identifier": "third-number-answer", + "source": "answers" + } + } + ], + "text": "{third_answer_value} - third answer" + }, + "value": "{third_answer_value}" + } + ] + }, + { + "id": "checkbox-exclusive-answer", + "mandatory": false, + "type": "Checkbox", + "options": [ + { + "label": "I prefer not to say", + "description": "Some description", + "value": "I prefer not to say" + } + ] + } + ] + } + }, + { + "type": "Question", + "id": "set-min-max-block", + "question": { + "answers": [ + { + "id": "set-minimum-answer", + "label": "Set a value greater than the total above", + "mandatory": true, + "description": "This is a description of the minimum value", + "type": "Currency", + "currency": "GBP", + "decimal_places": 2, + "minimum": { + "value": { + "source": "calculated_summary", + "identifier": "currency-total-playback-1" + } + } + }, + { + "id": "set-maximum-answer", + "description": "This is a description of the maximum value", + "label": "Set a value less than the total above", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2, + "maximum": { + "value": { + "source": "calculated_summary", + "identifier": "currency-total-playback-1" + } + } + } + ], + "id": "set-min-question", + "title": { + "placeholders": [ + { + "placeholder": "calculated_summary_answer", + "value": { + "identifier": "currency-total-playback-1", + "source": "calculated_summary" + } + } + ], + "text": "Set minimum and maximum values based on your calculated summary total of ÂŖ{calculated_summary_answer}" + }, + "type": "General" + } + } + ], + "id": "calculated-summary" + } + ] + } + ] +} diff --git a/schemas/test/en/test_new_calculated_summary_cross_section_dependencies_repeating.json b/schemas/test/en/test_new_calculated_summary_cross_section_dependencies_repeating.json new file mode 100644 index 0000000000..5bfb54b9a2 --- /dev/null +++ b/schemas/test/en/test_new_calculated_summary_cross_section_dependencies_repeating.json @@ -0,0 +1,603 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Calculated Summary Cross Section Dependencies", + "theme": "default", + "description": "A questionnaire to demo resolution of calculated summary values across sections", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Hub", + "options": {} + }, + "sections": [ + { + "id": "section", + "title": "Household", + "groups": [ + { + "id": "group", + "title": "List", + "blocks": [ + { + "id": "primary-person-list-collector", + "type": "PrimaryPersonListCollector", + "for_list": "people", + "add_or_edit_block": { + "id": "add-or-edit-primary-person", + "type": "PrimaryPersonListAddOrEditQuestion", + "question": { + "id": "primary-person-add-or-edit-question", + "type": "General", + "title": "What is your name?", + "answers": [ + { + "id": "first-name", + "label": "First name", + "mandatory": true, + "type": "TextField" + }, + { + "id": "last-name", + "label": "Last name", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "question": { + "id": "primary-confirmation-question", + "type": "General", + "title": "Do you live here?", + "answers": [ + { + "id": "you-live-here", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + { + "id": "list-collector", + "type": "ListCollector", + "for_list": "people", + "question": { + "id": "confirmation-question", + "type": "General", + "title": "Does anyone else live here?", + "answers": [ + { + "id": "anyone-else", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "add_block": { + "id": "add-person", + "type": "ListAddQuestion", + "question": { + "id": "add-question", + "type": "General", + "title": "What is the name of the person?", + "answers": [ + { + "id": "first-name", + "label": "First name", + "mandatory": true, + "type": "TextField" + }, + { + "id": "last-name", + "label": "Last name", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "edit_block": { + "id": "edit-person", + "type": "ListEditQuestion", + "question": { + "id": "edit-question", + "type": "General", + "title": "What is the name of the person?", + "answers": [ + { + "id": "first-name", + "label": "First name", + "mandatory": true, + "type": "TextField" + }, + { + "id": "last-name", + "label": "Last name", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "remove_block": { + "id": "remove-person", + "type": "ListRemoveQuestion", + "question": { + "id": "remove-question", + "type": "General", + "title": "Are you sure you want to remove this person?", + "answers": [ + { + "id": "remove-confirmation", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RemoveListItemAndAnswers" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + "summary": { + "title": "Household members", + "item_title": { + "text": "{person_name}", + "placeholders": [ + { + "placeholder": "person_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "first-name" + }, + { + "source": "answers", + "identifier": "last-name" + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + } + } + } + ] + } + ] + }, + { + "id": "questions-section", + "title": "Questions", + "summary": { "show_on_completion": true }, + "groups": [ + { + "id": "radio", + "title": "Questions", + "blocks": [ + { + "type": "Question", + "id": "skip-first-block", + "question": { + "type": "General", + "id": "skip-first-block-question", + "title": "Skip First Block so it doesn’t appear in Total?", + "answers": [ + { + "type": "Radio", + "id": "skip-first-block-answer", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + { + "skip_conditions": { + "when": { + "==": [ + { + "identifier": "skip-first-block-answer", + "source": "answers" + }, + "Yes" + ] + } + }, + "type": "Question", + "id": "first-number-block", + "question": { + "id": "first-number-question", + "title": "First Number Question Title", + "type": "General", + "answers": [ + { + "id": "first-number-answer", + "label": "First answer label (optional)", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "skip_conditions": { + "when": { + "==": [ + { + "identifier": "skip-first-block-answer", + "source": "answers" + }, + "Yes" + ] + } + }, + "type": "Question", + "id": "first-and-a-half-number-block", + "question": { + "id": "first-and-a-half-number-question-also-in-total", + "title": "First Number Additional Question Title", + "type": "General", + "answers": [ + { + "id": "first-and-a-half-number-answer-also-in-total", + "label": "First answer label also in total (optional)", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "Question", + "id": "second-number-block", + "question": { + "id": "second-number-question-also-in-total", + "title": "Second Number Additional Question Title", + "type": "General", + "answers": [ + { + "id": "second-number-answer-also-in-total", + "label": "Second answer label also in total (optional)", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "currency-total-playback-1", + "title": "We calculate the total of currency values entered to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "answers", + "identifier": "first-number-answer" + }, + { + "source": "answers", + "identifier": "first-and-a-half-number-answer-also-in-total" + }, + { + "source": "answers", + "identifier": "second-number-answer-also-in-total" + } + ] + }, + "title": "Grand total of previous values" + } + } + ] + } + ] + }, + { + "id": "calculated-summary-section", + "title": "Calculated Summary", + "summary": { "show_on_completion": true }, + "repeat": { + "for_list": "people", + "title": { + "text": "{person_name}", + "placeholders": [ + { + "placeholder": "person_name", + "transforms": [ + { + "transform": "concatenate_list", + "arguments": { + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "first-name" + }, + { + "source": "answers", + "identifier": "last-name" + } + ], + "delimiter": " " + } + } + ] + } + ] + } + }, + "groups": [ + { + "blocks": [ + { + "type": "Question", + "id": "third-number-block", + "question": { + "id": "third-number-question", + "title": "Third Number Question Title", + "type": "General", + "answers": [ + { + "id": "third-number-answer", + "label": "Third answer in currency label", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + }, + { + "id": "third-number-answer-also-in-total", + "label": "Third answer label also in currency total (optional)", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "currency-total-playback-2", + "title": "We calculate the total of currency values entered to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "answers", + "identifier": "third-number-answer" + }, + { + "source": "answers", + "identifier": "third-number-answer-also-in-total" + } + ] + }, + "title": "Grand total of previous values" + } + }, + { + "type": "Question", + "id": "mutually-exclusive-checkbox", + "question": { + "id": "mutually-exclusive-checkbox-question", + "type": "MutuallyExclusive", + "title": "Which answer did you give to question 4 and a half?", + "mandatory": false, + "answers": [ + { + "id": "checkbox-answer", + "instruction": "Select an answer", + "type": "Checkbox", + "mandatory": false, + "options": [ + { + "label": { + "placeholders": [ + { + "placeholder": "answer_value_1", + "value": { + "identifier": "first-and-a-half-number-answer-also-in-total", + "source": "answers" + } + } + ], + "text": "{answer_value_1} - first and a half answer" + }, + "value": "{answer_value_1}" + }, + { + "label": { + "placeholders": [ + { + "placeholder": "calc_value_1", + "value": { + "identifier": "currency-total-playback-1", + "source": "calculated_summary" + } + } + ], + "text": "{calc_value_1} - calculated summary answer (previous section)" + }, + "value": "{calc_value_1}" + }, + { + "label": { + "placeholders": [ + { + "placeholder": "calc_value_2", + "value": { + "identifier": "currency-total-playback-2", + "source": "calculated_summary" + } + } + ], + "text": "{calc_value_2} - calculated summary answer (current section)" + }, + "value": "{calc_value_2}" + }, + { + "label": { + "placeholders": [ + { + "placeholder": "third_answer_value", + "value": { + "identifier": "third-number-answer", + "source": "answers" + } + } + ], + "text": "{third_answer_value} - third answer" + }, + "value": "{third_answer_value}" + } + ] + }, + { + "id": "checkbox-exclusive-answer", + "mandatory": false, + "type": "Checkbox", + "options": [ + { + "label": "I prefer not to say", + "description": "Some description", + "value": "I prefer not to say" + } + ] + } + ] + } + }, + { + "type": "Question", + "id": "set-min-max-block", + "question": { + "answers": [ + { + "id": "set-minimum-answer", + "label": "Set a value greater than the total above", + "mandatory": true, + "description": "This is a description of the minimum value", + "type": "Currency", + "currency": "GBP", + "decimal_places": 2, + "minimum": { + "value": { + "source": "calculated_summary", + "identifier": "currency-total-playback-1" + } + } + }, + { + "id": "set-maximum-answer", + "description": "This is a description of the maximum value", + "label": "Set a value less than the total above", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2, + "maximum": { + "value": { + "source": "calculated_summary", + "identifier": "currency-total-playback-1" + } + } + } + ], + "id": "set-min-question", + "title": { + "placeholders": [ + { + "placeholder": "calculated_summary_answer", + "value": { + "identifier": "currency-total-playback-1", + "source": "calculated_summary" + } + } + ], + "text": "Set minimum and maximum values based on your calculated summary total of ÂŖ{calculated_summary_answer}" + }, + "type": "General" + } + } + ], + "id": "calculated-summary" + } + ] + } + ] +} diff --git a/schemas/test/en/test_new_calculated_summary_dependent_questions.json b/schemas/test/en/test_new_calculated_summary_dependent_questions.json new file mode 100644 index 0000000000..ada5453af6 --- /dev/null +++ b/schemas/test/en/test_new_calculated_summary_dependent_questions.json @@ -0,0 +1,176 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "A test schema to demo Calculated Summary", + "description": "A schema to showcase Calculated Summary with dependent questions.", + "questionnaire_flow": { + "type": "Linear", + "options": {} + }, + "sections": [ + { + "id": "default-section", + "title": "Section 1", + "summary": { + "show_on_completion": false, + "collapsible": false + }, + "show_on_hub": true, + "groups": [ + { + "id": "group-1", + "blocks": [ + { + "id": "block-1", + "type": "Question", + "question": { + "id": "question-1", + "title": "How much did you spend on food?", + "type": "General", + "answers": [ + { + "id": "answer-1", + "mandatory": true, + "type": "Currency", + "label": "Money spent on food", + "description": "Enter the full value", + "minimum": { + "value": 0, + "exclusive": true + }, + "decimal_places": 2, + "currency": "GBP" + } + ] + } + }, + { + "id": "block-2", + "type": "Question", + "question": { + "id": "question-2", + "title": "Of the money spent on food, how much did you spend on vegetables?", + "type": "General", + "answers": [ + { + "id": "answer-2", + "mandatory": true, + "type": "Currency", + "label": "Money spent on vegetables", + "description": "Enter the full value", + "minimum": { + "value": 0, + "exclusive": true + }, + "maximum": { + "value": { + "identifier": "answer-1", + "source": "answers" + }, + "exclusive": false + }, + "decimal_places": 2, + "currency": "GBP" + } + ] + } + }, + { + "id": "block-3", + "type": "Question", + "question": { + "id": "question-3", + "title": "How much did you spend on clothes?", + "type": "General", + "answers": [ + { + "id": "answer-3", + "mandatory": true, + "type": "Currency", + "label": "Money spent on clothes", + "description": "Enter the full value", + "minimum": { + "value": 0, + "exclusive": true + }, + "decimal_places": 2, + "currency": "GBP" + } + ] + } + }, + { + "id": "block-4", + "type": "Question", + "question": { + "id": "question-4", + "title": "Of the money spent on clothes, how much did you spend on shoes?", + "type": "General", + "answers": [ + { + "id": "answer-4", + "mandatory": true, + "type": "Currency", + "label": "Money spent on shoes", + "description": "Enter the full value", + "minimum": { + "value": 0, + "exclusive": true + }, + "maximum": { + "value": { + "identifier": "answer-3", + "source": "answers" + }, + "exclusive": false + }, + "decimal_places": 2, + "currency": "GBP" + } + ] + } + }, + { + "id": "calculated-summary-block", + "type": "CalculatedSummary", + "title": "We have calculated your total spend to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "answers", + "identifier": "answer-1" + }, + { + "source": "answers", + "identifier": "answer-3" + } + ] + }, + "title": "Grand total of previous values" + } + } + ] + } + ] + } + ], + "theme": "default", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ] +} diff --git a/schemas/test/en/test_new_calculated_summary_repeating_and_static_answers.json b/schemas/test/en/test_new_calculated_summary_repeating_and_static_answers.json new file mode 100644 index 0000000000..0a8a3ae325 --- /dev/null +++ b/schemas/test/en/test_new_calculated_summary_repeating_and_static_answers.json @@ -0,0 +1,727 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Calculated Summary with Dynamic Answers", + "theme": "default", + "description": "A questionnaire to demo calculated summaries which use a list of repeating answers.", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Hub", + "options": { "required_completed_sections": ["section-1"] } + }, + "sections": [ + { + "id": "section-1", + "title": "Weekly Shopping", + "summary": { + "show_on_completion": true, + "items": [ + { + "type": "List", + "for_list": "supermarkets", + "title": "Supermarkets", + "add_link_text": "Add another supermarket", + "empty_list_text": "There are no supermarkets" + } + ], + "show_non_item_answers": true + }, + "groups": [ + { + "id": "group", + "blocks": [ + { + "type": "ListCollectorDrivingQuestion", + "id": "any-supermarket", + "for_list": "supermarkets", + "question": { + "type": "General", + "id": "any-supermarket-question", + "title": "Do you visit any supermarkets for your weekly shopping?", + "answers": [ + { + "type": "Radio", + "id": "any-supermarket-answer", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock", + "params": { + "block_id": "add-supermarket", + "list_name": "supermarkets" + } + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "routing_rules": [ + { + "section": "End", + "when": { + "==": [ + { + "source": "answers", + "identifier": "any-supermarket-answer" + }, + "No" + ] + } + }, + { + "block": "list-collector" + } + ] + }, + { + "id": "list-collector", + "type": "ListCollector", + "for_list": "supermarkets", + "question": { + "id": "confirmation-question", + "type": "General", + "title": "Do you need to add any more supermarkets?", + "answers": [ + { + "id": "list-collector-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "add_block": { + "id": "add-supermarket", + "type": "ListAddQuestion", + "cancel_text": "Don’t need to add any other supermarkets?", + "question": { + "id": "add-question", + "type": "General", + "title": "Which supermarkets do you use for your weekly shopping?", + "answers": [ + { + "id": "supermarket-name", + "label": "Supermarket", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "edit_block": { + "id": "edit-supermarket", + "type": "ListEditQuestion", + "cancel_text": "Don’t need to change anything?", + "question": { + "id": "edit-question", + "type": "General", + "title": "What is the name of the supermarket?", + "answers": [ + { + "id": "supermarket-name", + "label": "Supermarket", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "remove_block": { + "id": "remove-supermarket", + "type": "ListRemoveQuestion", + "cancel_text": "Don’t need to remove this supermarket?", + "question": { + "id": "remove-question", + "type": "General", + "title": "Are you sure you want to remove this supermarket?", + "warning": "All of the information about this supermarket will be deleted", + "answers": [ + { + "id": "remove-confirmation", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RemoveListItemAndAnswers" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + "summary": { + "title": "Supermarkets", + "item_title": { + "text": "{supermarket_name}", + "placeholders": [ + { + "placeholder": "supermarket_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "supermarket-name" + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + } + } + }, + { + "type": "Question", + "id": "dynamic-answer", + "skip_conditions": { + "when": { + "==": [ + { + "count": [ + { + "source": "list", + "identifier": "supermarkets" + } + ] + }, + 0 + ] + } + }, + "question": { + "dynamic_answers": { + "values": { + "source": "list", + "identifier": "supermarkets" + }, + "answers": [ + { + "label": { + "text": "How much do you spend on groceries at {transformed_value}?", + "placeholders": [ + { + "placeholder": "transformed_value", + "value": { + "source": "answers", + "identifier": "supermarket-name" + } + } + ] + }, + "id": "cost-of-shopping", + "type": "Currency", + "mandatory": true, + "currency": "GBP", + "decimal_places": 2 + }, + { + "label": { + "text": "How much do you spend on other items at {transformed_value}?", + "placeholders": [ + { + "placeholder": "transformed_value", + "value": { + "source": "answers", + "identifier": "supermarket-name" + } + } + ] + }, + "id": "cost-of-other", + "type": "Currency", + "mandatory": true, + "currency": "GBP", + "decimal_places": 2 + }, + { + "id": "days-a-week", + "label": { + "text": "How many days a week do you shop at {transformed_value}?", + "placeholders": [ + { + "placeholder": "transformed_value", + "value": { + "source": "answers", + "identifier": "supermarket-name" + } + } + ] + }, + "mandatory": false, + "type": "Number", + "decimal_places": 0, + "minimum": { + "value": 1 + }, + "maximum": { + "value": 7 + } + } + ] + }, + "answers": [ + { + "id": "based-checkbox-answer", + "label": "Are supermarkets UK or non UK based?", + "instruction": "Select any answers that apply", + "mandatory": false, + "options": [ + { + "label": "UK based supermarkets", + "value": "UK based supermarkets" + }, + { + "label": "Non UK based supermarkets", + "value": "Non UK based supermarkets" + } + ], + "type": "Checkbox" + }, + { + "id": "extra-static-answer", + "label": "How much do you spend on food at other types of shop?", + "type": "Currency", + "mandatory": false, + "currency": "GBP", + "decimal_places": 2 + } + ], + "id": "dynamic-answer-question", + "title": "How much do you spend each week at each of the following supermarket?", + "type": "General" + } + }, + { + "type": "Question", + "id": "extra-spending-block", + "question": { + "id": "extra-spending-question", + "title": "How much extra money do you spend each week on online food shopping?", + "type": "General", + "guidance": { + "contents": [ + { + "title": "How to test", + "list": [ + "If you enter a value other than ÂŖ0 an additional question opens up.", + "Test that if you answer ÂŖ0 to this question and then edit the answer from the calculated summary change link", + "First you are taken to the new question which opens up (provided it isn’t already complete), and only then back to the calculated summary" + ] + } + ] + }, + "answers": [ + { + "id": "extra-spending-answer", + "label": "Online food shopping expenditure", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "Question", + "id": "extra-spending-method-block", + "skip_conditions": { + "when": { + "==": [ + { + "source": "answers", + "identifier": "extra-spending-answer" + }, + 0 + ] + } + }, + "question": { + "id": "extra-spending-method-question", + "title": "Do you use a mobile phone to do online food shopping?", + "type": "General", + "answers": [ + { + "id": "extra-spending-method-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "calculated-summary-spending", + "title": "We calculate the total cost of your weekly shopping to be %(total)s. Is this correct?", + "calculation": { + "title": "Weekly shopping cost", + "operation": { + "+": [ + { + "source": "answers", + "identifier": "cost-of-shopping" + }, + { + "source": "answers", + "identifier": "cost-of-other" + }, + { + "source": "answers", + "identifier": "extra-spending-answer" + }, + { + "source": "answers", + "identifier": "extra-static-answer" + } + ] + } + } + }, + { + "type": "CalculatedSummary", + "id": "calculated-summary-visits", + "title": "We calculate the total visits to the shop to be %(total)s. Is this correct?", + "calculation": { + "title": "Weekly shopping trips", + "operation": { + "+": [ + { + "source": "answers", + "identifier": "days-a-week" + } + ] + } + } + } + ] + } + ] + }, + { + "id": "section-2", + "title": "Shopping Details", + "enabled": { + "when": { + ">": [ + { + "source": "calculated_summary", + "identifier": "calculated-summary-spending" + }, + 0 + ] + } + }, + "groups": [ + { + "id": "group-2", + "blocks": [ + { + "type": "Question", + "id": "supermarket-transport", + "question": { + "id": "weekly-car-trips-question", + "title": { + "placeholders": [ + { + "placeholder": "total_visits", + "value": { + "identifier": "calculated-summary-visits", + "source": "calculated_summary" + } + } + ], + "text": "On how many of your {total_visits} weekly shopping trips do you travel by car?" + }, + "type": "General", + "answers": [ + { + "id": "weekly-car-trips-answer", + "label": "Number of visits by car", + "mandatory": true, + "description": "Cannot exceed the total weekly trips from section 1", + "type": "Number", + "decimal_places": 0, + "maximum": { + "value": { + "source": "calculated_summary", + "identifier": "calculated-summary-visits" + } + } + } + ] + } + }, + { + "type": "Question", + "id": "supermarket-transport-cost", + "skip_conditions": { + "when": { + "==": [ + { + "source": "answers", + "identifier": "weekly-car-trips-answer" + }, + 0 + ] + } + }, + "question": { + "id": "weekly-trips-cost", + "title": "How much do you spend on parking when travelling to the shop by car?", + "type": "General", + "answers": [ + { + "id": "weekly-trips-cost-answer", + "label": "Weekly spending on parking", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "Interstitial", + "id": "calculated-summary-piping", + "content_variants": [ + { + "content": { + "title": "You have provided the following information about your weekly shop.", + "contents": [ + { + "list": [ + { + "text": "Total weekly supermarket spending: {currency_total}", + "placeholders": [ + { + "placeholder": "currency_total", + "transforms": [ + { + "transform": "format_currency", + "arguments": { + "number": { + "source": "calculated_summary", + "identifier": "calculated-summary-spending" + } + } + } + ] + } + ] + }, + { + "text": "Total weekly supermarket visits: {number_total}", + "placeholders": [ + { + "placeholder": "number_total", + "transforms": [ + { + "transform": "format_number", + "arguments": { + "number": { + "source": "calculated_summary", + "identifier": "calculated-summary-visits" + } + } + } + ] + } + ] + }, + { + "text": "Total of supermarket visits by car: {number_total}", + "placeholders": [ + { + "placeholder": "number_total", + "transforms": [ + { + "transform": "format_number", + "arguments": { + "number": { + "source": "answers", + "identifier": "weekly-car-trips-answer" + } + } + } + ] + } + ] + } + ] + } + ] + }, + "when": { + "==": [ + { + "source": "answers", + "identifier": "weekly-car-trips-answer" + }, + 0 + ] + } + }, + { + "content": { + "title": "You have provided the following information about your weekly shop.", + "contents": [ + { + "list": [ + { + "text": "Total weekly supermarket spending: {currency_total}", + "placeholders": [ + { + "placeholder": "currency_total", + "transforms": [ + { + "transform": "format_currency", + "arguments": { + "number": { + "source": "calculated_summary", + "identifier": "calculated-summary-spending" + } + } + } + ] + } + ] + }, + { + "text": "Total weekly supermarket visits: {number_total}", + "placeholders": [ + { + "placeholder": "number_total", + "transforms": [ + { + "transform": "format_number", + "arguments": { + "number": { + "source": "calculated_summary", + "identifier": "calculated-summary-visits" + } + } + } + ] + } + ] + }, + { + "text": "Total of supermarket visits by car: {number_total}", + "placeholders": [ + { + "placeholder": "number_total", + "transforms": [ + { + "transform": "format_number", + "arguments": { + "number": { + "source": "answers", + "identifier": "weekly-car-trips-answer" + } + } + } + ] + } + ] + }, + { + "text": "Total spending on parking: {currency_total}", + "placeholders": [ + { + "placeholder": "currency_total", + "transforms": [ + { + "transform": "format_currency", + "arguments": { + "number": { + "source": "answers", + "identifier": "weekly-trips-cost-answer" + } + } + } + ] + } + ] + } + ] + } + ] + }, + "when": { + ">": [ + { + "source": "answers", + "identifier": "weekly-car-trips-answer" + }, + 0 + ] + } + } + ] + } + ] + } + ] + } + ] +} diff --git a/schemas/test/en/test_new_calculated_summary_repeating_answers_only.json b/schemas/test/en/test_new_calculated_summary_repeating_answers_only.json new file mode 100644 index 0000000000..40a6de8f9f --- /dev/null +++ b/schemas/test/en/test_new_calculated_summary_repeating_answers_only.json @@ -0,0 +1,322 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Calculated Summary with Dynamic Answers", + "theme": "default", + "description": "A simple demo of a calculated summary which uses a list of repeating answers.", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Linear", + "options": {} + }, + "sections": [ + { + "id": "section", + "title": "List Collector Section", + "summary": { + "show_on_completion": true, + "items": [ + { + "type": "List", + "for_list": "transport", + "title": "Transport", + "add_link_text": "Add another method of transport", + "empty_list_text": "There are no uses of public transport" + } + ], + "show_non_item_answers": true + }, + "groups": [ + { + "id": "group", + "blocks": [ + { + "type": "ListCollectorDrivingQuestion", + "id": "any-transport", + "for_list": "transport", + "question": { + "type": "General", + "id": "any-transport-question", + "title": "Do you use public transport?", + "answers": [ + { + "type": "Radio", + "id": "any-transport-answer", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock", + "params": { + "block_id": "add-transport", + "list_name": "transport" + } + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "routing_rules": [ + { + "section": "End", + "when": { + "==": [ + { + "source": "answers", + "identifier": "any-transport-answer" + }, + "No" + ] + } + }, + { + "block": "list-collector" + } + ] + }, + { + "id": "list-collector", + "type": "ListCollector", + "for_list": "transport", + "question": { + "id": "confirmation-question", + "type": "General", + "title": "Do you need to add any more types of transport?", + "answers": [ + { + "id": "list-collector-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "add_block": { + "id": "add-transport", + "type": "ListAddQuestion", + "cancel_text": "Don’t need to add any other type of transport?", + "question": { + "id": "add-question", + "type": "General", + "title": "Which types of public transport do you use?", + "answers": [ + { + "id": "transport-name", + "label": "Transport type", + "mandatory": true, + "type": "Dropdown", + "options": [ + { + "label": "Train", + "value": "Train" + }, + { + "label": "Bus", + "value": "Bus" + }, + { + "label": "Tube", + "value": "Tube" + } + ] + } + ] + } + }, + "edit_block": { + "id": "edit-transport", + "type": "ListEditQuestion", + "cancel_text": "Don’t need to change anything?", + "question": { + "id": "edit-question", + "type": "General", + "title": "What is the type of public transport?", + "answers": [ + { + "id": "transport-name", + "label": "Transport type", + "mandatory": true, + "type": "Dropdown", + "options": [ + { + "label": "Train", + "value": "Train" + }, + { + "label": "Bus", + "value": "Bus" + }, + { + "label": "Tube", + "value": "Tube" + } + ] + } + ] + } + }, + "remove_block": { + "id": "remove-transport", + "type": "ListRemoveQuestion", + "cancel_text": "Don’t need to remove this method of transport?", + "question": { + "id": "remove-question", + "type": "General", + "title": "Are you sure you want to remove this method of transport?", + "warning": "All of the information about this method of transport will be deleted", + "answers": [ + { + "id": "remove-confirmation", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RemoveListItemAndAnswers" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + "summary": { + "title": "transport", + "item_title": { + "text": "{transport_name}", + "placeholders": [ + { + "placeholder": "transport_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "transport-name" + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + } + } + }, + { + "type": "Question", + "id": "dynamic-answer", + "skip_conditions": { + "when": { + "==": [ + { + "count": [ + { + "source": "list", + "identifier": "transport" + } + ] + }, + 0 + ] + } + }, + "question": { + "dynamic_answers": { + "values": { + "source": "list", + "identifier": "transport" + }, + "answers": [ + { + "label": { + "text": "How much do you spend per month travelling by {transformed_value}?", + "placeholders": [ + { + "placeholder": "transformed_value", + "value": { + "source": "answers", + "identifier": "transport-name" + } + } + ] + }, + "id": "cost-of-transport", + "type": "Currency", + "mandatory": true, + "currency": "GBP", + "decimal_places": 2 + } + ] + }, + "id": "dynamic-answer-question", + "title": "How much do you spend per month on the following modes of public transport?", + "type": "General" + } + }, + { + "type": "CalculatedSummary", + "id": "calculated-summary-spending", + "title": "We calculate the total monthly spending on public transport to be %(total)s. Is this correct?", + "calculation": { + "title": "Monthly public transport spending", + "operation": { + "+": [ + { + "source": "answers", + "identifier": "cost-of-transport" + } + ] + } + } + } + ] + } + ] + } + ] +} diff --git a/schemas/test/en/test_new_calculated_summary_repeating_blocks.json b/schemas/test/en/test_new_calculated_summary_repeating_blocks.json new file mode 100644 index 0000000000..249413b174 --- /dev/null +++ b/schemas/test/en/test_new_calculated_summary_repeating_blocks.json @@ -0,0 +1,521 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Calculated Summary of answers from Repeating blocks", + "theme": "default", + "description": "A demo of a calculated summary which uses a answers from the repeating blocks in a list collector.", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Hub", + "options": { + "required_completed_sections": ["section-1"] + } + }, + "sections": [ + { + "id": "section-1", + "title": "Transport", + "summary": { + "show_on_completion": true, + "items": [ + { + "type": "List", + "for_list": "transport", + "title": "transport", + "item_anchor_answer_id": "transport-name", + "item_label": "Name of transport", + "add_link_text": "Add another method of transport", + "empty_list_text": "No method of transport added" + } + ], + "show_non_item_answers": true + }, + "groups": [ + { + "id": "group-1", + "blocks": [ + { + "id": "block-car", + "type": "Question", + "question": { + "id": "question-car", + "type": "General", + "title": "How much do you spend per month travelling by Car?", + "answers": [ + { + "id": "answer-car", + "label": "Monthly expenditure travelling by car", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "id": "block-skip", + "type": "Question", + "question": { + "id": "question-skip", + "type": "General", + "title": "Would you like to skip the list collector that asks about other methods of transport?", + "guidance": { + "contents": [ + { + "description": "Use this to check the calculated summary shows the correct values when the list collector is not on the path." + } + ] + }, + "answers": [ + { + "id": "answer-skip", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "routing_rules": [ + { + "block": "list-collector", + "when": { + "==": [ + { + "identifier": "answer-skip", + "source": "answers" + }, + "No" + ] + } + }, + { + "block": "calculated-summary-spending" + } + ] + }, + { + "id": "list-collector", + "type": "ListCollector", + "for_list": "transport", + "question": { + "id": "confirmation-question", + "type": "General", + "title": "Do you use any other methods of transport?", + "answers": [ + { + "id": "list-collector-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "add_block": { + "id": "add-transport", + "type": "ListAddQuestion", + "cancel_text": "Don’t need to add any other method of transport?", + "question": { + "id": "add-question", + "type": "General", + "title": "What other method of transport do you use?", + "answers": [ + { + "id": "transport-name", + "label": "Transport", + "mandatory": true, + "type": "Dropdown", + "options": [ + { + "label": "Tube", + "value": "Tube" + }, + { + "label": "Bus", + "value": "Bus" + }, + { + "label": "Train", + "value": "Train" + }, + { + "label": "Plane", + "value": "Plane" + } + ] + } + ] + } + }, + "edit_block": { + "id": "edit-transport", + "type": "ListEditQuestion", + "cancel_text": "Don’t need to add any other method of transport?", + "question": { + "id": "add-question", + "type": "General", + "title": "What other method of transport do you use?", + "answers": [ + { + "id": "transport-name", + "label": "Transport", + "mandatory": true, + "type": "Dropdown", + "options": [ + { + "label": "Tube", + "value": "Tube" + }, + { + "label": "Bus", + "value": "Bus" + }, + { + "label": "Train", + "value": "Train" + }, + { + "label": "Plane", + "value": "Plane" + } + ] + } + ] + } + }, + "remove_block": { + "id": "remove-transport", + "type": "ListRemoveQuestion", + "cancel_text": "Don’t need to remove this method of transport?", + "question": { + "id": "remove-question", + "type": "General", + "title": "Are you sure you want to remove this method of transport?", + "warning": "All of the information about this method of transport will be deleted", + "answers": [ + { + "id": "remove-confirmation", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RemoveListItemAndAnswers" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + "repeating_blocks": [ + { + "id": "transport-repeating-block-1", + "type": "ListRepeatingQuestion", + "question": { + "id": "transport-repeating-block-1-question", + "type": "General", + "title": { + "text": "Give details of your expenditure travelling by {transport_name}", + "placeholders": [ + { + "placeholder": "transport_name", + "value": { + "source": "answers", + "identifier": "transport-name" + } + } + ] + }, + "answers": [ + { + "id": "transport-company", + "label": { + "placeholders": [ + { + "placeholder": "transport_name", + "value": { + "source": "answers", + "identifier": "transport-name" + } + } + ], + "text": "Which company do primarily use for travelling by {transport_name}?" + }, + "mandatory": false, + "type": "TextField" + }, + { + "id": "transport-cost", + "label": { + "placeholders": [ + { + "placeholder": "transport_name", + "value": { + "source": "answers", + "identifier": "transport-name" + } + } + ], + "text": "Monthly season ticket expenditure for travel by {transport_name}" + }, + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + }, + { + "id": "transport-additional-cost", + "label": { + "placeholders": [ + { + "placeholder": "transport_name", + "value": { + "source": "answers", + "identifier": "transport-name" + } + } + ], + "text": "Additional monthly expenditure for travel by {transport_name}" + }, + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "id": "transport-repeating-block-2", + "type": "ListRepeatingQuestion", + "question": { + "id": "transport-repeating-block-2-question-1", + "type": "General", + "title": { + "text": "How often do you travel by {transport_name}?", + "placeholders": [ + { + "placeholder": "transport_name", + "value": { + "source": "answers", + "identifier": "transport-name" + } + } + ] + }, + "answers": [ + { + "id": "transport-count", + "label": { + "placeholders": [ + { + "placeholder": "transport_name", + "value": { + "source": "answers", + "identifier": "transport-name" + } + } + ], + "text": "Monthly journeys by {transport_name}" + }, + "mandatory": false, + "type": "Number" + } + ] + } + } + ], + "summary": { + "title": "transport", + "item_title": { + "text": "{transport_name}", + "placeholders": [ + { + "placeholder": "transport_name", + "value": { + "source": "answers", + "identifier": "transport-name" + } + } + ] + } + } + }, + { + "type": "CalculatedSummary", + "id": "calculated-summary-spending", + "title": "We calculate the total monthly expenditure on transport to be %(total)s. Is this correct?", + "calculation": { + "title": "Monthly transport expenditure", + "operation": { + "+": [ + { + "source": "answers", + "identifier": "answer-car" + }, + { + "source": "answers", + "identifier": "transport-cost" + }, + { + "source": "answers", + "identifier": "transport-additional-cost" + } + ] + } + } + }, + { + "type": "CalculatedSummary", + "id": "calculated-summary-count", + "title": "We calculate the total journeys made per month to be %(total)s. Is this correct?", + "calculation": { + "title": "Monthly journeys", + "operation": { + "+": [ + { + "source": "answers", + "identifier": "transport-count" + } + ] + } + }, + "skip_conditions": { + "when": { + "or": [ + { + "==": [ + { + "count": [ + { + "source": "list", + "identifier": "transport" + } + ] + }, + 0 + ] + }, + { + "==": [ + { + "source": "answers", + "identifier": "answer-skip" + }, + "Yes" + ] + } + ] + } + } + } + ] + } + ] + }, + { + "enabled": { + "when": { + ">": [ + { + "source": "calculated_summary", + "identifier": "calculated-summary-count" + }, + 0 + ] + } + }, + "id": "section-2", + "summary": { + "show_on_completion": true + }, + "title": "Travel Details", + "groups": [ + { + "id": "group-2", + "blocks": [ + { + "type": "Question", + "id": "family-journeys", + "question": { + "id": "family-journeys-question", + "title": { + "placeholders": [ + { + "placeholder": "total_journeys", + "value": { + "identifier": "calculated-summary-count", + "source": "calculated_summary" + } + } + ], + "text": "How many of your {total_journeys} journeys are to visit family?" + }, + "type": "General", + "answers": [ + { + "id": "family-journeys-answer", + "label": "Number of trips to visit family", + "mandatory": true, + "description": "Cannot exceed the total journeys from section 1", + "type": "Number", + "maximum": { + "value": { + "source": "calculated_summary", + "identifier": "calculated-summary-count" + } + } + } + ] + } + } + ] + } + ] + } + ] +} diff --git a/schemas/test/en/test_new_calculated_summary_repeating_section.json b/schemas/test/en/test_new_calculated_summary_repeating_section.json new file mode 100644 index 0000000000..a01eeb68cf --- /dev/null +++ b/schemas/test/en/test_new_calculated_summary_repeating_section.json @@ -0,0 +1,820 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "A test schema to demo Calculated Summary", + "theme": "default", + "description": "A schema to showcase Calculated Summary pages and usage in value source in repeating sections", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Hub", + "options": {} + }, + "sections": [ + { + "id": "section", + "title": "Household", + "groups": [ + { + "id": "group", + "title": "List", + "blocks": [ + { + "id": "primary-person-list-collector", + "type": "PrimaryPersonListCollector", + "for_list": "people", + "add_or_edit_block": { + "id": "add-or-edit-primary-person", + "type": "PrimaryPersonListAddOrEditQuestion", + "question": { + "id": "primary-person-add-or-edit-question", + "type": "General", + "title": "What is your name?", + "answers": [ + { + "id": "first-name", + "label": "First name", + "mandatory": true, + "type": "TextField" + }, + { + "id": "last-name", + "label": "Last name", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "question": { + "id": "primary-confirmation-question", + "type": "General", + "title": "Do you live here?", + "answers": [ + { + "id": "you-live-here", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + { + "id": "list-collector", + "type": "ListCollector", + "for_list": "people", + "question": { + "id": "confirmation-question", + "type": "General", + "title": "Does anyone else live here?", + "answers": [ + { + "id": "anyone-else", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "add_block": { + "id": "add-person", + "type": "ListAddQuestion", + "question": { + "id": "add-question", + "type": "General", + "title": "What is the name of the person?", + "answers": [ + { + "id": "first-name", + "label": "First name", + "mandatory": true, + "type": "TextField" + }, + { + "id": "last-name", + "label": "Last name", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "edit_block": { + "id": "edit-person", + "type": "ListEditQuestion", + "question": { + "id": "edit-question", + "type": "General", + "title": "What is the name of the person?", + "answers": [ + { + "id": "first-name", + "label": "First name", + "mandatory": true, + "type": "TextField" + }, + { + "id": "last-name", + "label": "Last name", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "remove_block": { + "id": "remove-person", + "type": "ListRemoveQuestion", + "question": { + "id": "remove-question", + "type": "General", + "title": "Are you sure you want to remove this person?", + "answers": [ + { + "id": "remove-confirmation", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RemoveListItemAndAnswers" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + "summary": { + "title": "Household members", + "item_title": { + "text": "{person_name}", + "placeholders": [ + { + "placeholder": "person_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "first-name" + }, + { + "source": "answers", + "identifier": "last-name" + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + } + } + } + ] + } + ] + }, + { + "id": "personal-details-section", + "title": "Personal Details", + "summary": { + "show_on_completion": true + }, + "repeat": { + "for_list": "people", + "title": { + "text": "{person_name}", + "placeholders": [ + { + "placeholder": "person_name", + "transforms": [ + { + "transform": "concatenate_list", + "arguments": { + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "first-name" + }, + { + "source": "answers", + "identifier": "last-name" + } + ], + "delimiter": " " + } + } + ] + } + ] + } + }, + "groups": [ + { + "id": "personal-details-group", + "title": "Personal Details", + "blocks": [ + { + "type": "Question", + "id": "first-number-block", + "question": { + "id": "first-number-question", + "title": "First Number Question Title", + "type": "General", + "answers": [ + { + "id": "first-number-answer", + "label": "First answer label", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "Question", + "id": "second-number-block", + "question": { + "id": "second-number-question", + "title": "Second Number Question Title", + "type": "General", + "answers": [ + { + "id": "second-number-answer", + "label": "Second answer in currency label", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + }, + { + "id": "second-number-answer-unit-total", + "label": "Second answer label in unit total", + "mandatory": true, + "type": "Unit", + "unit_length": "short", + "unit": "length-centimeter" + }, + { + "id": "second-number-answer-also-in-total", + "label": "Second answer label also in currency total (optional)", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "Question", + "id": "third-number-block", + "question": { + "id": "third-number-question", + "title": "Third Number Question Title", + "type": "General", + "answers": [ + { + "id": "third-number-answer", + "label": "Third answer label", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "Question", + "id": "third-and-a-half-number-block", + "question": { + "id": "third-and-a-half-number-question-unit-total", + "title": "Third Number Question Title Unit Total", + "type": "General", + "answers": [ + { + "id": "third-and-a-half-number-answer-unit-total", + "label": "Third answer label in unit total", + "mandatory": true, + "type": "Unit", + "unit_length": "short", + "unit": "length-centimeter" + } + ] + } + }, + { + "type": "Question", + "id": "skip-fourth-block", + "question": { + "type": "General", + "id": "skip-fourth-block-question", + "title": "Skip Fourth Block so it doesn’t appear in Total?", + "answers": [ + { + "type": "Radio", + "id": "skip-fourth-block-answer", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + { + "skip_conditions": { + "when": { + "==": [ + { + "identifier": "skip-fourth-block-answer", + "source": "answers" + }, + "Yes" + ] + } + }, + "type": "Question", + "id": "fourth-number-block", + "question": { + "id": "fourth-number-question", + "title": "Fourth Number Question Title", + "type": "General", + "answers": [ + { + "id": "fourth-number-answer", + "label": "Fourth answer label (optional)", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "skip_conditions": { + "when": { + "==": [ + { + "identifier": "skip-fourth-block-answer", + "source": "answers" + }, + "Yes" + ] + } + }, + "type": "Question", + "id": "fourth-and-a-half-number-block", + "question": { + "id": "fourth-and-a-half-number-question-also-in-total", + "title": "Fourth Number Additional Question Title", + "type": "General", + "answers": [ + { + "id": "fourth-and-a-half-number-answer-also-in-total", + "label": "Fourth answer label also in total (optional)", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "Question", + "id": "fifth-number-block", + "question": { + "id": "fifth-number-question", + "title": "Fifth Number Question Title Percentage", + "type": "General", + "answers": [ + { + "id": "fifth-percent-answer", + "label": "Fifth answer label percentage total", + "mandatory": true, + "type": "Percentage", + "maximum": { + "value": 100 + } + }, + { + "id": "fifth-number-answer", + "label": "Fifth answer label number total", + "mandatory": false, + "type": "Number", + "decimal_places": 2 + } + ] + } + }, + { + "type": "Question", + "id": "sixth-number-block", + "question": { + "id": "sixth-number-question", + "title": "Sixth Number Question Title Percentage", + "type": "General", + "answers": [ + { + "id": "sixth-percent-answer", + "label": "Sixth answer label percentage total", + "mandatory": true, + "type": "Percentage", + "maximum": { + "value": 100 + } + }, + { + "id": "sixth-number-answer", + "label": "Sixth answer label number total", + "mandatory": false, + "type": "Number", + "decimal_places": 2 + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "currency-total-playback", + "title": "We calculate the total of currency values entered to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "answers", + "identifier": "first-number-answer" + }, + { + "source": "answers", + "identifier": "second-number-answer" + }, + { + "source": "answers", + "identifier": "second-number-answer-also-in-total" + }, + { + "source": "answers", + "identifier": "third-number-answer" + }, + { + "source": "answers", + "identifier": "fourth-number-answer" + }, + { + "source": "answers", + "identifier": "fourth-and-a-half-number-answer-also-in-total" + } + ] + }, + "title": "Grand total of previous values" + } + }, + { + "type": "CalculatedSummary", + "id": "unit-total-playback", + "title": "We calculate the total of unit values entered to be %(total)s. Is this correct?", + "page_title": "Total Unit Values", + "calculation": { + "operation": { + "+": [ + { + "source": "answers", + "identifier": "second-number-answer-unit-total" + }, + { + "source": "answers", + "identifier": "third-and-a-half-number-answer-unit-total" + } + ] + }, + "title": "Grand total of previous values" + } + }, + { + "type": "CalculatedSummary", + "id": "percentage-total-playback", + "title": "We calculate the total of percentage values entered to be %(total)s. Is this correct?", + "page_title": "Percentage Calculated Summary: Person {list_item_position}", + "calculation": { + "operation": { + "+": [ + { + "source": "answers", + "identifier": "fifth-percent-answer" + }, + { + "source": "answers", + "identifier": "sixth-percent-answer" + } + ] + }, + "title": "Grand total of previous values" + } + }, + { + "type": "CalculatedSummary", + "id": "number-total-playback", + "title": "We calculate the total of number values entered to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "answers", + "identifier": "fifth-number-answer" + }, + { + "source": "answers", + "identifier": "sixth-number-answer" + } + ] + }, + "title": "Grand total of previous values" + } + }, + { + "id": "breakdown", + "type": "Question", + "question": { + "id": "breakdown-question", + "title": { + "text": "Enter two values that add up to the previous calculated summary total of {total_first}", + "placeholders": [ + { + "placeholder": "total_first", + "transforms": [ + { + "transform": "format_currency", + "arguments": { + "number": { + "identifier": "number-total-playback", + "source": "calculated_summary" + } + } + } + ] + } + ] + }, + "type": "General", + "answers": [ + { + "id": "breakdown-answer-1", + "mandatory": false, + "type": "Currency", + "label": "First Value", + "decimal_places": 2, + "currency": "GBP" + }, + { + "id": "breakdown-answer-2", + "mandatory": false, + "type": "Currency", + "label": "Second Value", + "decimal_places": 2, + "currency": "GBP" + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "second-currency-total-playback", + "title": "We calculate the total of number values entered to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "answers", + "identifier": "breakdown-answer-1" + }, + { + "source": "answers", + "identifier": "breakdown-answer-2" + } + ] + }, + "title": "Grand total of previous values" + } + }, + { + "type": "Interstitial", + "id": "calculated-summary-total-confirmation", + "content": { + "title": "You have provided the following grand totals.", + "contents": [ + { + "list": [ + { + "text": "Total currency values: {currency_total}", + "placeholders": [ + { + "placeholder": "currency_total", + "transforms": [ + { + "transform": "format_currency", + "arguments": { + "number": { + "source": "calculated_summary", + "identifier": "currency-total-playback" + } + } + } + ] + } + ] + }, + { + "text": "Total unit values: {unit_total}", + "placeholders": [ + { + "placeholder": "unit_total", + "transforms": [ + { + "transform": "format_number", + "arguments": { + "number": { + "source": "calculated_summary", + "identifier": "unit-total-playback" + } + } + } + ] + } + ] + }, + { + "text": "Total percentage values: {percentage_total}", + "placeholders": [ + { + "placeholder": "percentage_total", + "transforms": [ + { + "transform": "format_number", + "arguments": { + "number": { + "source": "calculated_summary", + "identifier": "percentage-total-playback" + } + } + } + ] + } + ] + }, + { + "text": "Total number values: {number_total}", + "placeholders": [ + { + "placeholder": "number_total", + "transforms": [ + { + "transform": "format_number", + "arguments": { + "number": { + "source": "calculated_summary", + "identifier": "number-total-playback" + } + } + } + ] + } + ] + } + ] + } + ] + } + }, + { + "type": "Question", + "id": "set-min-max-block", + "skip_conditions": { + "when": { + "==": [ + { + "identifier": "skip-fourth-block-answer", + "source": "answers" + }, + "No" + ] + } + }, + "question": { + "answers": [ + { + "id": "set-minimum-answer", + "label": "Set a value greater than the total above", + "mandatory": true, + "description": "This is a description of the minimum value", + "type": "Currency", + "currency": "GBP", + "decimal_places": 2, + "minimum": { + "value": { + "source": "calculated_summary", + "identifier": "currency-total-playback" + } + } + }, + { + "id": "set-maximum-answer", + "description": "This is a description of the maximum value", + "label": "Set a value less than the total above", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2, + "maximum": { + "value": { + "source": "calculated_summary", + "identifier": "currency-total-playback" + } + } + } + ], + "id": "set-min-question", + "title": { + "placeholders": [ + { + "placeholder": "calculated_summary_answer", + "value": { + "identifier": "currency-total-playback", + "source": "calculated_summary" + } + } + ], + "text": "Set minimum and maximum values based on your calculated summary total of ÂŖ{calculated_summary_answer}" + }, + "type": "General" + } + } + ] + } + ] + } + ] +} diff --git a/schemas/test/en/test_new_hub_section_required_and_enabled.json b/schemas/test/en/test_new_hub_section_required_and_enabled.json deleted file mode 100644 index 6320062ce6..0000000000 --- a/schemas/test/en/test_new_hub_section_required_and_enabled.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "mime_type": "application/json/ons/eq", - "language": "en", - "schema_version": "0.0.1", - "data_version": "0.0.3", - "survey_id": "0", - "title": "Hub & Spoke required and enabled sections", - "theme": "default", - "description": "A questionnaire to demo hub and spoke required and enabled sections", - "metadata": [ - { - "name": "user_id", - "type": "string" - }, - { - "name": "period_id", - "type": "string" - }, - { - "name": "ru_name", - "type": "string" - } - ], - "questionnaire_flow": { - "type": "Hub", - "options": { "required_completed_sections": ["household-section", "relationships-section"] } - }, - "sections": [ - { - "id": "household-section", - "title": "Household", - "groups": [ - { - "id": "radio", - "title": "Household Relationships", - "blocks": [ - { - "type": "Question", - "id": "household-relationships-block", - "question": { - "type": "General", - "id": "household-relationships-question", - "title": "Is anyone related in this household?", - "answers": [ - { - "type": "Radio", - "id": "household-relationships-answer", - "mandatory": true, - "options": [ - { - "label": "Yes", - "value": "Yes" - }, - { - "label": "No", - "value": "No" - } - ] - } - ] - } - } - ] - } - ] - }, - { - "id": "relationships-section", - "title": "Relationships", - "show_on_hub": false, - "groups": [ - { - "blocks": [ - { - "id": "relationships-count", - "question": { - "type": "General", - "id": "relationships-count-question", - "title": "How many people are related?", - "answers": [ - { - "type": "Radio", - "id": "relationships-count-answer", - "mandatory": true, - "options": [ - { - "label": "1", - "value": "1" - }, - { - "label": "2", - "value": "2" - }, - { - "label": "3+", - "value": "3+" - } - ] - } - ] - }, - "type": "Question" - } - ], - "id": "relationships-count-group", - "title": "Relationships count" - } - ], - "enabled": { - "when": { - "==": [ - "Yes", - { - "source": "answers", - "identifier": "household-relationships-answer" - } - ] - } - } - } - ] -} diff --git a/schemas/test/en/test_new_routing_date_equals.json b/schemas/test/en/test_new_routing_date_equals.json deleted file mode 100644 index e9b8fae0da..0000000000 --- a/schemas/test/en/test_new_routing_date_equals.json +++ /dev/null @@ -1,226 +0,0 @@ -{ - "mime_type": "application/json/ons/eq", - "language": "en", - "schema_version": "0.0.1", - "data_version": "0.0.3", - "survey_id": "001", - "title": "Test Routing Date Equals", - "theme": "default", - "description": "A test survey for routing based on equal dates", - "metadata": [ - { - "name": "user_id", - "type": "string" - }, - { - "name": "period_id", - "type": "string" - }, - { - "name": "ru_name", - "type": "string" - } - ], - "questionnaire_flow": { - "type": "Linear", - "options": { - "summary": { - "collapsible": false - } - } - }, - "sections": [ - { - "id": "default-section", - "groups": [ - { - "blocks": [ - { - "type": "Question", - "id": "comparison-date-block", - "question": { - "answers": [ - { - "id": "comparison-date-answer", - "mandatory": true, - "q_code": "11", - "type": "Date" - } - ], - "id": "comparison-date-question", - "title": "Title", - "type": "General", - "guidance": { - "contents": [ - { - "title": "If you enter 31/03/2018 the following dates will be valid", - "list": [ - "Yesterday 30/03/2018", - "Today 31/03/2018", - "Tomorrow 01/04/2018", - "Last Month 28/02/2018 (28th as no 31st February)", - "Next Month 30/04/2018 (30th as no 31st April)", - "Last Year 31/03/2017", - "Next Year 31/03/2019" - ] - } - ] - } - } - }, - { - "type": "Question", - "id": "date-question", - "question": { - "answers": [ - { - "id": "single-date-answer", - "label": "Today", - "mandatory": true, - "type": "Date" - } - ], - "id": "date-questions", - "title": { - "text": "Enter {date} or offset by one day, month or year in either direction", - "placeholders": [ - { - "placeholder": "date", - "transforms": [ - { - "transform": "format_date", - "arguments": { - "date_to_format": { - "source": "answers", - "identifier": "comparison-date-answer" - }, - "date_format": "d MMMM yyyy" - } - } - ] - } - ] - }, - "type": "General" - }, - "routing_rules": [ - { - "block": "correct-answer", - "when": { - "or": [ - { - "==": [ - { - "date": [{ "source": "answers", "identifier": "single-date-answer" }] - }, - { - "date": [{ "source": "answers", "identifier": "comparison-date-answer" }, { "days": -1 }] - } - ] - }, - { - "==": [ - { - "date": [{ "source": "answers", "identifier": "single-date-answer" }] - }, - { - "date": [{ "source": "answers", "identifier": "comparison-date-answer" }] - } - ] - }, - { - "==": [ - { - "date": [{ "source": "answers", "identifier": "single-date-answer" }] - }, - { - "date": [{ "source": "answers", "identifier": "comparison-date-answer" }, { "days": 1 }] - } - ] - }, - { - "==": [ - { - "date": [{ "source": "answers", "identifier": "single-date-answer" }] - }, - { - "date": [{ "source": "answers", "identifier": "comparison-date-answer" }, { "months": -1 }] - } - ] - }, - { - "==": [ - { - "date": [{ "source": "answers", "identifier": "single-date-answer" }] - }, - { - "date": [{ "source": "answers", "identifier": "comparison-date-answer" }, { "months": 1 }] - } - ] - }, - { - "==": [ - { - "date": [{ "source": "answers", "identifier": "single-date-answer" }] - }, - { - "date": [{ "source": "answers", "identifier": "comparison-date-answer" }, { "years": -1 }] - } - ] - }, - { - "==": [ - { - "date": [{ "source": "answers", "identifier": "single-date-answer" }] - }, - { - "date": [{ "source": "answers", "identifier": "comparison-date-answer" }, { "years": 1 }] - } - ] - } - ] - } - }, - { - "block": "incorrect-answer" - } - ] - }, - { - "type": "Interstitial", - "id": "incorrect-answer", - "content": { - "title": "Incorrect Date", - "contents": [ - { - "description": "You entered an incorrect date" - } - ] - }, - "routing_rules": [ - { - "goto": { - "section": "End" - } - } - ] - }, - { - "type": "Interstitial", - "id": "correct-answer", - "content": { - "title": "Correct Date", - "contents": [ - { - "description": "You entered a correct date." - } - ] - } - } - ], - "id": "group" - } - ] - } - ] -} diff --git a/schemas/test/en/test_new_routing_date_greater_than.json b/schemas/test/en/test_new_routing_date_greater_than.json deleted file mode 100644 index cecc09776f..0000000000 --- a/schemas/test/en/test_new_routing_date_greater_than.json +++ /dev/null @@ -1,168 +0,0 @@ -{ - "mime_type": "application/json/ons/eq", - "language": "en", - "schema_version": "0.0.1", - "data_version": "0.0.3", - "survey_id": "001", - "title": "Test Routing Date Greater Than", - "theme": "default", - "description": "A test survey for routing based on a date greater than", - "metadata": [ - { - "name": "user_id", - "type": "string" - }, - { - "name": "period_id", - "type": "string" - }, - { - "name": "ru_name", - "type": "string" - }, - { - "name": "return_by", - "type": "date" - } - ], - "questionnaire_flow": { - "type": "Linear", - "options": { - "summary": { - "collapsible": false - } - } - }, - "sections": [ - { - "id": "default-section", - "groups": [ - { - "blocks": [ - { - "type": "Question", - "id": "date-question", - "question": { - "answers": [ - { - "id": "single-date-answer", - "mandatory": true, - "type": "Date" - } - ], - "id": "date-questions", - "title": { - "text": "Enter a date greater than Return date: {date}", - "placeholders": [ - { - "placeholder": "date", - "transforms": [ - { - "transform": "format_date", - "arguments": { - "date_to_format": { - "source": "metadata", - "identifier": "return_by" - }, - "date_format": "d MMMM yyyy" - } - } - ] - } - ] - }, - "type": "General" - }, - "routing_rules": [ - { - "block": "correct-answer", - "when": { - ">": [ - { - "date": [{ "source": "answers", "identifier": "single-date-answer" }] - }, - { - "date": [{ "source": "metadata", "identifier": "return_by" }] - } - ] - } - }, - { - "block": "incorrect-answer" - } - ] - }, - { - "type": "Interstitial", - "id": "incorrect-answer", - "content": { - "title": "Incorrect answer", - "contents": [ - { - "description": { - "text": "You entered a return date earlier than {date}", - "placeholders": [ - { - "placeholder": "date", - "transforms": [ - { - "transform": "format_date", - "arguments": { - "date_to_format": { - "source": "metadata", - "identifier": "return_by" - }, - "date_format": "d MMMM yyyy" - } - } - ] - } - ] - } - } - ] - }, - "routing_rules": [ - { - "section": "End" - } - ] - }, - { - "type": "Interstitial", - "id": "correct-answer", - "content": { - "title": "Correct answer", - "contents": [ - { - "description": { - "text": "You entered a return date later than {date}", - "placeholders": [ - { - "placeholder": "date", - "transforms": [ - { - "transform": "format_date", - "arguments": { - "date_to_format": { - "source": "metadata", - "identifier": "return_by" - }, - "date_format": "d MMMM yyyy" - } - } - ] - } - ] - } - } - ] - } - } - ], - "id": "group" - } - ] - } - ] -} diff --git a/schemas/test/en/test_new_routing_date_less_than.json b/schemas/test/en/test_new_routing_date_less_than.json deleted file mode 100644 index 30925081ff..0000000000 --- a/schemas/test/en/test_new_routing_date_less_than.json +++ /dev/null @@ -1,108 +0,0 @@ -{ - "mime_type": "application/json/ons/eq", - "language": "en", - "schema_version": "0.0.1", - "data_version": "0.0.3", - "survey_id": "001", - "title": "Test Routing Date Less Than", - "theme": "default", - "description": "A test survey for routing based on a Date less than", - "metadata": [ - { - "name": "user_id", - "type": "string" - }, - { - "name": "period_id", - "type": "string" - }, - { - "name": "ru_name", - "type": "string" - } - ], - "questionnaire_flow": { - "type": "Linear", - "options": { - "summary": { - "collapsible": false - } - } - }, - "sections": [ - { - "id": "default-section", - "groups": [ - { - "blocks": [ - { - "type": "Question", - "id": "date-question", - "question": { - "answers": [ - { - "id": "single-date-answer", - "label": "Today", - "mandatory": true, - "type": "Date" - } - ], - "id": "date-questions", - "title": "Enter a date less than Today", - "type": "General" - }, - "routing_rules": [ - { - "block": "correct-answer", - "when": { - "<": [ - { - "date": [{ "source": "answers", "identifier": "single-date-answer" }] - }, - { - "date": ["now"] - } - ] - } - }, - { - "block": "incorrect-answer" - } - ] - }, - { - "type": "Interstitial", - "id": "incorrect-answer", - "content": { - "title": "Incorrect answer", - "contents": [ - { - "description": "You entered a date later than yesterday." - } - ] - }, - "routing_rules": [ - { - "section": "End" - } - ] - }, - { - "type": "Interstitial", - "id": "correct-answer", - "content": { - "title": "Correct answer", - "contents": [ - { - "description": "You entered a date older than Today." - } - ] - } - } - ], - "id": "group" - } - ] - } - ] -} diff --git a/schemas/test/en/test_new_routing_date_not_equals.json b/schemas/test/en/test_new_routing_date_not_equals.json deleted file mode 100644 index 0cbb7d5ee8..0000000000 --- a/schemas/test/en/test_new_routing_date_not_equals.json +++ /dev/null @@ -1,108 +0,0 @@ -{ - "mime_type": "application/json/ons/eq", - "language": "en", - "schema_version": "0.0.1", - "data_version": "0.0.3", - "survey_id": "001", - "title": "Test Routing Date Not Equals", - "theme": "default", - "description": "A test survey for routing based on a date not equals", - "metadata": [ - { - "name": "user_id", - "type": "string" - }, - { - "name": "period_id", - "type": "string" - }, - { - "name": "ru_name", - "type": "string" - } - ], - "questionnaire_flow": { - "type": "Linear", - "options": { - "summary": { - "collapsible": false - } - } - }, - "sections": [ - { - "id": "default-section", - "groups": [ - { - "blocks": [ - { - "type": "Question", - "id": "date-question", - "question": { - "answers": [ - { - "id": "single-date-answer", - "label": "Today", - "mandatory": true, - "type": "MonthYearDate" - } - ], - "id": "date-questions", - "title": "Enter a date other than February 2018", - "type": "General" - }, - "routing_rules": [ - { - "block": "correct-answer", - "when": { - "!=": [ - { - "date": [{ "source": "answers", "identifier": "single-date-answer" }] - }, - { - "date": ["2018-02"] - } - ] - } - }, - { - "block": "incorrect-answer" - } - ] - }, - { - "type": "Interstitial", - "id": "incorrect-answer", - "content": { - "title": "Incorrect Date", - "contents": [ - { - "description": "You entered 28 February 2018." - } - ] - }, - "routing_rules": [ - { - "section": "End" - } - ] - }, - { - "type": "Interstitial", - "id": "correct-answer", - "content": { - "title": "Correct Date", - "contents": [ - { - "description": "You entered a date other than 28 February 2018." - } - ] - } - } - ], - "id": "group" - } - ] - } - ] -} diff --git a/schemas/test/en/test_new_routing_number_equals.json b/schemas/test/en/test_new_routing_number_equals.json deleted file mode 100644 index 6338404ceb..0000000000 --- a/schemas/test/en/test_new_routing_number_equals.json +++ /dev/null @@ -1,129 +0,0 @@ -{ - "mime_type": "application/json/ons/eq", - "language": "en", - "schema_version": "0.0.1", - "data_version": "0.0.3", - "survey_id": "001", - "title": "Test Routing Number Equals", - "theme": "default", - "description": "A test survey for routing based on a number equals", - "metadata": [ - { - "name": "user_id", - "type": "string" - }, - { - "name": "period_id", - "type": "string" - }, - { - "name": "ru_name", - "type": "string" - } - ], - "questionnaire_flow": { - "type": "Linear", - "options": { - "summary": { - "collapsible": false - } - } - }, - "sections": [ - { - "id": "default-section", - "groups": [ - { - "blocks": [ - { - "type": "Question", - "id": "number-question", - "question": { - "answers": [ - { - "id": "answer", - "mandatory": true, - "type": "Number", - "label": "Enter 123" - } - ], - "id": "question", - "title": "Enter the number 123", - "type": "General" - }, - "routing_rules": [ - { - "block": "correct-answer", - "when": { - "==": [ - { - "source": "answers", - "identifier": "answer" - }, - 123 - ] - } - }, - { - "block": "incorrect-answer" - } - ] - }, - { - "type": "Interstitial", - "id": "incorrect-answer", - "content": { - "title": "You did not enter 123", - "contents": [ - { - "description": { - "text": "You were asked to enter 123 but you actually entered {answer}.", - "placeholders": [ - { - "placeholder": "answer", - "value": { - "source": "answers", - "identifier": "answer" - } - } - ] - } - } - ] - }, - "routing_rules": [ - { - "section": "End" - } - ] - }, - { - "type": "Interstitial", - "id": "correct-answer", - "content": { - "title": "Correct", - "contents": [ - { - "description": { - "text": "You were asked to enter 123 and you entered {answer}.", - "placeholders": [ - { - "placeholder": "answer", - "value": { - "source": "answers", - "identifier": "answer" - } - } - ] - } - } - ] - } - } - ], - "id": "group" - } - ] - } - ] -} diff --git a/schemas/test/en/test_new_routing_number_greater_than.json b/schemas/test/en/test_new_routing_number_greater_than.json deleted file mode 100644 index da375ab8f8..0000000000 --- a/schemas/test/en/test_new_routing_number_greater_than.json +++ /dev/null @@ -1,129 +0,0 @@ -{ - "mime_type": "application/json/ons/eq", - "language": "en", - "schema_version": "0.0.1", - "data_version": "0.0.3", - "survey_id": "001", - "title": "Test Routing Number Greater Than", - "theme": "default", - "description": "A test survey for routing based on a number greater than", - "metadata": [ - { - "name": "user_id", - "type": "string" - }, - { - "name": "period_id", - "type": "string" - }, - { - "name": "ru_name", - "type": "string" - } - ], - "questionnaire_flow": { - "type": "Linear", - "options": { - "summary": { - "collapsible": false - } - } - }, - "sections": [ - { - "id": "default-section", - "groups": [ - { - "blocks": [ - { - "type": "Question", - "id": "number-question", - "question": { - "answers": [ - { - "id": "answer", - "mandatory": true, - "type": "Number", - "label": "Enter a number greater than 123" - } - ], - "id": "question", - "title": "Enter a number greater than 123", - "type": "General" - }, - "routing_rules": [ - { - "block": "correct-answer", - "when": { - ">": [ - { - "source": "answers", - "identifier": "answer" - }, - 123 - ] - } - }, - { - "block": "incorrect-answer" - } - ] - }, - { - "type": "Interstitial", - "id": "incorrect-answer", - "content": { - "title": "You did not enter a number greater than 123", - "contents": [ - { - "description": { - "text": "You were asked to enter a number greater than 123 but you actually entered {answer}.", - "placeholders": [ - { - "placeholder": "answer", - "value": { - "source": "answers", - "identifier": "answer" - } - } - ] - } - } - ] - }, - "routing_rules": [ - { - "section": "End" - } - ] - }, - { - "type": "Interstitial", - "id": "correct-answer", - "content": { - "title": "Correct", - "contents": [ - { - "description": { - "text": "You were asked to enter a number greater than 123 and you entered {answer}.", - "placeholders": [ - { - "placeholder": "answer", - "value": { - "source": "answers", - "identifier": "answer" - } - } - ] - } - } - ] - } - } - ], - "id": "group" - } - ] - } - ] -} diff --git a/schemas/test/en/test_new_routing_number_greater_than_or_equal.json b/schemas/test/en/test_new_routing_number_greater_than_or_equal.json deleted file mode 100644 index 1380ec8b39..0000000000 --- a/schemas/test/en/test_new_routing_number_greater_than_or_equal.json +++ /dev/null @@ -1,131 +0,0 @@ -{ - "mime_type": "application/json/ons/eq", - "language": "en", - "schema_version": "0.0.1", - "data_version": "0.0.3", - "survey_id": "001", - "title": "Test Routing Number Greater Than Or Equal To", - "theme": "default", - "description": "A test survey for routing based on a number greater than or equal to", - "metadata": [ - { - "name": "user_id", - "type": "string" - }, - { - "name": "period_id", - "type": "string" - }, - { - "name": "ru_name", - "type": "string" - } - ], - "questionnaire_flow": { - "type": "Linear", - "options": { - "summary": { - "collapsible": false - } - } - }, - "sections": [ - { - "id": "default-section", - "groups": [ - { - "blocks": [ - { - "type": "Question", - "id": "number-question", - "question": { - "answers": [ - { - "id": "answer", - "mandatory": true, - "type": "Number", - "label": "123 or greater" - } - ], - "id": "question", - "title": "Enter the number greater than or equal to 123", - "type": "General" - }, - "routing_rules": [ - { - "block": "correct-answer", - "when": { - ">=": [ - { - "source": "answers", - "identifier": "answer" - }, - 123 - ] - } - }, - { - "block": "incorrect-answer" - } - ] - }, - { - "type": "Interstitial", - "id": "incorrect-answer", - "content": { - "title": "Incorrect answer", - "contents": [ - { - "description": { - "text": "You were asked to enter a number greater than or equal to 123 but you entered {answer}.", - "placeholders": [ - { - "placeholder": "answer", - "value": { - "source": "answers", - "identifier": "answer" - } - } - ] - } - } - ] - }, - "routing_rules": [ - { - "goto": { - "section": "End" - } - } - ] - }, - { - "type": "Interstitial", - "id": "correct-answer", - "content": { - "title": "Correct answer", - "contents": [ - { - "description": { - "text": "You were asked to enter a number greater than or equal to 123 and you entered {answer}.", - "placeholders": [ - { - "placeholder": "answer", - "value": { - "source": "answers", - "identifier": "answer" - } - } - ] - } - } - ] - } - } - ], - "id": "group" - } - ] - } - ] -} diff --git a/schemas/test/en/test_new_routing_number_less_than.json b/schemas/test/en/test_new_routing_number_less_than.json deleted file mode 100644 index 5036a036b9..0000000000 --- a/schemas/test/en/test_new_routing_number_less_than.json +++ /dev/null @@ -1,129 +0,0 @@ -{ - "mime_type": "application/json/ons/eq", - "language": "en", - "schema_version": "0.0.1", - "data_version": "0.0.3", - "survey_id": "001", - "title": "Test Routing Number Less Than", - "theme": "default", - "description": "A test survey for routing based on a number less than", - "metadata": [ - { - "name": "user_id", - "type": "string" - }, - { - "name": "period_id", - "type": "string" - }, - { - "name": "ru_name", - "type": "string" - } - ], - "questionnaire_flow": { - "type": "Linear", - "options": { - "summary": { - "collapsible": false - } - } - }, - "sections": [ - { - "id": "default-section", - "groups": [ - { - "blocks": [ - { - "type": "Question", - "id": "number-question", - "question": { - "answers": [ - { - "id": "answer", - "mandatory": true, - "type": "Number", - "label": "Enter a number less than 123" - } - ], - "id": "question", - "title": "Enter a number less than 123", - "type": "General" - }, - "routing_rules": [ - { - "block": "correct-answer", - "when": { - "<": [ - { - "source": "answers", - "identifier": "answer" - }, - 123 - ] - } - }, - { - "block": "incorrect-answer" - } - ] - }, - { - "type": "Interstitial", - "id": "incorrect-answer", - "content": { - "title": "You did not enter a number less than 123", - "contents": [ - { - "description": { - "text": "You were asked to enter a number less than 123 but you actually entered {answer}.", - "placeholders": [ - { - "placeholder": "answer", - "value": { - "source": "answers", - "identifier": "answer" - } - } - ] - } - } - ] - }, - "routing_rules": [ - { - "section": "End" - } - ] - }, - { - "type": "Interstitial", - "id": "correct-answer", - "content": { - "title": "Correct", - "contents": [ - { - "description": { - "text": "You were asked to enter a number less than 123 and you entered {answer}.", - "placeholders": [ - { - "placeholder": "answer", - "value": { - "source": "answers", - "identifier": "answer" - } - } - ] - } - } - ] - } - } - ], - "id": "group" - } - ] - } - ] -} diff --git a/schemas/test/en/test_new_routing_number_less_than_or_equal.json b/schemas/test/en/test_new_routing_number_less_than_or_equal.json deleted file mode 100644 index ccdfca3517..0000000000 --- a/schemas/test/en/test_new_routing_number_less_than_or_equal.json +++ /dev/null @@ -1,131 +0,0 @@ -{ - "mime_type": "application/json/ons/eq", - "language": "en", - "schema_version": "0.0.1", - "data_version": "0.0.3", - "survey_id": "001", - "title": "Test Routing Number Less Than Or Equal To", - "theme": "default", - "description": "A test survey for routing based on a number less than or equal to", - "metadata": [ - { - "name": "user_id", - "type": "string" - }, - { - "name": "period_id", - "type": "string" - }, - { - "name": "ru_name", - "type": "string" - } - ], - "questionnaire_flow": { - "type": "Linear", - "options": { - "summary": { - "collapsible": false - } - } - }, - "sections": [ - { - "id": "default-section", - "groups": [ - { - "blocks": [ - { - "type": "Question", - "id": "number-question", - "question": { - "answers": [ - { - "id": "answer", - "mandatory": true, - "type": "Number", - "label": "Number" - } - ], - "id": "question", - "title": "Enter the number less than or equal to 123", - "type": "General" - }, - "routing_rules": [ - { - "block": "correct-answer", - "when": { - "<=": [ - { - "source": "answers", - "identifier": "answer" - }, - 123 - ] - } - }, - { - "block": "incorrect-answer" - } - ] - }, - { - "type": "Interstitial", - "id": "incorrect-answer", - "content": { - "title": "Incorrect answer", - "contents": [ - { - "description": { - "text": "You were asked to enter a number less than or equal to 123 but you entered {answer}.", - "placeholders": [ - { - "placeholder": "answer", - "value": { - "source": "answers", - "identifier": "answer" - } - } - ] - } - } - ] - }, - "routing_rules": [ - { - "goto": { - "section": "End" - } - } - ] - }, - { - "type": "Interstitial", - "id": "correct-answer", - "content": { - "title": "Correct answer", - "contents": [ - { - "description": { - "text": "You were asked to enter a number less than or equal to 123 and you entered {answer}.", - "placeholders": [ - { - "placeholder": "answer", - "value": { - "source": "answers", - "identifier": "answer" - } - } - ] - } - } - ] - } - } - ], - "id": "group" - } - ] - } - ] -} diff --git a/schemas/test/en/test_new_routing_number_not_equals.json b/schemas/test/en/test_new_routing_number_not_equals.json deleted file mode 100644 index c400bcf04a..0000000000 --- a/schemas/test/en/test_new_routing_number_not_equals.json +++ /dev/null @@ -1,129 +0,0 @@ -{ - "mime_type": "application/json/ons/eq", - "language": "en", - "schema_version": "0.0.1", - "data_version": "0.0.3", - "survey_id": "001", - "title": "Test Routing Number Not Equals", - "theme": "default", - "description": "A test survey for routing based on a number not equals", - "metadata": [ - { - "name": "user_id", - "type": "string" - }, - { - "name": "period_id", - "type": "string" - }, - { - "name": "ru_name", - "type": "string" - } - ], - "questionnaire_flow": { - "type": "Linear", - "options": { - "summary": { - "collapsible": false - } - } - }, - "sections": [ - { - "id": "default-section", - "groups": [ - { - "blocks": [ - { - "type": "Question", - "id": "number-question", - "question": { - "answers": [ - { - "id": "answer", - "mandatory": true, - "type": "Number", - "label": "Not 123" - } - ], - "id": "question", - "title": "Enter the number that isn’t 123", - "type": "General" - }, - "routing_rules": [ - { - "block": "correct-answer", - "when": { - "!=": [ - { - "source": "answers", - "identifier": "answer" - }, - 123 - ] - } - }, - { - "block": "incorrect-answer" - } - ] - }, - { - "type": "Interstitial", - "id": "incorrect-answer", - "content": { - "title": "Incorrect answer", - "contents": [ - { - "description": { - "text": "You were asked not to enter 123 but you entered {answer}.", - "placeholders": [ - { - "placeholder": "answer", - "value": { - "source": "answers", - "identifier": "answer" - } - } - ] - } - } - ] - }, - "routing_rules": [ - { - "section": "End" - } - ] - }, - { - "type": "Interstitial", - "id": "correct-answer", - "content": { - "title": "Correct answer", - "contents": [ - { - "description": { - "text": "You were asked not to enter 123 and you entered {answer}.", - "placeholders": [ - { - "placeholder": "answer", - "value": { - "source": "answers", - "identifier": "answer" - } - } - ] - } - } - ] - } - } - ], - "id": "group" - } - ] - } - ] -} diff --git a/schemas/test/en/test_new_section_enabled_checkbox.json b/schemas/test/en/test_new_section_enabled_checkbox.json deleted file mode 100644 index 7c60b826c5..0000000000 --- a/schemas/test/en/test_new_section_enabled_checkbox.json +++ /dev/null @@ -1,168 +0,0 @@ -{ - "mime_type": "application/json/ons/eq", - "language": "en", - "schema_version": "0.0.1", - "data_version": "0.0.3", - "survey_id": "0", - "title": "Test Section Enabled", - "theme": "default", - "description": "A questionnaire to demo section enabled key usage with checkbox options", - "metadata": [ - { - "name": "user_id", - "type": "string" - }, - { - "name": "period_id", - "type": "string" - }, - { - "name": "ru_name", - "type": "string" - } - ], - "questionnaire_flow": { - "type": "Linear", - "options": { - "summary": { - "collapsible": false - } - } - }, - "sections": [ - { - "id": "section-1", - "title": "Section 1", - "groups": [ - { - "blocks": [ - { - "id": "section-1-block", - "type": "Question", - "question": { - "answers": [ - { - "id": "section-1-answer", - "label": "label 1", - "mandatory": false, - "options": [ - { - "label": "Section 2", - "value": "Section 2" - }, - { - "label": "Section 3", - "value": "Section 3" - } - ], - "type": "Checkbox" - }, - { - "id": "section-1-answer-exclusive", - "mandatory": false, - "options": [ - { - "label": "Neither", - "value": "Neither" - } - ], - "type": "Checkbox" - } - ], - "mandatory": false, - "description": ["This is section 1."], - "id": "section-1-question", - "title": "Which sections do you want to enable?", - "type": "MutuallyExclusive" - } - } - ], - "id": "section-1-group", - "title": "Section 1" - } - ] - }, - { - "id": "section-2", - "title": "Section 2", - "enabled": { - "when": { - "in": [ - "Section 2", - { - "source": "answers", - "identifier": "section-1-answer" - } - ] - } - }, - "groups": [ - { - "blocks": [ - { - "id": "section-2-block", - "type": "Question", - "question": { - "answers": [ - { - "id": "section-2-answer", - "label": "label 2", - "mandatory": false, - "type": "Number" - } - ], - "description": ["This is section 2."], - "id": "section-2-question", - "title": "Which section is this?", - "type": "General" - } - } - ], - "id": "section-2-group", - "title": "Section 2" - } - ] - }, - { - "id": "section-3", - "title": "Section 3", - "enabled": { - "when": { - "in": [ - "Section 3", - { - "source": "answers", - "identifier": "section-1-answer" - } - ] - } - }, - "groups": [ - { - "blocks": [ - { - "id": "section-3-block", - "type": "Question", - "question": { - "answers": [ - { - "id": "section-3-answer", - "label": "label 3", - "mandatory": false, - "type": "Number" - } - ], - "description": ["This is section 3."], - "id": "section-3-question", - "title": "Which section is this?", - "type": "General" - } - } - ], - "id": "section-3-group", - "title": "Section 3" - } - ] - } - ] -} diff --git a/schemas/test/en/test_new_section_enabled_hub.json b/schemas/test/en/test_new_section_enabled_hub.json deleted file mode 100644 index 5f6e82f73b..0000000000 --- a/schemas/test/en/test_new_section_enabled_hub.json +++ /dev/null @@ -1,163 +0,0 @@ -{ - "mime_type": "application/json/ons/eq", - "language": "en", - "schema_version": "0.0.1", - "data_version": "0.0.3", - "survey_id": "0", - "title": "Test Section Enabled", - "theme": "default", - "description": "A questionnaire to demo section enabled key usage with hub enabled", - "metadata": [ - { - "name": "user_id", - "type": "string" - }, - { - "name": "period_id", - "type": "string" - }, - { - "name": "ru_name", - "type": "string" - } - ], - "questionnaire_flow": { - "type": "Hub", - "options": { "required_completed_sections": ["section-1"] } - }, - "sections": [ - { - "id": "section-1", - "title": "Section 1", - "groups": [ - { - "blocks": [ - { - "id": "section-1-block", - "type": "Question", - "question": { - "answers": [ - { - "id": "section-1-answer", - "mandatory": false, - "options": [ - { - "label": "Section 2", - "value": "Section 2" - }, - { - "label": "Section 3", - "value": "Section 3" - } - ], - "type": "Checkbox" - }, - { - "id": "section-1-answer-exclusive", - "mandatory": false, - "options": [ - { - "label": "Neither", - "value": "Neither" - } - ], - "type": "Checkbox" - } - ], - "mandatory": false, - "description": ["This is section 1."], - "id": "section-1-question", - "title": "Which sections do you want to enable?", - "type": "MutuallyExclusive" - } - } - ], - "id": "section-1-group", - "title": "Section 1" - } - ] - }, - { - "id": "section-2", - "title": "Section 2", - "enabled": { - "when": { - "in": [ - "Section 2", - { - "source": "answers", - "identifier": "section-1-answer" - } - ] - } - }, - "groups": [ - { - "blocks": [ - { - "id": "section-2-block", - "type": "Question", - "question": { - "answers": [ - { - "id": "section-2-answer", - "label": "Section 2", - "mandatory": false, - "type": "Number" - } - ], - "description": ["This is section 2."], - "id": "section-2-question", - "title": "Which section is this?", - "type": "General" - } - } - ], - "id": "section-2-group", - "title": "Section 2" - } - ] - }, - { - "id": "section-3", - "title": "Section 3", - "enabled": { - "when": { - "in": [ - "Section 3", - { - "source": "answers", - "identifier": "section-1-answer" - } - ] - } - }, - "groups": [ - { - "blocks": [ - { - "id": "section-3-block", - "type": "Question", - "question": { - "answers": [ - { - "id": "section-3-answer", - "label": "Section 3", - "mandatory": false, - "type": "Number" - } - ], - "description": ["This is section 3."], - "id": "section-3-question", - "title": "Which section is this?", - "type": "General" - } - } - ], - "id": "section-3-group", - "title": "Section 3" - } - ] - } - ] -} diff --git a/schemas/test/en/test_new_section_enabled_radio.json b/schemas/test/en/test_new_section_enabled_radio.json deleted file mode 100644 index 3141ae569f..0000000000 --- a/schemas/test/en/test_new_section_enabled_radio.json +++ /dev/null @@ -1,115 +0,0 @@ -{ - "mime_type": "application/json/ons/eq", - "language": "en", - "schema_version": "0.0.1", - "data_version": "0.0.3", - "survey_id": "0", - "title": "Test Section Enabled", - "theme": "default", - "description": "A questionnaire to demo section enabled key usage with radio options", - "metadata": [ - { - "name": "user_id", - "type": "string" - }, - { - "name": "period_id", - "type": "string" - }, - { - "name": "ru_name", - "type": "string" - } - ], - "questionnaire_flow": { - "type": "Linear", - "options": { - "summary": { - "collapsible": false - } - } - }, - "sections": [ - { - "id": "section-1", - "title": "Section 1", - "groups": [ - { - "blocks": [ - { - "id": "section-1-block", - "type": "Question", - "question": { - "answers": [ - { - "id": "section-1-answer", - "label": "Section 1", - "mandatory": false, - "options": [ - { - "label": "Yes, enable section 2", - "value": "Yes, enable section 2" - }, - { - "label": "No, disable section 2", - "value": "No, disable section 2" - } - ], - "type": "Radio" - } - ], - "description": ["This is section 1."], - "id": "section-1-question", - "title": "Do you want to enable section 2?", - "type": "General" - } - } - ], - "id": "section-1-group", - "title": "Section 1" - } - ] - }, - { - "id": "section-2", - "title": "Section 2", - "enabled": { - "when": { - "==": [ - "Yes, enable section 2", - { - "source": "answers", - "identifier": "section-1-answer" - } - ] - } - }, - "groups": [ - { - "blocks": [ - { - "id": "section-2-block", - "type": "Question", - "question": { - "answers": [ - { - "id": "section-2-answer", - "label": "Section 2", - "mandatory": false, - "type": "Number" - } - ], - "description": ["This is section 2."], - "id": "section-2-question", - "title": "Which section is this?", - "type": "General" - } - } - ], - "id": "section-2-group", - "title": "Section 2" - } - ] - } - ] -} diff --git a/schemas/test/en/test_new_skip_condition_block.json b/schemas/test/en/test_new_skip_condition_block.json deleted file mode 100644 index 389aa2095e..0000000000 --- a/schemas/test/en/test_new_skip_condition_block.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "mime_type": "application/json/ons/eq", - "language": "en", - "schema_version": "0.0.1", - "data_version": "0.0.3", - "survey_id": "0", - "title": "Skip block", - "theme": "default", - "metadata": [ - { - "name": "user_id", - "type": "string" - }, - { - "name": "period_id", - "type": "string" - }, - { - "name": "ru_name", - "type": "string" - } - ], - "questionnaire_flow": { - "type": "Linear", - "options": { - "summary": { - "collapsible": false - } - } - }, - "sections": [ - { - "id": "default-section", - "groups": [ - { - "id": "default-group", - "title": "Group 1", - "blocks": [ - { - "type": "Question", - "id": "do-you-want-to-skip", - "question": { - "id": "do-you-want-to-skip-question", - "title": "Do you want to skip the next question?", - "type": "General", - "description": ["Select “Yes” to skip the next question and go straight to the summary"], - "answers": [ - { - "id": "do-you-want-to-skip-answer", - "label": "Select an answer", - "mandatory": true, - "options": [ - { - "label": "Yes", - "value": "Yes" - }, - { - "label": "No", - "value": "No" - } - ], - "type": "Radio" - } - ] - } - }, - { - "type": "Question", - "id": "should-skip", - "question": { - "id": "should-skip-question", - "title": "Why didn’t you skip the block?", - "type": "General", - "answers": [ - { - "id": "should-skip-answer", - "label": "Enter your answer", - "mandatory": true, - "type": "TextArea" - } - ] - }, - "skip_conditions": { - "when": { - "==": [ - { - "source": "answers", - "identifier": "do-you-want-to-skip-answer" - }, - "Yes" - ] - } - } - } - ] - } - ] - } - ] -} diff --git a/schemas/test/en/test_new_skip_condition_group.json b/schemas/test/en/test_new_skip_condition_group.json deleted file mode 100644 index 1640b7ee88..0000000000 --- a/schemas/test/en/test_new_skip_condition_group.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "mime_type": "application/json/ons/eq", - "language": "en", - "schema_version": "0.0.1", - "data_version": "0.0.3", - "survey_id": "0", - "title": "Skip group", - "theme": "default", - "metadata": [ - { - "name": "user_id", - "type": "string" - }, - { - "name": "period_id", - "type": "string" - }, - { - "name": "ru_name", - "type": "string" - } - ], - "questionnaire_flow": { - "type": "Linear", - "options": { - "summary": { - "collapsible": false - } - } - }, - "sections": [ - { - "id": "default-section", - "groups": [ - { - "id": "default-group", - "title": "Group 1", - "blocks": [ - { - "type": "Question", - "id": "do-you-want-to-skip", - "question": { - "id": "do-you-want-to-skip-question", - "title": "Do you want to skip the next question?", - "type": "General", - "description": ["Select “Yes” to skip the next question and go straight to the summary"], - "answers": [ - { - "id": "do-you-want-to-skip-answer", - "label": "Select an answer", - "mandatory": true, - "options": [ - { - "label": "Yes", - "value": "Yes" - }, - { - "label": "No", - "value": "No" - } - ], - "type": "Radio" - } - ] - } - } - ] - }, - { - "id": "should-skip-group", - "title": "Group 2 (Skippable)", - "skip_conditions": { - "when": { - "==": [ - { - "source": "answers", - "identifier": "do-you-want-to-skip-answer" - }, - "Yes" - ] - } - }, - "blocks": [ - { - "type": "Question", - "id": "should-skip", - "question": { - "id": "should-skip-question", - "title": "Why didn’t you skip the group?", - "type": "General", - "answers": [ - { - "id": "should-skip-answer", - "label": "Enter your answer", - "mandatory": true, - "type": "TextArea" - } - ] - } - } - ] - } - ] - } - ] -} diff --git a/schemas/test/en/test_new_variants_content.json b/schemas/test/en/test_new_variants_content.json deleted file mode 100644 index ccec714151..0000000000 --- a/schemas/test/en/test_new_variants_content.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "mime_type": "application/json/ons/eq", - "language": "en", - "schema_version": "0.0.1", - "data_version": "0.0.3", - "survey_id": "0", - "title": "Test New Content Variants", - "theme": "default", - "description": "A questionnaire to test new content variants and variant choices", - "metadata": [ - { - "name": "user_id", - "type": "string" - }, - { - "name": "period_id", - "type": "string" - }, - { - "name": "ru_name", - "type": "string" - } - ], - "questionnaire_flow": { - "type": "Linear", - "options": { - "summary": { - "collapsible": false - } - } - }, - "sections": [ - { - "id": "section", - "groups": [ - { - "id": "group", - "title": "Variants", - "blocks": [ - { - "type": "Question", - "id": "age-question-block", - "question": { - "id": "age-question", - "type": "General", - "title": "What is your age?", - "answers": [ - { - "id": "age-answer", - "label": "Your age?", - "mandatory": true, - "type": "Number" - } - ] - } - }, - { - "type": "Interstitial", - "id": "age-display-block", - "content_variants": [ - { - "content": { - "title": "You are 16 or older", - "contents": [ - { - "description": "According to your answer" - } - ] - }, - "when": { - ">": [ - { - "source": "answers", - "identifier": "age-answer" - }, - 16 - ] - } - }, - { - "content": { - "title": "You are 16 or younger", - "contents": [ - { - "description": "According to your answer" - } - ] - }, - "when": { - "<=": [ - { - "source": "answers", - "identifier": "age-answer" - }, - 16 - ] - } - } - ] - } - ] - } - ] - } - ] -} diff --git a/schemas/test/en/test_new_variants_question.json b/schemas/test/en/test_new_variants_question.json deleted file mode 100644 index bb3cb64a11..0000000000 --- a/schemas/test/en/test_new_variants_question.json +++ /dev/null @@ -1,622 +0,0 @@ -{ - "mime_type": "application/json/ons/eq", - "language": "en", - "schema_version": "0.0.1", - "data_version": "0.0.3", - "survey_id": "0", - "title": "Test New Question Variants", - "theme": "default", - "description": "A questionnaire to test new question variants", - "metadata": [ - { - "name": "user_id", - "type": "string" - }, - { - "name": "period_id", - "type": "string" - }, - { - "name": "ru_name", - "type": "string" - } - ], - "questionnaire_flow": { - "type": "Linear", - "options": { - "summary": { - "collapsible": false - } - } - }, - "sections": [ - { - "id": "variant-proxy-section", - "groups": [ - { - "id": "variant-proxy-group", - "title": "Variants for proxy", - "blocks": [ - { - "type": "Question", - "id": "name-block", - "question": { - "id": "name-question", - "title": "Who is this questionnaire about?", - "type": "General", - "answers": [ - { - "id": "first-name-answer", - "label": "First Name", - "mandatory": true, - "type": "TextField" - }, - { - "id": "last-name-answer", - "label": "Last Name", - "mandatory": false, - "type": "TextField" - } - ] - } - }, - { - "type": "Question", - "id": "proxy-block", - "question": { - "id": "proxy-question", - "title": { - "text": "Are you {person_name}?", - "placeholders": [ - { - "placeholder": "person_name", - "transforms": [ - { - "transform": "concatenate_list", - "arguments": { - "list_to_concatenate": [ - { - "source": "answers", - "identifier": "first-name-answer" - }, - { - "source": "answers", - "identifier": "last-name-answer" - } - ], - "delimiter": " " - } - } - ] - } - ] - }, - "type": "General", - "answers": [ - { - "id": "proxy-answer", - "mandatory": true, - "options": [ - { - "label": "Yes, I am", - "value": "Yes, I am" - }, - { - "label": "No, I am answering on their behalf", - "value": "No, I am answering on their behalf" - } - ], - "type": "Radio" - } - ] - } - } - ] - } - ] - }, - { - "id": "basic-question-variant-section", - "summary": { "show_on_completion": true }, - "title": "Question variant section", - "groups": [ - { - "id": "basic-question-variant-group", - "title": "Variants", - "blocks": [ - { - "type": "Question", - "id": "age-block", - "question_variants": [ - { - "question": { - "id": "age-question", - "type": "General", - "title": "What is your age?", - "answers": [ - { - "id": "age-answer", - "mandatory": false, - "type": "Number", - "label": "Age" - } - ] - }, - "when": { - "==": [ - { - "source": "answers", - "identifier": "proxy-answer" - }, - "Yes, I am" - ] - } - }, - { - "question": { - "id": "age-question", - "type": "General", - "title": { - "text": "What age is {person_name}?", - "placeholders": [ - { - "placeholder": "person_name", - "transforms": [ - { - "transform": "concatenate_list", - "arguments": { - "list_to_concatenate": [ - { - "source": "answers", - "identifier": "first-name-answer" - }, - { - "source": "answers", - "identifier": "last-name-answer" - } - ], - "delimiter": " " - } - } - ] - } - ] - }, - "answers": [ - { - "id": "age-answer", - "mandatory": true, - "type": "Number", - "label": "Age" - } - ] - }, - "when": { - "==": [ - { - "source": "answers", - "identifier": "proxy-answer" - }, - "No, I am answering on their behalf" - ] - } - } - ] - }, - { - "type": "ConfirmationQuestion", - "id": "age-confirmation-block", - "question_variants": [ - { - "question": { - "id": "age-confirmation-question", - "type": "General", - "title": "You are over 16?", - "answers": [ - { - "id": "age-confirm-answer", - "type": "Radio", - "mandatory": true, - "options": [ - { - "label": "Yes", - "value": "Yes" - }, - { - "label": "No", - "value": "No" - } - ] - } - ] - }, - "when": { - "and": [ - { - ">=": [ - { - "source": "answers", - "identifier": "age-answer" - }, - 16 - ] - }, - { - "!=": [ - { - "source": "answers", - "identifier": "proxy-answer" - }, - "No, I am answering on their behalf" - ] - } - ] - } - }, - { - "question": { - "id": "age-confirmation-question", - "type": "General", - "title": "You are under 16?", - "answers": [ - { - "id": "age-confirm-answer", - "type": "Radio", - "mandatory": true, - "options": [ - { - "label": "Yes", - "value": "Yes" - }, - { - "label": "No", - "value": "No" - } - ] - } - ] - }, - "when": { - "and": [ - { - "<=": [ - { - "source": "answers", - "identifier": "age-answer" - }, - 16 - ] - }, - { - "!=": [ - { - "source": "answers", - "identifier": "proxy-answer" - }, - "No, I am answering on their behalf" - ] - } - ] - } - }, - { - "question": { - "id": "age-confirmation-question", - "type": "General", - "title": { - "text": "{person_name} is over 16?", - "placeholders": [ - { - "placeholder": "person_name", - "transforms": [ - { - "transform": "concatenate_list", - "arguments": { - "list_to_concatenate": [ - { - "source": "answers", - "identifier": "first-name-answer" - }, - { - "source": "answers", - "identifier": "last-name-answer" - } - ], - "delimiter": " " - } - } - ] - } - ] - }, - "answers": [ - { - "id": "age-confirm-answer", - "type": "Radio", - "mandatory": true, - "options": [ - { - "label": "Yes", - "value": "Yes" - }, - { - "label": "No", - "value": "No" - } - ] - } - ] - }, - "when": { - "and": [ - { - ">=": [ - { - "source": "answers", - "identifier": "age-answer" - }, - 16 - ] - }, - { - "==": [ - { - "source": "answers", - "identifier": "proxy-answer" - }, - "No, I am answering on their behalf" - ] - } - ] - } - }, - { - "question": { - "id": "age-confirmation-question", - "type": "General", - "title": { - "text": "{person_name} is under 16?", - "placeholders": [ - { - "placeholder": "person_name", - "transforms": [ - { - "transform": "concatenate_list", - "arguments": { - "list_to_concatenate": [ - { - "source": "answers", - "identifier": "first-name-answer" - }, - { - "source": "answers", - "identifier": "last-name-answer" - } - ], - "delimiter": " " - } - } - ] - } - ] - }, - "answers": [ - { - "id": "age-confirm-answer", - "type": "Radio", - "mandatory": true, - "options": [ - { - "label": "Yes", - "value": "Yes" - }, - { - "label": "No", - "value": "No" - } - ] - } - ] - }, - "when": { - "and": [ - { - "<=": [ - { - "source": "answers", - "identifier": "age-answer" - }, - 16 - ] - }, - { - "==": [ - { - "source": "answers", - "identifier": "proxy-answer" - }, - "No, I am answering on their behalf" - ] - } - ] - } - } - ], - "routing_rules": [ - { - "block": "age-block", - "when": { - "==": [ - { - "source": "answers", - "identifier": "age-confirm-answer" - }, - "No" - ] - } - }, - { - "section": "End" - } - ] - } - ] - } - ] - }, - { - "id": "currency-section", - "summary": { "show_on_completion": true }, - "groups": [ - { - "id": "currency-group", - "title": "Section Summary With Variants", - "blocks": [ - { - "type": "Question", - "id": "currency-block", - "question": { - "id": "currency-question", - "type": "General", - "title": "What currency would you like", - "answers": [ - { - "id": "currency-answer", - "type": "Radio", - "mandatory": true, - "options": [ - { - "label": "US Dollars", - "value": "US Dollars" - }, - { - "label": "Sterling", - "value": "Sterling" - } - ] - } - ] - } - }, - { - "type": "Question", - "id": "first-number-block", - "question_variants": [ - { - "question": { - "id": "first-number-question", - "title": "First Number Question Title", - "type": "General", - "answers": [ - { - "id": "first-number-answer", - "label": "First answer in GBP", - "mandatory": true, - "type": "Currency", - "currency": "GBP", - "decimal_places": 2 - } - ] - }, - "when": { - "==": [ - { - "source": "answers", - "identifier": "currency-answer" - }, - "Sterling" - ] - } - }, - { - "question": { - "id": "first-number-question", - "title": "First Number Question Title", - "type": "General", - "answers": [ - { - "id": "first-number-answer", - "label": "First answer in USD", - "mandatory": true, - "type": "Currency", - "currency": "USD", - "decimal_places": 2 - } - ] - }, - "when": { - "==": [ - { - "source": "answers", - "identifier": "currency-answer" - }, - "US Dollars" - ] - } - } - ] - }, - { - "type": "Question", - "id": "second-number-block", - "question_variants": [ - { - "question": { - "id": "second-number-question", - "title": "Second Number Question Title", - "type": "General", - "answers": [ - { - "id": "second-number-answer", - "label": "Second answer in GBP", - "mandatory": true, - "type": "Currency", - "currency": "GBP", - "decimal_places": 2 - } - ] - }, - "when": { - "==": [ - { - "source": "answers", - "identifier": "currency-answer" - }, - "Sterling" - ] - } - }, - { - "question": { - "id": "second-number-question", - "title": "Second Number Question Title", - "type": "General", - "answers": [ - { - "id": "second-number-answer", - "label": "Second answer in USD", - "mandatory": true, - "type": "Currency", - "currency": "USD", - "decimal_places": 2 - } - ] - }, - "when": { - "==": [ - { - "source": "answers", - "identifier": "currency-answer" - }, - "US Dollars" - ] - } - } - ] - } - ] - } - ] - } - ] -} diff --git a/schemas/test/en/test_numbers.json b/schemas/test/en/test_numbers.json index 1746e25300..195f2ea576 100644 --- a/schemas/test/en/test_numbers.json +++ b/schemas/test/en/test_numbers.json @@ -48,7 +48,7 @@ "type": "Number", "decimal_places": 2, "minimum": { - "value": 0 + "value": -1000.98 }, "maximum": { "value": 1000 @@ -65,7 +65,7 @@ "value": 1001 }, "maximum": { - "value": 10000 + "value": 10000.98 } } ], @@ -115,7 +115,7 @@ } ] }, - "mandatory": false, + "mandatory": true, "type": "Number", "decimal_places": 2, "maximum": { @@ -187,11 +187,11 @@ }, { "id": "test-min", - "label": "Min Test (123 to 9,999,999,999)", + "label": "Min Test (-123 to 999,999,999,999,999)", "mandatory": false, "type": "Number", "minimum": { - "value": 123 + "value": -123 } }, { @@ -205,7 +205,7 @@ }, { "id": "test-min-exclusive", - "label": "Min Exclusive Test (124 to 9,999,999,999 - 123 Exclusive)", + "label": "Min Exclusive Test (124 to 999,999,999,999,999 - 123 Exclusive)", "mandatory": false, "type": "Number", "minimum": { @@ -271,7 +271,7 @@ "mandatory": false, "type": "Currency", "currency": "GBP", - "decimal_places": 2, + "decimal_places": 5, "maximum": { "value": { "source": "answers", @@ -319,7 +319,7 @@ "maximum": { "value": { "source": "answers", - "identifier": "set-maximum" + "identifier": "test-range" } } } @@ -340,7 +340,7 @@ "arguments": { "number": { "source": "answers", - "identifier": "set-maximum" + "identifier": "test-range" } } } @@ -355,6 +355,193 @@ "id": "test" } ] + }, + { + "id": "currency-section", + "summary": { "show_on_completion": true }, + "groups": [ + { + "id": "currency-group", + "title": "Section Summary With Variants", + "blocks": [ + { + "type": "Question", + "id": "currency-block", + "question": { + "id": "currency-question", + "type": "General", + "title": "What currency would you like", + "answers": [ + { + "id": "currency-answer", + "type": "Radio", + "mandatory": true, + "options": [ + { + "label": "US Dollars", + "value": "US Dollars" + }, + { + "label": "Sterling", + "value": "Sterling" + } + ] + } + ] + } + }, + { + "type": "Question", + "id": "first-number-block", + "question_variants": [ + { + "question": { + "id": "first-number-question", + "title": "First Number Question Title", + "type": "General", + "answers": [ + { + "id": "first-number-answer", + "label": "First answer in GBP", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2, + "maximum": { + "value": 1000 + }, + "minimum": { + "value": 1 + } + } + ] + }, + "when": { + "==": [ + { + "source": "answers", + "identifier": "currency-answer" + }, + "Sterling" + ] + } + }, + { + "question": { + "id": "first-number-question", + "title": "First Number Question Title", + "type": "General", + "answers": [ + { + "id": "first-number-answer", + "label": "First answer in USD", + "mandatory": true, + "type": "Currency", + "currency": "USD", + "decimal_places": 2, + "maximum": { + "value": 100 + }, + "minimum": { + "value": 10 + } + } + ] + }, + "when": { + "==": [ + { + "source": "answers", + "identifier": "currency-answer" + }, + "US Dollars" + ] + } + } + ] + }, + { + "type": "Question", + "id": "second-number-block", + "question_variants": [ + { + "question": { + "id": "second-number-question", + "title": "Second Number Question Title", + "type": "General", + "answers": [ + { + "id": "second-number-answer", + "label": "Second answer in GBP", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2, + "maximum": { + "value": { + "source": "answers", + "identifier": "first-number-answer" + } + }, + "minimum": { + "value": { + "source": "answers", + "identifier": "first-number-answer" + } + } + } + ] + }, + "when": { + "==": [ + { + "source": "answers", + "identifier": "currency-answer" + }, + "Sterling" + ] + } + }, + { + "question": { + "id": "second-number-question", + "title": "Second Number Question Title", + "type": "General", + "answers": [ + { + "id": "second-number-answer", + "label": "Second answer in USD", + "mandatory": true, + "type": "Currency", + "currency": "USD", + "decimal_places": 2, + "maximum": { + "value": { + "source": "answers", + "identifier": "set-maximum" + } + }, + "minimum": { + "value": 100 + } + } + ] + }, + "when": { + "==": [ + { + "source": "answers", + "identifier": "currency-answer" + }, + "US Dollars" + ] + } + } + ] + } + ] + } + ] } ] } diff --git a/schemas/test/en/test_optional_guidance_and_description.json b/schemas/test/en/test_optional_guidance_and_description.json new file mode 100644 index 0000000000..38cdb32f44 --- /dev/null +++ b/schemas/test/en/test_optional_guidance_and_description.json @@ -0,0 +1,219 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Other input fields", + "theme": "default", + "description": "A questionnaire to demo optional question guidance and descriptions", + "metadata": [ + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + }, + { + "name": "user_id", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Linear", + "options": { + "summary": { + "collapsible": false + } + } + }, + "sections": [ + { + "id": "default-section", + "groups": [ + { + "blocks": [ + { + "type": "Question", + "id": "description-block", + "question": { + "answers": [ + { + "id": "answer-1", + "label": "Enter any description text that you want to be displayed", + "max_length": 20, + "mandatory": false, + "type": "TextField" + }, + { + "id": "answer-2", + "label": "Enter any alternative text that you want to be displayed", + "max_length": 20, + "mandatory": false, + "type": "TextField" + } + ], + "description": [ + "If entered, only one of the text fields will be used for the description as the next question uses the first_non_empty_item placeholder" + ], + "id": "description-question", + "title": "Do not enter anything here so you get an empty question description and question guidance on the following pages!", + "type": "General" + } + }, + { + "type": "Question", + "id": "mandatory-radio", + "question": { + "guidance": { + "contents": [ + { + "description": { + "text": "{description_text}", + "placeholders": [ + { + "placeholder": "description_text", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "source": "answers", + "identifier": "answer-1" + }, + { + "source": "answers", + "identifier": "answer-2" + } + ] + } + } + ] + } + ] + } + } + ] + }, + "description": [ + { + "text": "{description_text}", + "placeholders": [ + { + "placeholder": "description_text", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "source": "answers", + "identifier": "answer-1" + }, + { + "source": "answers", + "identifier": "answer-2" + } + ] + } + } + ] + } + ] + } + ], + "answers": [ + { + "id": "mandatory-radio-answer", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ], + "type": "Radio" + } + ], + "id": "mandatory-radio-question", + "title": "Were the question description and guidance displayed?", + "type": "General" + } + }, + { + "type": "Question", + "id": "mandatory-radio-two", + "question": { + "guidance": { + "contents": [ + { + "description": "Description with an empty content list" + }, + { + "list": [ + "List item one", + { + "text": "{description_text}", + "placeholders": [ + { + "placeholder": "description_text", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "source": "answers", + "identifier": "answer-1" + }, + { + "source": "answers", + "identifier": "answer-2" + } + ] + } + } + ] + } + ] + } + ] + } + ] + }, + "answers": [ + { + "id": "mandatory-radio-answer-two", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ], + "type": "Radio" + } + ], + "id": "mandatory-radio-question-two", + "title": "Was the contents list in the question guidance displayed?", + "type": "General" + } + } + ], + "id": "radios" + } + ] + } + ] +} diff --git a/schemas/test/en/test_percentage.json b/schemas/test/en/test_percentage.json index 7eb17679ae..29d9c586d4 100644 --- a/schemas/test/en/test_percentage.json +++ b/schemas/test/en/test_percentage.json @@ -49,7 +49,6 @@ "id": "answer", "label": "New to the market 2012-2014", "mandatory": false, - "q_code": "0", "type": "Percentage", "maximum": { "value": 100 @@ -57,7 +56,29 @@ } ], "id": "question", - "title": "Title", + "title": "Enter a percentage", + "type": "General" + } + }, + { + "type": "Question", + "id": "block-decimal", + "question": { + "answers": [ + { + "description": "Enter percentage of growth", + "id": "answer-decimal", + "label": "New to the market 2012-2014", + "mandatory": false, + "type": "Percentage", + "maximum": { + "value": 100 + }, + "decimal_places": 3 + } + ], + "id": "question-decimal", + "title": "Enter a percentage with up to 3 decimal places", "type": "General" } } diff --git a/schemas/test/en/test_placeholder_based_on_first_item_in_list.json b/schemas/test/en/test_placeholder_based_on_first_item_in_list.json index 76169583a3..5b6a1543b0 100644 --- a/schemas/test/en/test_placeholder_based_on_first_item_in_list.json +++ b/schemas/test/en/test_placeholder_based_on_first_item_in_list.json @@ -175,7 +175,9 @@ { "id": "personal-details-section", "title": "Personal Details", - "summary": { "show_on_completion": true }, + "summary": { + "show_on_completion": true + }, "repeat": { "for_list": "people", "title": { @@ -221,17 +223,19 @@ ], "title": "List Status" }, - "when": [ - { - "list": "people", - "id_selector": "first", - "condition": "equals", - "comparison": { + "when": { + "==": [ + { + "identifier": "people", + "source": "list", + "selector": "first" + }, + { "source": "location", - "id": "list_item_id" + "identifier": "list_item_id" } - } - ] + ] + } }, { "content": { @@ -242,17 +246,19 @@ ], "title": "List Status" }, - "when": [ - { - "list": "people", - "id_selector": "first", - "condition": "not equals", - "comparison": { + "when": { + "!=": [ + { + "identifier": "people", + "source": "list", + "selector": "first" + }, + { "source": "location", - "id": "list_item_id" + "identifier": "list_item_id" } - } - ] + ] + } } ], "id": "list-status", @@ -268,7 +274,6 @@ "label": "What is your favourite drink", "max_length": 20, "mandatory": false, - "q_code": "0", "type": "TextField" } ], @@ -303,17 +308,19 @@ "title": "What is your second favourite drink?", "type": "General" }, - "when": [ - { - "list": "people", - "id_selector": "first", - "condition": "equals", - "comparison": { + "when": { + "==": [ + { + "identifier": "people", + "source": "list", + "selector": "first" + }, + { "source": "location", - "id": "list_item_id" + "identifier": "list_item_id" } - } - ] + ] + } }, { "question": { @@ -354,17 +361,19 @@ "title": "What is your second favourite drink?", "type": "General" }, - "when": [ - { - "list": "people", - "id_selector": "first", - "condition": "not equals", - "comparison": { + "when": { + "!=": [ + { + "identifier": "people", + "source": "list", + "selector": "first" + }, + { "source": "location", - "id": "list_item_id" + "identifier": "list_item_id" } - } - ] + ] + } } ], "type": "Question" diff --git a/schemas/test/en/test_placeholder_default_value.json b/schemas/test/en/test_placeholder_default_value.json index c728152865..227ab461bd 100644 --- a/schemas/test/en/test_placeholder_default_value.json +++ b/schemas/test/en/test_placeholder_default_value.json @@ -79,7 +79,7 @@ "contents": [ { "description": { - "text": "The total number of employees confirmed are {answer_employee} , now next question is about training budget", + "text": "The total number of employees confirmed are {answer_employee} , now next question is about training budget", "placeholders": [ { "placeholder": "answer_employee", @@ -136,7 +136,7 @@ "contents": [ { "description": { - "text": "The average training budget per employee is {answer_emp_training}", + "text": "The average training budget per employee is {answer_emp_training}", "placeholders": [ { "placeholder": "answer_emp_training", diff --git a/schemas/test/en/test_placeholder_dependencies_with_calculation_summaries.json b/schemas/test/en/test_placeholder_dependencies_with_calculation_summaries.json new file mode 100644 index 0000000000..24e4fe25fb --- /dev/null +++ b/schemas/test/en/test_placeholder_dependencies_with_calculation_summaries.json @@ -0,0 +1,647 @@ +{ + "language": "en", + "mime_type": "application/json/ons/eq", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Test placeholder dependencies in calculated summary pages", + "questionnaire_flow": { + "type": "Hub", + "options": { + "required_completed_sections": ["reporting-period-section"] + } + }, + "sections": [ + { + "id": "reporting-period-section", + "title": "Reporting period", + "summary": { + "show_on_completion": false, + "page_title": "Reporting period", + "collapsible": false + }, + "show_on_hub": true, + "groups": [ + { + "id": "reporting-period-group", + "blocks": [ + { + "id": "reporting-date", + "type": "Question", + "question": { + "id": "reporting-date-question", + "title": { + "text": "Are you able to report for the calendar year, {ref_p_start_date} to {ref_p_end_date}?", + "placeholders": [ + { + "placeholder": "ref_p_start_date", + "transforms": [ + { + "transform": "format_date", + "arguments": { + "date_to_format": { + "source": "metadata", + "identifier": "ref_p_start_date" + }, + "date_format": "d MMMM yyyy" + } + } + ] + }, + { + "placeholder": "ref_p_end_date", + "transforms": [ + { + "transform": "format_date", + "arguments": { + "date_to_format": { + "source": "metadata", + "identifier": "ref_p_end_date" + }, + "date_format": "d MMMM yyyy" + } + } + ] + } + ] + }, + "type": "General", + "answers": [ + { + "id": "reporting-date-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes, I can report for this period", + "value": "Yes, I can report for this period" + }, + { + "label": "No, I need to report for a different period", + "value": "No, I need to report for a different period" + } + ] + } + ] + }, + "page_title": "Calendar year reporting period", + "routing_rules": [ + { + "block": "undertake-rnd", + "when": { + "in": [ + { + "identifier": "reporting-date-answer", + "source": "answers" + }, + ["Yes, I can report for this period"] + ] + } + }, + { + "block": "enter-dates" + } + ] + }, + { + "id": "enter-dates", + "type": "Question", + "question": { + "id": "enter-dates-question", + "title": "What dates will you be reporting for?", + "type": "DateRange", + "answers": [ + { + "id": "date-from", + "type": "Date", + "mandatory": true, + "label": "From" + }, + { + "id": "date-to", + "type": "Date", + "mandatory": true, + "label": "To" + } + ], + "period_limits": { + "minimum": { + "months": 3 + }, + "maximum": { + "months": 18 + } + } + }, + "page_title": "Alternative reporting period" + }, + { + "id": "undertake-rnd", + "type": "Question", + "question": { + "id": "undertake-rnd-question", + "title": "For the reporting period, did your business undertake any in-house R&D?", + "type": "General", + "answers": [ + { + "id": "undertake-rnd-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "page_title": "In-house R&D for reporting period" + } + ] + } + ] + }, + { + "id": "questions-section", + "title": "In-house R&D", + "summary": { + "show_on_completion": false, + "page_title": "In-house R&D", + "collapsible": false + }, + "show_on_hub": true, + "groups": [ + { + "id": "questions-group", + "blocks": [ + { + "id": "how-much-rnd", + "type": "Question", + "question": { + "id": "how-much-rnd-question", + "title": { + "text": "For the period {from} to {to} what was the expenditure on R&D for {ru_name}?", + "placeholders": [ + { + "placeholder": "from", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "identifier": "date-from", + "source": "answers" + }, + { + "source": "metadata", + "identifier": "ref_p_start_date" + } + ] + } + }, + { + "transform": "format_date", + "arguments": { + "date_to_format": { + "source": "previous_transform" + }, + "date_format": "d MMMM yyyy" + } + } + ] + }, + { + "placeholder": "to", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "identifier": "date-to", + "source": "answers" + }, + { + "source": "metadata", + "identifier": "ref_p_end_date" + } + ] + } + }, + { + "transform": "format_date", + "arguments": { + "date_to_format": { + "source": "previous_transform" + }, + "date_format": "d MMMM yyyy" + } + } + ] + }, + { + "placeholder": "ru_name", + "value": { + "source": "metadata", + "identifier": "ru_name" + } + } + ] + }, + "type": "Calculated", + "answers": [ + { + "id": "civil-research", + "mandatory": true, + "type": "Currency", + "label": "Civil Research and Development", + "description": "Enter a value to the nearest thousand (e.g. 56,000).", + "decimal_places": 0, + "currency": "GBP" + }, + { + "id": "defence", + "mandatory": true, + "type": "Currency", + "label": "Defence Research and Development", + "description": "Enter a value to the nearest thousand (e.g. 56,000).", + "decimal_places": 0, + "currency": "GBP" + } + ], + "calculations": [ + { + "calculation_type": "sum", + "answers_to_calculate": ["civil-research", "defence"], + "conditions": ["greater than"], + "value": 0 + } + ] + }, + "page_title": "In-house expenditure on R&D" + }, + { + "id": "calc-summary-1", + "type": "CalculatedSummary", + "page_title": "Total in-house expenditure on R&D", + "title": { + "text": "We have calculated your total in-house expenditure on R&D for {ru_name} for the period {from} to {to} to be %(total)s. Is this correct?", + "placeholders": [ + { + "placeholder": "from", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "identifier": "date-from", + "source": "answers" + }, + { + "source": "metadata", + "identifier": "ref_p_start_date" + } + ] + } + }, + { + "transform": "format_date", + "arguments": { + "date_to_format": { + "source": "previous_transform" + }, + "date_format": "d MMMM yyyy" + } + } + ] + }, + { + "placeholder": "to", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "identifier": "date-to", + "source": "answers" + }, + { + "source": "metadata", + "identifier": "ref_p_end_date" + } + ] + } + }, + { + "transform": "format_date", + "arguments": { + "date_to_format": { + "source": "previous_transform" + }, + "date_format": "d MMMM yyyy" + } + } + ] + }, + { + "placeholder": "ru_name", + "value": { + "source": "metadata", + "identifier": "ru_name" + } + } + ] + }, + "calculation": { + "operation": { + "+": [ + { + "identifier": "civil-research", + "source": "answers" + }, + { + "identifier": "defence", + "source": "answers" + } + ] + }, + "title": "Total in-house expenditure on R&D" + } + }, + { + "id": "how-much-rnd-2", + "type": "Question", + "question": { + "id": "how-much-rnd-question-2", + "title": { + "text": "For the period {from} to {to} what was the expenditure on R&D for {ru_name}?", + "placeholders": [ + { + "placeholder": "from", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "identifier": "date-from", + "source": "answers" + }, + { + "source": "metadata", + "identifier": "ref_p_start_date" + } + ] + } + }, + { + "transform": "format_date", + "arguments": { + "date_to_format": { + "source": "previous_transform" + }, + "date_format": "d MMMM yyyy" + } + } + ] + }, + { + "placeholder": "to", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "identifier": "date-to", + "source": "answers" + }, + { + "source": "metadata", + "identifier": "ref_p_end_date" + } + ] + } + }, + { + "transform": "format_date", + "arguments": { + "date_to_format": { + "source": "previous_transform" + }, + "date_format": "d MMMM yyyy" + } + } + ] + }, + { + "placeholder": "ru_name", + "value": { + "source": "metadata", + "identifier": "ru_name" + } + } + ] + }, + "type": "Calculated", + "answers": [ + { + "id": "innovation", + "mandatory": true, + "type": "Currency", + "label": "Innovation", + "description": "Enter a value to the nearest thousand (e.g. 56,000).", + "decimal_places": 0, + "currency": "GBP" + }, + { + "id": "software", + "mandatory": true, + "type": "Currency", + "label": "Software Development", + "description": "Enter a value to the nearest thousand (e.g. 56,000).", + "decimal_places": 0, + "currency": "GBP" + } + ], + "calculations": [ + { + "calculation_type": "sum", + "answers_to_calculate": ["innovation", "software"], + "conditions": ["greater than"], + "value": 0 + } + ] + }, + "page_title": "In-house expenditure on R&D" + }, + { + "id": "calc-summary-2", + "type": "CalculatedSummary", + "page_title": "Total in-house expenditure on R&D - Part Two", + "title": { + "text": "We have calculated your total in-house expenditure on R&D for {ru_name} for the period {from} to {to} to be %(total)s. Is this correct?", + "placeholders": [ + { + "placeholder": "from", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "identifier": "date-from", + "source": "answers" + }, + { + "source": "metadata", + "identifier": "ref_p_start_date" + } + ] + } + }, + { + "transform": "format_date", + "arguments": { + "date_to_format": { + "source": "previous_transform" + }, + "date_format": "d MMMM yyyy" + } + } + ] + }, + { + "placeholder": "to", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "identifier": "date-to", + "source": "answers" + }, + { + "source": "metadata", + "identifier": "ref_p_end_date" + } + ] + } + }, + { + "transform": "format_date", + "arguments": { + "date_to_format": { + "source": "previous_transform" + }, + "date_format": "d MMMM yyyy" + } + } + ] + }, + { + "placeholder": "ru_name", + "value": { + "source": "metadata", + "identifier": "ru_name" + } + } + ] + }, + "calculation": { + "operation": { + "+": [ + { + "identifier": "innovation", + "source": "answers" + }, + { + "identifier": "software", + "source": "answers" + } + ] + }, + "title": "Total in-house expenditure on R&D - Part Two" + } + }, + { + "type": "GrandCalculatedSummary", + "id": "rnd-grand-calculated-summary", + "title": "We have calculated the grand total of in-house expenditure on R&D to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "calculated_summary", + "identifier": "calc-summary-1" + }, + { + "source": "calculated_summary", + "identifier": "calc-summary-2" + } + ] + }, + "title": "Grand Total in-house expenditure on R&D" + } + } + ] + } + ], + "enabled": { + "when": { + "in": [ + { + "identifier": "undertake-rnd-answer", + "source": "answers" + }, + ["Yes"] + ] + } + } + } + ], + "theme": "business", + "navigation": { + "visible": false + }, + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + }, + { + "name": "ru_ref", + "type": "string" + }, + { + "name": "trad_as", + "type": "string", + "optional": true + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ref_p_start_date", + "type": "date" + }, + { + "name": "ref_p_end_date", + "type": "date" + } + ] +} diff --git a/schemas/test/en/test_placeholder_difference_in_years.json b/schemas/test/en/test_placeholder_difference_in_years.json index f4a12e4904..ab633d7326 100644 --- a/schemas/test/en/test_placeholder_difference_in_years.json +++ b/schemas/test/en/test_placeholder_difference_in_years.json @@ -103,21 +103,19 @@ }, "routing_rules": [ { - "goto": { - "block": "age-block", - "when": [ + "block": "age-block", + "when": { + "==": [ { - "id": "date-test-answer", - "condition": "equals", - "value": "No" - } + "source": "answers", + "identifier": "date-test-answer" + }, + "No" ] } }, { - "goto": { - "section": "End" - } + "section": "End" } ] } diff --git a/schemas/test/en/test_placeholder_difference_in_years_month_year.json b/schemas/test/en/test_placeholder_difference_in_years_month_year.json index 526033720d..e25029ffdf 100644 --- a/schemas/test/en/test_placeholder_difference_in_years_month_year.json +++ b/schemas/test/en/test_placeholder_difference_in_years_month_year.json @@ -103,21 +103,19 @@ }, "routing_rules": [ { - "goto": { - "block": "age-block", - "when": [ + "block": "age-block", + "when": { + "==": [ { - "id": "date-test-answer", - "condition": "equals", - "value": "No" - } + "source": "answers", + "identifier": "date-test-answer" + }, + "No" ] } }, { - "goto": { - "section": "End" - } + "section": "End" } ] } diff --git a/schemas/test/en/test_placeholder_difference_in_years_month_year_range.json b/schemas/test/en/test_placeholder_difference_in_years_month_year_range.json index 7b84830fbf..61071e78ee 100644 --- a/schemas/test/en/test_placeholder_difference_in_years_month_year_range.json +++ b/schemas/test/en/test_placeholder_difference_in_years_month_year_range.json @@ -110,21 +110,19 @@ }, "routing_rules": [ { - "goto": { - "block": "date-block", - "when": [ + "block": "date-block", + "when": { + "==": [ { - "id": "date-test-answer", - "condition": "equals", - "value": "No" - } + "source": "answers", + "identifier": "date-test-answer" + }, + "No" ] } }, { - "goto": { - "section": "End" - } + "section": "End" } ] } diff --git a/schemas/test/en/test_placeholder_difference_in_years_range.json b/schemas/test/en/test_placeholder_difference_in_years_range.json index 42ac8db467..35f2356af4 100644 --- a/schemas/test/en/test_placeholder_difference_in_years_range.json +++ b/schemas/test/en/test_placeholder_difference_in_years_range.json @@ -110,21 +110,19 @@ }, "routing_rules": [ { - "goto": { - "block": "date-block", - "when": [ + "block": "date-block", + "when": { + "==": [ { - "id": "date-test-answer", - "condition": "equals", - "value": "No" - } + "source": "answers", + "identifier": "date-test-answer" + }, + "No" ] } }, { - "goto": { - "section": "End" - } + "section": "End" } ] } diff --git a/schemas/test/en/test_placeholder_first_non_empty_item.json b/schemas/test/en/test_placeholder_first_non_empty_item.json new file mode 100644 index 0000000000..a0c6d6e8df --- /dev/null +++ b/schemas/test/en/test_placeholder_first_non_empty_item.json @@ -0,0 +1,275 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Other input fields", + "theme": "default", + "description": "Questionnaire to check placeholder takes account answers on a path", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + }, + { + "name": "ru_ref", + "type": "string" + }, + { + "name": "trad_as", + "type": "string", + "optional": true + }, + { + "name": "ref_p_start_date", + "type": "date" + }, + { + "name": "ref_p_end_date", + "type": "date" + } + ], + "questionnaire_flow": { + "type": "Linear", + "options": { + "summary": { + "collapsible": false + } + } + }, + "sections": [ + { + "id": "default-section", + "groups": [ + { + "id": "default-group", + "blocks": [ + { + "id": "date-question-block", + "type": "Question", + "question": { + "id": "date-question", + "title": { + "text": "Are you able to report for the period from {ref_p_start_date} to {ref_p_end_date}?", + "placeholders": [ + { + "placeholder": "ref_p_start_date", + "transforms": [ + { + "transform": "format_date", + "arguments": { + "date_to_format": { + "source": "metadata", + "identifier": "ref_p_start_date" + }, + "date_format": "d MMMM yyyy" + } + } + ] + }, + { + "placeholder": "ref_p_end_date", + "transforms": [ + { + "transform": "format_date", + "arguments": { + "date_to_format": { + "source": "metadata", + "identifier": "ref_p_end_date" + }, + "date_format": "d MMMM yyyy" + } + } + ] + } + ] + }, + "type": "General", + "answers": [ + { + "id": "date-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes, I can report for this period", + "value": "Yes, I can report for this period" + }, + { + "label": "No, I need to report for a different period", + "value": "No, I need to report for a different period" + } + ] + } + ] + }, + "routing_rules": [ + { + "block": "total-turnover-block", + "when": { + "in": [ + { + "identifier": "date-answer", + "source": "answers" + }, + ["Yes, I can report for this period"] + ] + } + }, + { + "block": "date-entry-block" + } + ] + }, + { + "id": "date-entry-block", + "type": "Question", + "question": { + "id": "date-entry-question", + "title": "What are the dates of the period that you will be reporting for?", + "guidance": { + "contents": [ + { + "description": "Enter a date between 1st of May 2016 and the 31st of May 2016" + } + ] + }, + "type": "DateRange", + "answers": [ + { + "id": "date-entry-answer-from", + "type": "Date", + "mandatory": true, + "label": "From", + "minimum": { + "value": { + "source": "metadata", + "identifier": "ref_p_start_date" + }, + "offset_by": { + "days": -19 + } + } + }, + { + "id": "date-entry-answer-to", + "type": "Date", + "mandatory": true, + "label": "To", + "maximum": { + "value": { + "source": "metadata", + "identifier": "ref_p_end_date" + }, + "offset_by": { + "days": 20 + } + } + } + ] + } + }, + { + "id": "total-turnover-block", + "type": "Question", + "question": { + "id": "total-turnover-question", + "title": { + "text": "For the period {date_entry_answer_from} to {date_entry_answer_to}, what was {ru_name}'s total turnover, excluding VAT?", + "placeholders": [ + { + "placeholder": "date_entry_answer_from", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "source": "answers", + "identifier": "date-entry-answer-from" + }, + { + "source": "metadata", + "identifier": "ref_p_start_date" + } + ] + } + }, + { + "transform": "format_date", + "arguments": { + "date_to_format": { + "source": "previous_transform" + }, + "date_format": "d MMMM yyyy" + } + } + ] + }, + { + "placeholder": "date_entry_answer_to", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "source": "answers", + "identifier": "date-entry-answer-to" + }, + { + "source": "metadata", + "identifier": "ref_p_end_date" + } + ] + } + }, + { + "transform": "format_date", + "arguments": { + "date_to_format": { + "source": "previous_transform" + }, + "date_format": "d MMMM yyyy" + } + } + ] + }, + { + "placeholder": "ru_name", + "value": { + "source": "metadata", + "identifier": "ru_name" + } + } + ] + }, + "type": "General", + "answers": [ + { + "id": "total-turnover-answer", + "mandatory": true, + "type": "Currency", + "label": "Total turnover excluding VAT", + "description": "Enter the full value (e.g. 56,234.33) or a value to the nearest £thousand (e.g. 56,000). Do not enter '56' for £56,000.", + "decimal_places": 2, + "currency": "GBP" + } + ] + } + } + ] + } + ] + } + ] +} diff --git a/schemas/test/en/test_placeholder_first_non_empty_item_cross_section_dependencies.json b/schemas/test/en/test_placeholder_first_non_empty_item_cross_section_dependencies.json new file mode 100644 index 0000000000..ec3c2f1734 --- /dev/null +++ b/schemas/test/en/test_placeholder_first_non_empty_item_cross_section_dependencies.json @@ -0,0 +1,283 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Other input fields", + "theme": "default", + "description": "Questionnaire to check placeholder takes account answers on a path with cross section dependencies", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + }, + { + "name": "ru_ref", + "type": "string" + }, + { + "name": "trad_as", + "type": "string", + "optional": true + }, + { + "name": "ref_p_start_date", + "type": "date" + }, + { + "name": "ref_p_end_date", + "type": "date" + } + ], + "questionnaire_flow": { + "type": "Hub", + "options": {} + }, + "sections": [ + { + "id": "default-section", + "title": "Date Selection", + "groups": [ + { + "id": "default-group", + "blocks": [ + { + "id": "date-question-block", + "type": "Question", + "question": { + "id": "date-question", + "title": { + "text": "Are you able to report for the period from {ref_p_start_date} to {ref_p_end_date}?", + "placeholders": [ + { + "placeholder": "ref_p_start_date", + "transforms": [ + { + "transform": "format_date", + "arguments": { + "date_to_format": { + "source": "metadata", + "identifier": "ref_p_start_date" + }, + "date_format": "d MMMM yyyy" + } + } + ] + }, + { + "placeholder": "ref_p_end_date", + "transforms": [ + { + "transform": "format_date", + "arguments": { + "date_to_format": { + "source": "metadata", + "identifier": "ref_p_end_date" + }, + "date_format": "d MMMM yyyy" + } + } + ] + } + ] + }, + "type": "General", + "answers": [ + { + "id": "date-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes, I can report for this period", + "value": "Yes, I can report for this period" + }, + { + "label": "No, I need to report for a different period", + "value": "No, I need to report for a different period" + } + ] + } + ] + }, + "routing_rules": [ + { + "section": "End", + "when": { + "in": [ + { + "identifier": "date-answer", + "source": "answers" + }, + ["Yes, I can report for this period"] + ] + } + }, + { + "block": "date-entry-block" + } + ] + }, + { + "id": "date-entry-block", + "type": "Question", + "question": { + "id": "date-entry-question", + "title": "What are the dates of the period that you will be reporting for?", + "guidance": { + "contents": [ + { + "description": "Enter a date between 1st of May 2016 and the 31st of May 2016" + } + ] + }, + "type": "DateRange", + "answers": [ + { + "id": "date-entry-answer-from", + "type": "Date", + "mandatory": true, + "label": "From", + "minimum": { + "value": { + "source": "metadata", + "identifier": "ref_p_start_date" + }, + "offset_by": { + "days": -19 + } + } + }, + { + "id": "date-entry-answer-to", + "type": "Date", + "mandatory": true, + "label": "To", + "maximum": { + "value": { + "source": "metadata", + "identifier": "ref_p_end_date" + }, + "offset_by": { + "days": 20 + } + } + } + ] + } + } + ] + } + ] + }, + { + "id": "second-section", + "title": "Food Expenses", + "groups": [ + { + "id": "second-section-group", + "blocks": [ + { + "id": "food-question-block", + "type": "Question", + "question": { + "id": "food-question", + "title": { + "text": "For the period {date_entry_answer_from} to {date_entry_answer_to}, how much do you spend on food?", + "placeholders": [ + { + "placeholder": "date_entry_answer_from", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "source": "answers", + "identifier": "date-entry-answer-from" + }, + { + "source": "metadata", + "identifier": "ref_p_start_date" + } + ] + } + }, + { + "transform": "format_date", + "arguments": { + "date_to_format": { + "source": "previous_transform" + }, + "date_format": "d MMMM yyyy" + } + } + ] + }, + { + "placeholder": "date_entry_answer_to", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "source": "answers", + "identifier": "date-entry-answer-to" + }, + { + "source": "metadata", + "identifier": "ref_p_end_date" + } + ] + } + }, + { + "transform": "format_date", + "arguments": { + "date_to_format": { + "source": "previous_transform" + }, + "date_format": "d MMMM yyyy" + } + } + ] + }, + { + "placeholder": "ru_name", + "value": { + "source": "metadata", + "identifier": "ru_name" + } + } + ] + }, + "type": "General", + "answers": [ + { + "id": "food-answer", + "mandatory": true, + "type": "Currency", + "label": "Total food expense", + "description": "Enter the full value (e.g. 32.33) or a value to the nearest £thousand (e.g. 56,000). Do not enter '56' for £56,000.", + "decimal_places": 2, + "currency": "GBP" + } + ] + } + } + ] + } + ] + } + ] +} diff --git a/schemas/test/en/test_placeholder_first_non_empty_item_repeating_sections.json b/schemas/test/en/test_placeholder_first_non_empty_item_repeating_sections.json new file mode 100644 index 0000000000..5feccb91fd --- /dev/null +++ b/schemas/test/en/test_placeholder_first_non_empty_item_repeating_sections.json @@ -0,0 +1,528 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Test first non empty item with repeating sections", + "theme": "default", + "description": "Questionnaire to test first non empty item with repeating sections", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + }, + { + "name": "ref_p_start_date", + "type": "date" + }, + { + "name": "ref_p_end_date", + "type": "date" + } + ], + "questionnaire_flow": { + "type": "Hub", + "options": {} + }, + "sections": [ + { + "id": "date-section", + "title": "Date Range", + "groups": [ + { + "id": "date-group", + "blocks": [ + { + "id": "date-question-block", + "type": "Question", + "question": { + "id": "date-question", + "title": { + "text": "Are you able to report for the period from {ref_p_start_date} to {ref_p_end_date}?", + "placeholders": [ + { + "placeholder": "ref_p_start_date", + "transforms": [ + { + "transform": "format_date", + "arguments": { + "date_to_format": { + "source": "metadata", + "identifier": "ref_p_start_date" + }, + "date_format": "d MMMM yyyy" + } + } + ] + }, + { + "placeholder": "ref_p_end_date", + "transforms": [ + { + "transform": "format_date", + "arguments": { + "date_to_format": { + "source": "metadata", + "identifier": "ref_p_end_date" + }, + "date_format": "d MMMM yyyy" + } + } + ] + } + ] + }, + "type": "General", + "answers": [ + { + "id": "date-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes, I can report for this period", + "value": "Yes, I can report for this period" + }, + { + "label": "No, I need to report for a different period", + "value": "No, I need to report for a different period" + } + ] + } + ] + }, + "routing_rules": [ + { + "section": "End", + "when": { + "in": [ + { + "identifier": "date-answer", + "source": "answers" + }, + ["Yes, I can report for this period"] + ] + } + }, + { + "block": "date-entry-block" + } + ] + }, + { + "id": "date-entry-block", + "type": "Question", + "question": { + "id": "date-entry-question", + "title": "What are the dates of the period that you will be reporting for?", + "guidance": { + "contents": [ + { + "description": "Enter a date between 1st of May 2016 and the 31st of May 2016" + } + ] + }, + "type": "DateRange", + "answers": [ + { + "id": "date-entry-answer-from", + "type": "Date", + "mandatory": true, + "label": "From", + "minimum": { + "value": { + "source": "metadata", + "identifier": "ref_p_start_date" + }, + "offset_by": { + "days": -19 + } + } + }, + { + "id": "date-entry-answer-to", + "type": "Date", + "mandatory": true, + "label": "To", + "maximum": { + "value": { + "source": "metadata", + "identifier": "ref_p_end_date" + }, + "offset_by": { + "days": 20 + } + } + } + ] + } + } + ] + } + ] + }, + { + "id": "list-collector-section", + "title": "List Collector Section", + "summary": { + "show_on_completion": true, + "items": [ + { + "type": "List", + "for_list": "people", + "title": "Household members", + "add_link_text": "Add someone to this household", + "empty_list_text": "There are no householders" + } + ] + }, + "groups": [ + { + "id": "group", + "title": "List", + "blocks": [ + { + "id": "list-collector", + "type": "ListCollector", + "for_list": "people", + "question": { + "id": "confirmation-question", + "title": { + "text": "Between {date_entry_answer_from} to {date_entry_answer_to} did anyone else live here?", + "placeholders": [ + { + "placeholder": "date_entry_answer_from", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "source": "answers", + "identifier": "date-entry-answer-from" + }, + { + "source": "metadata", + "identifier": "ref_p_start_date" + } + ] + } + }, + { + "transform": "format_date", + "arguments": { + "date_to_format": { + "source": "previous_transform" + }, + "date_format": "d MMMM yyyy" + } + } + ] + }, + { + "placeholder": "date_entry_answer_to", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "source": "answers", + "identifier": "date-entry-answer-to" + }, + { + "source": "metadata", + "identifier": "ref_p_end_date" + } + ] + } + }, + { + "transform": "format_date", + "arguments": { + "date_to_format": { + "source": "previous_transform" + }, + "date_format": "d MMMM yyyy" + } + } + ] + } + ] + }, + "type": "General", + "answers": [ + { + "id": "anyone-else", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "add_block": { + "id": "add-person", + "type": "ListAddQuestion", + "cancel_text": "Don’t need to add anyone else?", + "question": { + "id": "add-question", + "type": "General", + "title": "What is the name of the person?", + "answers": [ + { + "id": "first-name", + "label": "First name", + "mandatory": true, + "type": "TextField" + }, + { + "id": "last-name", + "label": "Last name", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "edit_block": { + "id": "edit-person", + "type": "ListEditQuestion", + "cancel_text": "Don’t need to change anything?", + "question": { + "id": "edit-question", + "type": "General", + "title": "What is the name of the person?", + "answers": [ + { + "id": "first-name", + "label": "First name", + "mandatory": true, + "type": "TextField" + }, + { + "id": "last-name", + "label": "Last name", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "remove_block": { + "id": "remove-person", + "type": "ListRemoveQuestion", + "cancel_text": "Don’t need to remove this person?", + "question": { + "id": "remove-question", + "type": "General", + "title": "Are you sure you want to remove this person?", + "warning": "All of the information about this person will be deleted", + "answers": [ + { + "id": "remove-confirmation", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RemoveListItemAndAnswers" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + "summary": { + "title": "Household members", + "item_title": { + "text": "{person_name}", + "placeholders": [ + { + "placeholder": "person_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "first-name" + }, + { + "source": "answers", + "identifier": "last-name" + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + } + } + } + ] + } + ] + }, + { + "id": "repeating-section", + "title": "Personal Details", + "summary": { "show_on_completion": true }, + "repeat": { + "for_list": "people", + "title": { + "text": "{person_name}", + "placeholders": [ + { + "placeholder": "person_name", + "transforms": [ + { + "transform": "concatenate_list", + "arguments": { + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "first-name" + }, + { + "source": "answers", + "identifier": "last-name" + } + ], + "delimiter": " " + } + } + ] + } + ] + } + }, + "groups": [ + { + "id": "personal-details-group", + "title": "Personal Details", + "blocks": [ + { + "type": "Question", + "id": "personal-details-block", + "question": { + "type": "General", + "id": "personal-details-question", + "title": { + "text": "Between {date_entry_answer_from} to {date_entry_answer_to} what did you drink the most?", + "placeholders": [ + { + "placeholder": "date_entry_answer_from", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "source": "answers", + "identifier": "date-entry-answer-from" + }, + { + "source": "metadata", + "identifier": "ref_p_start_date" + } + ] + } + }, + { + "transform": "format_date", + "arguments": { + "date_to_format": { + "source": "previous_transform" + }, + "date_format": "d MMMM yyyy" + } + } + ] + }, + { + "placeholder": "date_entry_answer_to", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "source": "answers", + "identifier": "date-entry-answer-to" + }, + { + "source": "metadata", + "identifier": "ref_p_end_date" + } + ] + } + }, + { + "transform": "format_date", + "arguments": { + "date_to_format": { + "source": "previous_transform" + }, + "date_format": "d MMMM yyyy" + } + } + ] + } + ] + }, + "answers": [ + { + "id": "personal-details-answer", + "mandatory": true, + "options": [ + { + "label": "Coffee", + "value": "Coffee" + }, + { + "label": "Tea", + "value": "Tea" + } + ], + "type": "Checkbox" + } + ] + } + } + ] + } + ] + } + ] +} diff --git a/schemas/test/en/test_placeholder_full.json b/schemas/test/en/test_placeholder_full.json index dd4299d0ae..8218e923af 100644 --- a/schemas/test/en/test_placeholder_full.json +++ b/schemas/test/en/test_placeholder_full.json @@ -250,21 +250,19 @@ }, "routing_rules": [ { - "goto": { - "group": "dob-input-group", - "when": [ + "group": "dob-input-group", + "when": { + "==": [ { - "id": "confirm-date-of-birth-answer-proxy", - "condition": "equals", - "value": "No, I need to change their date of birth" - } + "source": "answers", + "identifier": "confirm-date-of-birth-answer-proxy" + }, + "No, I need to change their date of birth" ] } }, { - "goto": { - "section": "End" - } + "section": "End" } ] } diff --git a/schemas/test/en/test_placeholder_option_label_from_value.json b/schemas/test/en/test_placeholder_option_label_from_value.json index 6e2227e41b..79102c41e6 100644 --- a/schemas/test/en/test_placeholder_option_label_from_value.json +++ b/schemas/test/en/test_placeholder_option_label_from_value.json @@ -103,7 +103,7 @@ "question": { "id": "confirmation-question-radio", "title": { - "text": "You chose {business_name} as your business name, is that correct ?", + "text": "You chose {business_name} as your business name, is that correct ?", "placeholders": [ { "placeholder": "business_name", diff --git a/schemas/test/en/test_placeholder_playback_list.json b/schemas/test/en/test_placeholder_playback_list.json index 03bc2a4521..6844986e74 100644 --- a/schemas/test/en/test_placeholder_playback_list.json +++ b/schemas/test/en/test_placeholder_playback_list.json @@ -46,37 +46,30 @@ "options": [ { "label": "None", - "value": "None", - "q_code": "0" + "value": "None" }, { "label": "Cheese", - "value": "Cheese", - "q_code": "1" + "value": "Cheese" }, { "label": "Ham", - "value": "Ham", - "q_code": "2" + "value": "Ham" }, { "label": "Pineapple", - "value": "Pineapple", - "q_code": "3" + "value": "Pineapple" }, { "label": "Tuna", - "value": "Tuna", - "q_code": "4" + "value": "Tuna" }, { "label": "Pepperoni", - "value": "Pepperoni", - "q_code": "5" + "value": "Pepperoni" }, { "label": "Other", - "q_code": "6", "description": "Choose any other topping", "value": "Other", "detail_answer": { @@ -146,21 +139,19 @@ }, "routing_rules": [ { - "goto": { - "when": [ + "when": { + "==": [ { - "value": "No I need to change this", - "id": "confirm-answers", - "condition": "equals" - } - ], - "block": "mandatory-checkbox" - } + "source": "answers", + "identifier": "confirm-answers" + }, + "No I need to change this" + ] + }, + "block": "mandatory-checkbox" }, { - "goto": { - "section": "End" - } + "section": "End" } ] } diff --git a/schemas/test/en/test_placeholder_transform.json b/schemas/test/en/test_placeholder_transform.json index ceba160023..78f2bfb63e 100644 --- a/schemas/test/en/test_placeholder_transform.json +++ b/schemas/test/en/test_placeholder_transform.json @@ -108,7 +108,7 @@ "title": "Please enter the value of internet sales", "description": [ { - "text": "Of the {total_turnover} total retail turnover, what was the value of internet sales?", + "text": "Of the {total_turnover} total retail turnover, what was the value of internet sales?", "placeholders": [ { "placeholder": "total_turnover", @@ -221,11 +221,112 @@ ] } ], - "text": "Do you want to add {item} item?" + "text": "Do you want to add {item} item?" }, "type": "General" }, "type": "Question" + }, + { + "type": "Question", + "id": "training-percentage-block", + "question": { + "answers": [ + { + "id": "training-percentage", + "mandatory": false, + "decimal_places": 0, + "type": "Percentage", + "label": "Percentage of company budget", + "default": 0 + } + ], + "id": "training-percentage-question", + "title": "What percentage of the company budget you spend on training ?", + "type": "General" + } + }, + { + "id": "training-percentage-interstitial", + "content": { + "title": "Percentage of budget spent on training interstitial", + "contents": [ + { + "description": { + "text": "The percentage of the company budget you spend on training is {answer_percentage}", + "placeholders": [ + { + "placeholder": "answer_percentage", + "transforms": [ + { + "transform": "format_percentage", + "arguments": { + "value": { + "source": "answers", + "identifier": "training-percentage" + } + } + } + ] + } + ] + } + } + ] + }, + "type": "Interstitial" + }, + { + "type": "Question", + "id": "average-distance-block", + "question": { + "answers": [ + { + "id": "average-distance", + "mandatory": false, + "unit": "length-mile", + "type": "Unit", + "unit_length": "long", + "label": "Average commuting distance", + "default": 0 + } + ], + "id": "average-distance-question", + "title": "What is the average commuting distance of an employee (in miles) ?", + "type": "General" + } + }, + { + "id": "average-distance-interstitial", + "content": { + "title": "Average commuting distance interstitial", + "contents": [ + { + "description": { + "text": "The average commuting distance of an employee is {answer_distance}", + "placeholders": [ + { + "placeholder": "answer_distance", + "transforms": [ + { + "transform": "format_unit", + "arguments": { + "value": { + "source": "answers", + "identifier": "average-distance" + }, + "unit": "length-mile", + "unit_length": "long" + } + } + ] + } + ] + } + } + ] + }, + "type": "Interstitial" } ] } diff --git a/schemas/test/en/test_progress_block_value_source_repeating_sections.json b/schemas/test/en/test_progress_block_value_source_repeating_sections.json new file mode 100644 index 0000000000..764e05b6ea --- /dev/null +++ b/schemas/test/en/test_progress_block_value_source_repeating_sections.json @@ -0,0 +1,392 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "survey_id": "139", + "theme": "default", + "title": "Progress Value Source Repeating Sections Test", + "data_version": "0.0.3", + "description": "Progress Value Source Repeating Sections Test", + "navigation": { + "visible": true + }, + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + }, + { + "name": "trad_as", + "type": "string", + "optional": true + } + ], + "questionnaire_flow": { + "type": "Hub", + "options": {} + }, + "sections": [ + { + "id": "section-1", + "title": "List collector + random question", + "groups": [ + { + "id": "group", + "title": "List", + "blocks": [ + { + "id": "list-collector", + "type": "ListCollector", + "for_list": "people", + "question": { + "id": "confirmation-question", + "type": "General", + "title": "Does anyone else live here?", + "answers": [ + { + "id": "anyone-else", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "add_block": { + "id": "add-person", + "type": "ListAddQuestion", + "question": { + "id": "add-question", + "type": "General", + "title": "What is the name of the person?", + "answers": [ + { + "id": "first-name", + "label": "First name", + "mandatory": true, + "type": "TextField" + }, + { + "id": "last-name", + "label": "Last name", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "edit_block": { + "id": "edit-person", + "type": "ListEditQuestion", + "question": { + "id": "edit-question", + "type": "General", + "title": "What is the name of the person?", + "answers": [ + { + "id": "first-name", + "label": "First name", + "mandatory": true, + "type": "TextField" + }, + { + "id": "last-name", + "label": "Last name", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "remove_block": { + "id": "remove-person", + "type": "ListRemoveQuestion", + "question": { + "id": "remove-question", + "type": "General", + "title": "Are you sure you want to remove this person?", + "answers": [ + { + "id": "remove-confirmation", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RemoveListItemAndAnswers" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + "summary": { + "title": "Household members", + "item_title": { + "text": "{person_name}", + "placeholders": [ + { + "placeholder": "person_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "first-name" + }, + { + "source": "answers", + "identifier": "last-name" + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + } + } + }, + { + "type": "Question", + "id": "question-block", + "question": { + "id": "question", + "title": "Question", + "description": ["The next question is used as a dependency in the repeating sections."], + "type": "General", + "answers": [ + { + "id": "answer", + "mandatory": false, + "label": "Enter any number", + "type": "Number" + } + ] + } + }, + { + "type": "Question", + "id": "random-question-enabler-block", + "question": { + "id": "random-question-enabler-question", + "title": "Random question enabler", + "description": [ + "Answering this question will enable the random question in the repeated section coming after the list collector." + ], + "type": "General", + "answers": [ + { + "id": "random-question-enabler-answer", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + } + } + ] + } + ] + }, + { + "id": "section-2", + "title": "Questions", + "summary": { "show_on_completion": true }, + "repeat": { + "for_list": "people", + "title": { + "text": "{person_name}", + "placeholders": [ + { + "placeholder": "person_name", + "transforms": [ + { + "transform": "concatenate_list", + "arguments": { + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "first-name" + }, + { + "source": "answers", + "identifier": "last-name" + } + ], + "delimiter": " " + } + } + ] + } + ] + } + }, + "groups": [ + { + "id": "dob-group", + "title": "Date of birth", + "blocks": [ + { + "type": "Question", + "id": "dob-block", + "question": { + "answers": [ + { + "id": "date-of-birth-answer", + "mandatory": false, + "maximum": { + "value": "now" + }, + "minimum": { + "offset_by": { + "years": -115 + }, + "value": "2019-10-13" + }, + "type": "Date" + } + ], + "guidance": { + "contents": [ + { + "description": "For example 31 12 1970" + } + ] + }, + "id": "date-of-birth-question", + "title": { + "placeholders": [ + { + "placeholder": "person_name_possessive", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "first-name" + }, + { + "source": "answers", + "identifier": "last-name" + } + ] + }, + "transform": "concatenate_list" + }, + { + "arguments": { + "string_to_format": { + "source": "previous_transform" + } + }, + "transform": "format_possessive" + } + ] + } + ], + "text": "What is {person_name_possessive} date of birth?" + }, + "type": "General" + } + }, + { + "type": "Question", + "id": "other-question-block", + "question": { + "id": "other-question", + "answers": [ + { + "id": "other-answer", + "mandatory": true, + "label": "Anything", + "type": "Number" + } + ], + "title": { + "placeholders": [ + { + "placeholder": "person_name_possessive", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "first-name" + }, + { + "source": "answers", + "identifier": "last-name" + } + ] + }, + "transform": "concatenate_list" + }, + { + "arguments": { + "string_to_format": { + "source": "previous_transform" + } + }, + "transform": "format_possessive" + } + ] + } + ], + "text": "Random question about {person_name_possessive}" + }, + "description": ["Shows because the random question was completed in section 1"], + "type": "General" + }, + "skip_conditions": { + "when": { + "!=": [ + { + "source": "progress", + "selector": "block", + "identifier": "random-question-enabler-block" + }, + "COMPLETED" + ] + } + } + } + ] + } + ] + } + ] +} diff --git a/schemas/test/en/test_progress_section_value_source_repeating_sections.json b/schemas/test/en/test_progress_section_value_source_repeating_sections.json new file mode 100644 index 0000000000..dcdfad3364 --- /dev/null +++ b/schemas/test/en/test_progress_section_value_source_repeating_sections.json @@ -0,0 +1,392 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "survey_id": "139", + "theme": "default", + "title": "Progress Value Source Repeating Sections Test", + "data_version": "0.0.3", + "description": "Progress Value Source Repeating Sections Test", + "navigation": { + "visible": true + }, + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + }, + { + "name": "trad_as", + "type": "string", + "optional": true + } + ], + "questionnaire_flow": { + "type": "Hub", + "options": {} + }, + "sections": [ + { + "id": "section-1", + "title": "List collector + random question", + "groups": [ + { + "id": "group", + "title": "List", + "blocks": [ + { + "id": "list-collector", + "type": "ListCollector", + "for_list": "people", + "question": { + "id": "confirmation-question", + "type": "General", + "title": "Does anyone else live here?", + "answers": [ + { + "id": "anyone-else", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "add_block": { + "id": "add-person", + "type": "ListAddQuestion", + "question": { + "id": "add-question", + "type": "General", + "title": "What is the name of the person?", + "answers": [ + { + "id": "first-name", + "label": "First name", + "mandatory": true, + "type": "TextField" + }, + { + "id": "last-name", + "label": "Last name", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "edit_block": { + "id": "edit-person", + "type": "ListEditQuestion", + "question": { + "id": "edit-question", + "type": "General", + "title": "What is the name of the person?", + "answers": [ + { + "id": "first-name", + "label": "First name", + "mandatory": true, + "type": "TextField" + }, + { + "id": "last-name", + "label": "Last name", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "remove_block": { + "id": "remove-person", + "type": "ListRemoveQuestion", + "question": { + "id": "remove-question", + "type": "General", + "title": "Are you sure you want to remove this person?", + "answers": [ + { + "id": "remove-confirmation", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RemoveListItemAndAnswers" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + "summary": { + "title": "Household members", + "item_title": { + "text": "{person_name}", + "placeholders": [ + { + "placeholder": "person_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "first-name" + }, + { + "source": "answers", + "identifier": "last-name" + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + } + } + }, + { + "type": "Question", + "id": "question-block", + "question": { + "id": "question", + "title": "Question", + "description": ["The next question is used as a dependency in the repeating sections."], + "type": "General", + "answers": [ + { + "id": "answer", + "mandatory": false, + "label": "Enter any number", + "type": "Number" + } + ] + } + }, + { + "type": "Question", + "id": "random-question-enabler-block", + "question": { + "id": "random-question-enabler-question", + "title": "Random question enabler", + "description": [ + "Answering this question will enable the random question in the repeated section coming after the list collector." + ], + "type": "General", + "answers": [ + { + "id": "random-question-enabler-answer", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + } + } + ] + } + ] + }, + { + "id": "section-2", + "title": "Questions", + "summary": { "show_on_completion": true }, + "repeat": { + "for_list": "people", + "title": { + "text": "{person_name}", + "placeholders": [ + { + "placeholder": "person_name", + "transforms": [ + { + "transform": "concatenate_list", + "arguments": { + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "first-name" + }, + { + "source": "answers", + "identifier": "last-name" + } + ], + "delimiter": " " + } + } + ] + } + ] + } + }, + "groups": [ + { + "id": "dob-group", + "title": "Date of birth", + "blocks": [ + { + "type": "Question", + "id": "dob-block", + "question": { + "answers": [ + { + "id": "date-of-birth-answer", + "mandatory": false, + "maximum": { + "value": "now" + }, + "minimum": { + "offset_by": { + "years": -115 + }, + "value": "2019-10-13" + }, + "type": "Date" + } + ], + "guidance": { + "contents": [ + { + "description": "For example 31 12 1970" + } + ] + }, + "id": "date-of-birth-question", + "title": { + "placeholders": [ + { + "placeholder": "person_name_possessive", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "first-name" + }, + { + "source": "answers", + "identifier": "last-name" + } + ] + }, + "transform": "concatenate_list" + }, + { + "arguments": { + "string_to_format": { + "source": "previous_transform" + } + }, + "transform": "format_possessive" + } + ] + } + ], + "text": "What is {person_name_possessive} date of birth?" + }, + "type": "General" + } + }, + { + "type": "Question", + "id": "other-question-block", + "question": { + "id": "other-question", + "answers": [ + { + "id": "other-answer", + "mandatory": true, + "label": "Anything", + "type": "Number" + } + ], + "title": { + "placeholders": [ + { + "placeholder": "person_name_possessive", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "first-name" + }, + { + "source": "answers", + "identifier": "last-name" + } + ] + }, + "transform": "concatenate_list" + }, + { + "arguments": { + "string_to_format": { + "source": "previous_transform" + } + }, + "transform": "format_possessive" + } + ] + } + ], + "text": "Random question about {person_name_possessive}" + }, + "description": ["Shows because the random question was completed in section 1"], + "type": "General" + }, + "skip_conditions": { + "when": { + "!=": [ + { + "source": "progress", + "selector": "section", + "identifier": "section-1" + }, + "COMPLETED" + ] + } + } + } + ] + } + ] + } + ] +} diff --git a/schemas/test/en/test_progress_value_source_blocks.json b/schemas/test/en/test_progress_value_source_blocks.json new file mode 100644 index 0000000000..3a10d2a382 --- /dev/null +++ b/schemas/test/en/test_progress_value_source_blocks.json @@ -0,0 +1,210 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "001", + "title": "Test progress value source", + "theme": "default", + "description": "A test survey for testing progres value source referencing blocks", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Linear", + "options": { + "summary": { + "collapsible": false + } + } + }, + "sections": [ + { + "id": "section-1", + "groups": [ + { + "blocks": [ + { + "type": "Question", + "id": "s1-b1", + "question": { + "id": "s1-b1-q1", + "title": "Section 1 Question 1", + "description": [ + "If you answer 0, then the second question will be skipped because of a routing rule, as well as the fourth question because of a skip condition referencing the progress of question 2, as well as the 6th question because of a routing rule referencing the progress of question 4.", + "So only questions 3, 5, 7 will be displayed.", + "Otherwise, questions 2, 4 and 6 can also display." + ], + "type": "General", + "answers": [ + { + "id": "s1-b1-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + }, + "routing_rules": [ + { + "when": { + "==": [ + { + "source": "answers", + "identifier": "s1-b1-q1-a1" + }, + 0 + ] + }, + "block": "s1-b3" + }, + { + "block": "s1-b2" + } + ] + }, + { + "type": "Question", + "id": "s1-b2", + "question": { + "id": "s1-b2-q1", + "title": "Section 1 Question 2", + "description": ["Showing this question because question 1 value is not 0"], + "type": "General", + "answers": [ + { + "id": "s1-b2-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + } + }, + { + "type": "Question", + "id": "s1-b3", + "question": { + "id": "s1-b3-q1", + "title": "Section 1 Question 3", + "type": "General", + "answers": [ + { + "id": "s1-b3-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + } + }, + { + "type": "Question", + "id": "s1-b4", + "question": { + "id": "s1-b4-q1", + "title": "Section 1 Question 4", + "type": "General", + "answers": [ + { + "id": "s1-b4-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + }, + "skip_conditions": { + "when": { + "!=": [{ "source": "progress", "selector": "block", "identifier": "s1-b2" }, "COMPLETED"] + } + } + }, + { + "type": "Question", + "id": "s1-b5", + "question": { + "id": "s1-b5-q1", + "title": "Section 1 Question 5", + "type": "General", + "answers": [ + { + "id": "s1-b5-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + }, + "routing_rules": [ + { + "when": { + "==": [ + { + "source": "progress", + "selector": "block", + "identifier": "s1-b4" + }, + "COMPLETED" + ] + }, + "block": "s1-b6" + }, + { + "block": "s1-b7" + } + ] + }, + { + "type": "Question", + "id": "s1-b6", + "question": { + "id": "s1-b6-q1", + "title": "Section 1 Question 6", + "description": ["Showing this question because question 4 was completed"], + "type": "General", + "answers": [ + { + "id": "s1-b6-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + } + }, + { + "type": "Question", + "id": "s1-b7", + "question": { + "id": "s1-b7-q1", + "title": "Section 1 Question 7", + "type": "General", + "answers": [ + { + "id": "s1-b7-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + } + } + ], + "id": "group-1" + } + ] + } + ] +} diff --git a/schemas/test/en/test_progress_value_source_blocks_cross_section.json b/schemas/test/en/test_progress_value_source_blocks_cross_section.json new file mode 100644 index 0000000000..d8cb40da44 --- /dev/null +++ b/schemas/test/en/test_progress_value_source_blocks_cross_section.json @@ -0,0 +1,224 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "001", + "title": "Test progress value source", + "theme": "default", + "description": "A test survey for testing progres value source referencing blocks", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Hub", + "options": {} + }, + "sections": [ + { + "id": "section-1", + "title": "Section One", + "summary": { + "show_on_completion": true + }, + "groups": [ + { + "blocks": [ + { + "type": "Question", + "id": "s1-b1", + "question": { + "id": "s1-b1-q1", + "title": "Section 1 Question 1", + "description": [ + "If you answer 0, then the second question will be skipped because of a routing rule, as well as the fourth question because of a skip condition referencing the progress of question 2, as well as the 6th question in the Second Section because of a routing rule referencing the progress of question 4.", + "So only question 3 in Section One, and questions 5 and 7 in Section Two will be displayed.", + "Otherwise, questions 2 and 4 in Section Ona and question 6 in Section Two can also display." + ], + "type": "General", + "answers": [ + { + "id": "s1-b1-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + }, + "routing_rules": [ + { + "when": { + "==": [ + { + "source": "answers", + "identifier": "s1-b1-q1-a1" + }, + 0 + ] + }, + "block": "s1-b3" + }, + { + "block": "s1-b2" + } + ] + }, + { + "type": "Question", + "id": "s1-b2", + "question": { + "id": "s1-b2-q1", + "title": "Section 1 Question 2", + "description": ["Showing this question because question 1 value is not 0"], + "type": "General", + "answers": [ + { + "id": "s1-b2-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + } + }, + { + "type": "Question", + "id": "s1-b3", + "question": { + "id": "s1-b3-q1", + "title": "Section 1 Question 3", + "type": "General", + "answers": [ + { + "id": "s1-b3-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + } + }, + { + "type": "Question", + "id": "s1-b4", + "question": { + "id": "s1-b4-q1", + "title": "Section 1 Question 4", + "type": "General", + "answers": [ + { + "id": "s1-b4-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + }, + "skip_conditions": { + "when": { + "!=": [{ "source": "progress", "selector": "block", "identifier": "s1-b2" }, "COMPLETED"] + } + } + } + ], + "id": "group-1" + } + ] + }, + { + "id": "section-2", + "title": "Section Two", + "summary": { + "show_on_completion": true + }, + "groups": [ + { + "blocks": [ + { + "type": "Question", + "id": "s2-b5", + "question": { + "id": "s2-b5-q1", + "title": "Section 2 Question 5", + "type": "General", + "answers": [ + { + "id": "s2-b5-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + }, + "routing_rules": [ + { + "when": { + "==": [ + { + "source": "progress", + "selector": "block", + "identifier": "s1-b4" + }, + "COMPLETED" + ] + }, + "block": "s2-b6" + }, + { + "block": "s2-b7" + } + ] + }, + { + "type": "Question", + "id": "s2-b6", + "question": { + "id": "s2-b6-q1", + "title": "Section 2 Question 6", + "description": ["Showing this question because question 4 in Section One was completed"], + "type": "General", + "answers": [ + { + "id": "s2-b6-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + } + }, + { + "type": "Question", + "id": "s2-b7", + "question": { + "id": "s2-b7-q1", + "title": "Section 2 Question 7", + "type": "General", + "answers": [ + { + "id": "s2-b7-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + } + } + ], + "id": "group-2" + } + ] + } + ] +} diff --git a/schemas/test/en/test_new_routing_and_skipping_section_dependencies.json b/schemas/test/en/test_progress_value_source_calculated_summary.json similarity index 53% rename from schemas/test/en/test_new_routing_and_skipping_section_dependencies.json rename to schemas/test/en/test_progress_value_source_calculated_summary.json index 95803cf823..be36064f2b 100644 --- a/schemas/test/en/test_new_routing_and_skipping_section_dependencies.json +++ b/schemas/test/en/test_progress_value_source_calculated_summary.json @@ -2,11 +2,14 @@ "mime_type": "application/json/ons/eq", "language": "en", "schema_version": "0.0.1", - "data_version": "0.0.3", - "survey_id": "001", - "title": "Routing and Skipping Section Dependencies", + "survey_id": "139", "theme": "default", - "description": "A questionnaire to test routing and skipping rules, when the rule references a different section to its current section", + "title": "Progress Value Source Repeating Sections Test", + "data_version": "0.0.3", + "description": "Progress Value Source Repeating Sections Test", + "navigation": { + "visible": true + }, "metadata": [ { "name": "user_id", @@ -19,6 +22,11 @@ { "name": "ru_name", "type": "string" + }, + { + "name": "trad_as", + "type": "string", + "optional": true } ], "questionnaire_flow": { @@ -27,271 +35,113 @@ }, "sections": [ { - "title": "Skip question", + "id": "section-1", + "title": "Calculated Summary", "summary": { "show_on_completion": true }, - "id": "skip-section", "groups": [ { + "id": "group-1", + "title": "Calculated Summary group", "blocks": [ { "type": "Question", - "id": "skip-age", + "id": "first-number-block", "question": { + "id": "first-number-question", + "title": "First Number Question Title", + "type": "General", "answers": [ { - "id": "skip-age-answer", - "mandatory": false, - "options": [ - { - "label": "Yes", - "value": "Yes" - }, - { - "label": "No", - "value": "No" - } - ], - "type": "Radio" + "id": "first-number-answer", + "label": "First answer label", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 } - ], - "id": "skip-age-question", - "title": "Do you want to skip all age questions in all sections", - "type": "General" + ] } - } - ], - "id": "skip-age-group" - } - ] - }, - { - "title": "Skip question confirmation", - "summary": { "show_on_completion": true }, - "id": "skip-confirmation-section", - "groups": [ - { - "blocks": [ - { - "type": "Question", - "id": "security", - "question": { - "answers": [ - { - "id": "security-answer", - "mandatory": false, - "options": [ - { - "label": "Yes", - "value": "Yes" - }, - { - "label": "No", - "value": "No" - } - ], - "type": "Radio" - } - ], - "id": "security-question", - "title": "You understand that your personal details will be held securely and not shared with anyone?", - "type": "General" - }, - "routing_rules": [ - { - "block": "skip-confirmation", - "when": { - "==": [ - "Yes", - { - "source": "answers", - "identifier": "skip-age-answer" - } - ] - } - }, - { - "section": "End" - } - ] }, { "type": "Question", - "id": "skip-confirmation", + "id": "second-number-block", "question": { + "id": "second-number-question", + "title": "Second Number Question Title", + "type": "General", "answers": [ { - "id": "skip-confirmation-answer", + "id": "second-number-answer", + "label": "Second answer label", "mandatory": true, - "options": [ - { - "label": "Yes", - "value": "Yes" - }, - { - "label": "No", - "value": "No" - } - ], - "type": "Radio" + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 } - ], - "id": "skip-confirmation-question", - "title": "Are you sure you want to skip all age questions in all sections?", - "type": "General" + ] + } + }, + { + "type": "CalculatedSummary", + "id": "calculated-summary-block", + "title": "We calculate the total of currency values entered to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "answers", + "identifier": "first-number-answer" + }, + { + "source": "answers", + "identifier": "second-number-answer" + } + ] + }, + "title": "Grand total of previous values" } } - ], - "id": "skip-confirmation-group" + ] } ] }, { - "title": "Primary Person", - "summary": { "show_on_completion": true }, - "id": "primary-person", + "id": "section-2", + "title": "Skippable random question + List collector", "groups": [ { + "id": "group", + "title": "List", "blocks": [ { "type": "Question", - "id": "name-block", - "question": { - "answers": [ - { - "label": "Full Name", - "id": "name-answer", - "mandatory": false, - "type": "TextField" - } - ], - "id": "name-question", - "title": "What is your name?", - "type": "General" - } - }, - { - "type": "Question", - "id": "age", + "id": "s2-b1", "question": { + "id": "s2-b1-q1", + "title": "Skippable random question", + "type": "General", "answers": [ { - "label": "Age in years", - "id": "age-answer", - "mandatory": false, + "id": "s2-b1-q1-a1", + "mandatory": true, + "label": "Enter any number", "type": "Number" } - ], - "id": "age-question", - "title": "What is your age?", - "type": "General" + ] }, "skip_conditions": { "when": { - "and": [ + "!=": [ { - "==": [ - { - "source": "answers", - "identifier": "skip-age-answer" - }, - "Yes" - ] + "source": "progress", + "selector": "block", + "identifier": "calculated-summary-block" }, - { - "!=": [ - { - "source": "answers", - "identifier": "skip-confirmation-answer" - }, - "No" - ] - } + "COMPLETED" ] } } - } - ], - "id": "primary-person-group", - "title": "Primary Person" - }, - { - "blocks": [ - { - "type": "Question", - "id": "reason-no-confirmation", - "question": { - "answers": [ - { - "id": "reason-no-confirmation-answer", - "mandatory": false, - "options": [ - { - "label": "I did not visit section 2, so confirmation was not needed", - "value": "I did not visit section 2, so confirmation was not needed" - }, - { - "label": "I did, but it was removed from the path as I changed my answer to No on the skip question", - "value": "I did, but it was removed from the path as I changed my answer to No on the skip question" - } - ], - "type": "Radio" - } - ], - "id": "reason-no-confirmation-question", - "title": "Why did you not answer the age skipping confirmation question?", - "type": "General" - } - } - ], - "id": "confirmation-group", - "title": "Confirmation Question", - "skip_conditions": { - "when": { - "or": [ - { - "==": [ - { - "source": "answers", - "identifier": "skip-confirmation-answer" - }, - "Yes" - ] - }, - { - "==": [ - { - "source": "answers", - "identifier": "skip-confirmation-answer" - }, - "No" - ] - } - ] - } - } - } - ] - }, - { - "id": "household-section", - "title": "Household Summary", - "summary": { - "show_on_completion": true, - "items": [ - { - "type": "List", - "for_list": "people", - "title": "Household members", - "add_link_text": "Add someone to this household", - "empty_list_text": "There are no householders" - } - ] - }, - "groups": [ - { - "id": "group", - "title": "List", - "blocks": [ + }, { "id": "list-collector", "type": "ListCollector", @@ -324,7 +174,6 @@ "add_block": { "id": "add-person", "type": "ListAddQuestion", - "cancel_text": "Don’t need to add anyone else?", "question": { "id": "add-question", "type": "General", @@ -348,7 +197,6 @@ "edit_block": { "id": "edit-person", "type": "ListEditQuestion", - "cancel_text": "Don’t need to change anything?", "question": { "id": "edit-question", "type": "General", @@ -372,12 +220,10 @@ "remove_block": { "id": "remove-person", "type": "ListRemoveQuestion", - "cancel_text": "Don’t need to remove this person?", "question": { "id": "remove-question", "type": "General", "title": "Are you sure you want to remove this person?", - "warning": "All of the information about this person will be deleted", "answers": [ { "id": "remove-confirmation", @@ -435,8 +281,8 @@ ] }, { - "id": "household-personal-details-section", - "title": "Personal Details", + "id": "section-3", + "title": "Repeating section", "summary": { "show_on_completion": true }, "repeat": { "for_list": "people", @@ -469,72 +315,196 @@ }, "groups": [ { - "id": "personal-details-group", - "title": "Personal Details", + "id": "dob-group", + "title": "Date of birth", "blocks": [ { - "id": "repeating-sex", + "type": "Question", + "id": "dob-block", "question": { "answers": [ { - "id": "repeating-sex-answer", + "id": "date-of-birth-answer", "mandatory": false, - "options": [ - { - "label": "Male", - "value": "Male" + "maximum": { + "value": "now" + }, + "minimum": { + "offset_by": { + "years": -115 }, - { - "label": "Female", - "value": "Female" - } - ], - "type": "Radio" + "value": "2019-10-13" + }, + "type": "Date" } ], - "id": "repeating-sex-question", - "title": "What sex is this person?", + "guidance": { + "contents": [ + { + "description": "For example 31 12 1970" + } + ] + }, + "id": "date-of-birth-question", + "title": { + "placeholders": [ + { + "placeholder": "person_name_possessive", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "first-name" + }, + { + "source": "answers", + "identifier": "last-name" + } + ] + }, + "transform": "concatenate_list" + }, + { + "arguments": { + "string_to_format": { + "source": "previous_transform" + } + }, + "transform": "format_possessive" + } + ] + } + ], + "text": "What is {person_name_possessive} date of birth?" + }, "type": "General" - }, - "type": "Question" + } }, { "type": "Question", - "id": "repeating-age", + "id": "other-question-block", "question": { + "id": "other-question", "answers": [ { - "label": "Age in years", - "id": "repeating-age-answer", - "mandatory": false, + "id": "other-answer", + "mandatory": true, + "label": "Anything", "type": "Number" } ], - "id": "repeating-age-question", - "title": "What age is this person?", + "title": { + "placeholders": [ + { + "placeholder": "person_name_possessive", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "first-name" + }, + { + "source": "answers", + "identifier": "last-name" + } + ] + }, + "transform": "concatenate_list" + }, + { + "arguments": { + "string_to_format": { + "source": "previous_transform" + } + }, + "transform": "format_possessive" + } + ] + } + ], + "text": "Random question about {person_name_possessive}" + }, + "description": ["Shows because the calculated summary was completed in section 1"], "type": "General" }, "skip_conditions": { "when": { - "and": [ + "!=": [ { - "==": [ - { - "source": "answers", - "identifier": "skip-age-answer" - }, - "Yes" - ] + "source": "progress", + "selector": "block", + "identifier": "calculated-summary-block" }, + "COMPLETED" + ] + } + } + }, + { + "type": "Question", + "id": "other-question-block-2", + "question": { + "id": "other-question-2", + "answers": [ + { + "id": "other-answer-2", + "mandatory": true, + "label": "Anything", + "type": "Number" + } + ], + "title": { + "placeholders": [ { - "!=": [ + "placeholder": "person_name_possessive", + "transforms": [ { - "source": "answers", - "identifier": "skip-confirmation-answer" + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "first-name" + }, + { + "source": "answers", + "identifier": "last-name" + } + ] + }, + "transform": "concatenate_list" }, - "No" + { + "arguments": { + "string_to_format": { + "source": "previous_transform" + } + }, + "transform": "format_possessive" + } ] } + ], + "text": "Another random question about {person_name_possessive}" + }, + "description": ["Shows because block 2 of this repeating section was completed."], + "type": "General" + }, + "skip_conditions": { + "when": { + "!=": [ + { + "source": "progress", + "selector": "block", + "identifier": "other-question-block" + }, + "COMPLETED" ] } } diff --git a/schemas/test/en/test_progress_value_source_calculated_summary_extended.json b/schemas/test/en/test_progress_value_source_calculated_summary_extended.json new file mode 100644 index 0000000000..caac2ba203 --- /dev/null +++ b/schemas/test/en/test_progress_value_source_calculated_summary_extended.json @@ -0,0 +1,1129 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "survey_id": "139", + "theme": "default", + "title": "Progress Value Source Caluclated Summary (Extended)", + "data_version": "0.0.3", + "description": "An extended version of the Progress Value Source Calculated Summary schema intended to test chained dependency evaluation for progress value sources where multiple sections have progress value source dependencies on one another", + "navigation": { + "visible": true + }, + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + }, + { + "name": "trad_as", + "type": "string", + "optional": true + } + ], + "questionnaire_flow": { + "type": "Hub", + "options": { + "required_completed_sections": ["introduction-section"] + } + }, + "sections": [ + { + "id": "introduction-section", + "title": "Guidance", + "show_on_hub": false, + "groups": [ + { + "blocks": [ + { + "id": "interstitial", + "content": { + "title": "Guidance for completing this test schema", + "contents": [ + { + "description": "This schema was created in order to ensure that dependencies based on a progress value sources captured in order." + }, + { + "description": "It is also being used to test progress value sources with chained dependents. For example, In this schema, Sections 7, 8, 9 and 10 are all dependent on Section 2, and Sections 11 and 12 are dependent on Section 9 and 10." + }, + { + "description": "So we can use this schema to test journeys and ensure that all dependent sections are updated. For example if we had not started Section 2 yet, but Sections 8, 9 and 10 are all Complete, and sections 11 and 12 are Partially Completed. Given the dependencies in this schema, completing Section 2 would mean that the status of Sections 8, 9 and 10 would change to Partially Complete and Sections 11 and 12 to Complete." + } + ] + }, + "type": "Interstitial" + } + ], + "id": "introduction-group", + "title": "Test Schema Guidance" + } + ] + }, + { + "id": "section-1", + "title": "Calculated Summary", + "groups": [ + { + "id": "group-1", + "title": "Calculated Summary group", + "blocks": [ + { + "type": "Question", + "id": "first-number-block", + "question": { + "id": "first-number-question", + "title": "First Number Question Title", + "type": "General", + "answers": [ + { + "id": "first-number-answer", + "label": "First answer label", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "Question", + "id": "second-number-block", + "question": { + "id": "second-number-question", + "title": "Second Number Question Title", + "type": "General", + "answers": [ + { + "id": "second-number-answer", + "label": "Second answer label", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "calculated-summary-block", + "title": "We calculate the total of currency values entered to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "answers", + "identifier": "first-number-answer" + }, + { + "source": "answers", + "identifier": "second-number-answer" + } + ] + }, + "title": "Grand total of previous values" + } + } + ] + } + ] + }, + { + "id": "section-2", + "title": "Skippable random question + List collector", + "groups": [ + { + "id": "group", + "title": "List", + "blocks": [ + { + "type": "Question", + "id": "s2-b1", + "question": { + "id": "s2-b1-q1", + "title": "Skippable random question", + "type": "General", + "answers": [ + { + "id": "s2-b1-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + }, + "skip_conditions": { + "when": { + "!=": [ + { + "source": "progress", + "selector": "block", + "identifier": "calculated-summary-block" + }, + "COMPLETED" + ] + } + } + }, + { + "id": "list-collector", + "type": "ListCollector", + "for_list": "people", + "question": { + "id": "confirmation-question", + "type": "General", + "title": "Does anyone else live here?", + "answers": [ + { + "id": "anyone-else", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "add_block": { + "id": "add-person", + "type": "ListAddQuestion", + "question": { + "id": "add-question", + "type": "General", + "title": "What is the name of the person?", + "answers": [ + { + "id": "first-name", + "label": "First name", + "mandatory": true, + "type": "TextField" + }, + { + "id": "last-name", + "label": "Last name", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "edit_block": { + "id": "edit-person", + "type": "ListEditQuestion", + "question": { + "id": "edit-question", + "type": "General", + "title": "What is the name of the person?", + "answers": [ + { + "id": "first-name", + "label": "First name", + "mandatory": true, + "type": "TextField" + }, + { + "id": "last-name", + "label": "Last name", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "remove_block": { + "id": "remove-person", + "type": "ListRemoveQuestion", + "question": { + "id": "remove-question", + "type": "General", + "title": "Are you sure you want to remove this person?", + "answers": [ + { + "id": "remove-confirmation", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RemoveListItemAndAnswers" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + "summary": { + "title": "Household members", + "item_title": { + "text": "{person_name}", + "placeholders": [ + { + "placeholder": "person_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "first-name" + }, + { + "source": "answers", + "identifier": "last-name" + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + } + } + } + ] + } + ] + }, + { + "id": "section-3", + "title": "Repeating section", + "summary": { "show_on_completion": true }, + "repeat": { + "for_list": "people", + "title": { + "text": "{person_name}", + "placeholders": [ + { + "placeholder": "person_name", + "transforms": [ + { + "transform": "concatenate_list", + "arguments": { + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "first-name" + }, + { + "source": "answers", + "identifier": "last-name" + } + ], + "delimiter": " " + } + } + ] + } + ] + } + }, + "groups": [ + { + "id": "dob-group", + "title": "Date of birth", + "blocks": [ + { + "type": "Question", + "id": "dob-block", + "question": { + "answers": [ + { + "id": "date-of-birth-answer", + "mandatory": false, + "maximum": { + "value": "now" + }, + "minimum": { + "offset_by": { + "years": -115 + }, + "value": "2019-10-13" + }, + "type": "Date" + } + ], + "guidance": { + "contents": [ + { + "description": "For example 31 12 1970" + } + ] + }, + "id": "date-of-birth-question", + "title": { + "placeholders": [ + { + "placeholder": "person_name_possessive", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "first-name" + }, + { + "source": "answers", + "identifier": "last-name" + } + ] + }, + "transform": "concatenate_list" + }, + { + "arguments": { + "string_to_format": { + "source": "previous_transform" + } + }, + "transform": "format_possessive" + } + ] + } + ], + "text": "What is {person_name_possessive} date of birth?" + }, + "type": "General" + } + }, + { + "type": "Question", + "id": "other-question-block", + "question": { + "id": "other-question", + "answers": [ + { + "id": "other-answer", + "mandatory": true, + "label": "Anything", + "type": "Number" + } + ], + "title": { + "placeholders": [ + { + "placeholder": "person_name_possessive", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "first-name" + }, + { + "source": "answers", + "identifier": "last-name" + } + ] + }, + "transform": "concatenate_list" + }, + { + "arguments": { + "string_to_format": { + "source": "previous_transform" + } + }, + "transform": "format_possessive" + } + ] + } + ], + "text": "Random question about {person_name_possessive}" + }, + "description": ["Shows because the calculated summary was completed in section 1"], + "type": "General" + }, + "skip_conditions": { + "when": { + "!=": [ + { + "source": "progress", + "selector": "block", + "identifier": "calculated-summary-block" + }, + "COMPLETED" + ] + } + } + }, + { + "type": "Question", + "id": "other-question-block-2", + "question": { + "id": "other-question-2", + "answers": [ + { + "id": "other-answer-2", + "mandatory": true, + "label": "Anything", + "type": "Number" + } + ], + "title": { + "placeholders": [ + { + "placeholder": "person_name_possessive", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "first-name" + }, + { + "source": "answers", + "identifier": "last-name" + } + ] + }, + "transform": "concatenate_list" + }, + { + "arguments": { + "string_to_format": { + "source": "previous_transform" + } + }, + "transform": "format_possessive" + } + ] + } + ], + "text": "Another random question about {person_name_possessive}" + }, + "description": ["Shows because block 2 of this repeating section was completed."], + "type": "General" + }, + "skip_conditions": { + "when": { + "!=": [ + { + "source": "progress", + "selector": "block", + "identifier": "other-question-block" + }, + "COMPLETED" + ] + } + } + } + ] + } + ] + }, + { + "id": "section-4", + "title": "Section 4 (Dependent on Section 1)", + "groups": [ + { + "id": "group-4", + "title": "List", + "blocks": [ + { + "type": "Question", + "id": "s4-b1", + "question": { + "id": "s4-b1-q1", + "title": "Skippable random question", + "type": "General", + "answers": [ + { + "id": "s4-b1-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + }, + "skip_conditions": { + "when": { + "!=": [ + { + "source": "progress", + "selector": "section", + "identifier": "section-1" + }, + "COMPLETED" + ] + } + } + }, + { + "type": "Question", + "id": "s4-b2", + "question": { + "id": "s4-b2-q1", + "title": "Random question", + "type": "General", + "answers": [ + { + "id": "s4-b2-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + } + } + ] + } + ] + }, + { + "id": "section-5", + "title": "Section 5 (Dependent on Calc Summary Block Section 1)", + "groups": [ + { + "id": "group-5", + "title": "List", + "blocks": [ + { + "type": "Question", + "id": "s5-b1", + "question": { + "id": "s5-b1-q1", + "title": "Skippable random question", + "type": "General", + "answers": [ + { + "id": "s5-b1-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + }, + "skip_conditions": { + "when": { + "!=": [ + { + "source": "progress", + "selector": "block", + "identifier": "calculated-summary-block" + }, + "COMPLETED" + ] + } + } + }, + { + "type": "Question", + "id": "s5-b2", + "question": { + "id": "s5-b2-q1", + "title": "Random question", + "type": "General", + "answers": [ + { + "id": "s5-b2-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + } + } + ] + } + ] + }, + { + "id": "section-6", + "title": "Section 6 (Dependent on Section 4)", + "groups": [ + { + "id": "group-6", + "title": "List", + "blocks": [ + { + "type": "Question", + "id": "s6-b1", + "question": { + "id": "s6-b1-q1", + "title": "Skippable random question", + "type": "General", + "answers": [ + { + "id": "s6-b1-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + }, + "skip_conditions": { + "when": { + "!=": [ + { + "source": "progress", + "selector": "section", + "identifier": "section-4" + }, + "COMPLETED" + ] + } + } + }, + { + "type": "Question", + "id": "s6-b2", + "question": { + "id": "s6-b2-q1", + "title": "Random question", + "type": "General", + "answers": [ + { + "id": "s6-b2-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + } + } + ] + } + ] + }, + { + "id": "section-7", + "title": "Section 7 (Dependent on Section 5)", + "groups": [ + { + "id": "group-7", + "title": "List", + "blocks": [ + { + "type": "Question", + "id": "s7-b1", + "question": { + "id": "s7-b1-q1", + "title": "Skippable random question", + "type": "General", + "answers": [ + { + "id": "s7-b1-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + }, + "skip_conditions": { + "when": { + "!=": [ + { + "source": "progress", + "selector": "section", + "identifier": "section-5" + }, + "COMPLETED" + ] + } + } + }, + { + "type": "Question", + "id": "s7-b2", + "question": { + "id": "s7-b2-q1", + "title": "Skippable random question", + "type": "General", + "answers": [ + { + "id": "s7-b2-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + }, + "skip_conditions": { + "when": { + "!=": [ + { + "source": "progress", + "selector": "section", + "identifier": "section-2" + }, + "COMPLETED" + ] + } + } + }, + { + "type": "Question", + "id": "s7-b3", + "question": { + "id": "s7-b3-q1", + "title": "Random question", + "type": "General", + "answers": [ + { + "id": "s7-b3-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + } + } + ] + } + ] + }, + { + "id": "section-8", + "title": "Section 8 (Dependent on Section 7 and Section 2)", + "groups": [ + { + "id": "group-8", + "title": "List", + "blocks": [ + { + "type": "Question", + "id": "s8-b1", + "question": { + "id": "s8-b1-q1", + "title": "Skippable random question", + "type": "General", + "answers": [ + { + "id": "s8-b1-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + }, + "skip_conditions": { + "when": { + "!=": [ + { + "source": "progress", + "selector": "section", + "identifier": "section-7" + }, + "COMPLETED" + ] + } + } + }, + { + "type": "Question", + "id": "s8-b2", + "question": { + "id": "s8-b2-q1", + "title": "Skippable random question", + "type": "General", + "answers": [ + { + "id": "s8-b2-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + }, + "skip_conditions": { + "when": { + "!=": [ + { + "source": "progress", + "selector": "section", + "identifier": "section-2" + }, + "COMPLETED" + ] + } + } + }, + { + "type": "Question", + "id": "s8-b3", + "question": { + "id": "s8-b3-q1", + "title": "Random question", + "type": "General", + "answers": [ + { + "id": "s8-b3-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + } + } + ] + } + ] + }, + { + "id": "section-9", + "title": "Section 9 (Dependent on Section 2)", + "groups": [ + { + "id": "group-9", + "title": "List", + "blocks": [ + { + "type": "Question", + "id": "s9-b1", + "question": { + "id": "s9-b1-q1", + "title": "Skippable random question", + "type": "General", + "answers": [ + { + "id": "s9-b1-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + }, + "skip_conditions": { + "when": { + "!=": [ + { + "source": "progress", + "selector": "section", + "identifier": "section-2" + }, + "COMPLETED" + ] + } + } + }, + { + "type": "Question", + "id": "s9-b2", + "question": { + "id": "s9-b2-q1", + "title": "Random question", + "type": "General", + "answers": [ + { + "id": "s9-b2-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + } + } + ] + } + ] + }, + { + "id": "section-10", + "title": "Section 10 (Dependent on Section 2)", + "groups": [ + { + "id": "group-10", + "title": "List", + "blocks": [ + { + "type": "Question", + "id": "s10-b1", + "question": { + "id": "s10-b1-q1", + "title": "Skippable random question", + "type": "General", + "answers": [ + { + "id": "s10-b1-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + }, + "skip_conditions": { + "when": { + "!=": [ + { + "source": "progress", + "selector": "section", + "identifier": "section-2" + }, + "COMPLETED" + ] + } + } + }, + { + "type": "Question", + "id": "s10-b2", + "question": { + "id": "s10-b2-q1", + "title": "Random question", + "type": "General", + "answers": [ + { + "id": "s10-b2-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + } + } + ] + } + ] + }, + { + "id": "section-11", + "title": "Section 11 (Dependent on Section 10)", + "groups": [ + { + "id": "group-11", + "title": "List", + "blocks": [ + { + "type": "Question", + "id": "s11-b1", + "question": { + "id": "s11-b1-q1", + "title": "Skippable random question", + "type": "General", + "answers": [ + { + "id": "s11-b1-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + }, + "skip_conditions": { + "when": { + "!=": [ + { + "source": "progress", + "selector": "section", + "identifier": "section-10" + }, + "COMPLETED" + ] + } + } + }, + { + "type": "Question", + "id": "s11-b2", + "question": { + "id": "s11-b2-q1", + "title": "Random question", + "type": "General", + "answers": [ + { + "id": "s11-b2-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + } + } + ] + } + ] + }, + { + "id": "section-12", + "title": "Section 12 (Dependent on Section 9)", + "groups": [ + { + "id": "group-12", + "title": "List", + "blocks": [ + { + "type": "Question", + "id": "s12-b1", + "question": { + "id": "s12-b1-q1", + "title": "Skippable random question", + "type": "General", + "answers": [ + { + "id": "s12-b1-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + }, + "skip_conditions": { + "when": { + "!=": [ + { + "source": "progress", + "selector": "section", + "identifier": "section-9" + }, + "COMPLETED" + ] + } + } + }, + { + "type": "Question", + "id": "s12-b2", + "question": { + "id": "s12-b2-q1", + "title": "Random question", + "type": "General", + "answers": [ + { + "id": "s12-b2-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + } + } + ] + } + ] + } + ] +} diff --git a/schemas/test/en/test_progress_value_source_repeating_sections_chained_dependencies.json b/schemas/test/en/test_progress_value_source_repeating_sections_chained_dependencies.json new file mode 100644 index 0000000000..390821f170 --- /dev/null +++ b/schemas/test/en/test_progress_value_source_repeating_sections_chained_dependencies.json @@ -0,0 +1,490 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "survey_id": "139", + "theme": "default", + "title": "Progress Value Source Repeating Sections With Chained Dependencies Test", + "data_version": "0.0.3", + "description": "Progress Value Source Repeating Sections Test", + "navigation": { + "visible": true + }, + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + }, + { + "name": "trad_as", + "type": "string", + "optional": true + } + ], + "questionnaire_flow": { + "type": "Hub", + "options": {} + }, + "sections": [ + { + "id": "section-1", + "title": "Section 1", + "groups": [ + { + "id": "group-1", + "title": "List", + "blocks": [ + { + "type": "Question", + "id": "s1-b2", + "question": { + "id": "s1-b1-q1", + "title": "Random question", + "type": "General", + "answers": [ + { + "id": "s1-b1-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + } + } + ] + } + ] + }, + { + "id": "section-2", + "title": "Section 2 (Dependent on Section 1)", + "groups": [ + { + "id": "group-2", + "title": "List", + "blocks": [ + { + "type": "Question", + "id": "s2-b1", + "question": { + "id": "s2-b1-q1", + "title": "Skippable random question", + "type": "General", + "answers": [ + { + "id": "s2-b1-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + }, + "skip_conditions": { + "when": { + "!=": [ + { + "source": "progress", + "selector": "section", + "identifier": "section-1" + }, + "COMPLETED" + ] + } + } + }, + { + "type": "Question", + "id": "s2-b2", + "question": { + "id": "s2-b2-q1", + "title": "Random question", + "type": "General", + "answers": [ + { + "id": "s2-b2-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + } + } + ] + } + ] + }, + { + "id": "section-3", + "title": "Section 3 (Depends on Section 2)", + "groups": [ + { + "id": "group", + "title": "Second List Collector", + "blocks": [ + { + "id": "second-list-collector", + "type": "ListCollector", + "for_list": "second-people", + "question": { + "id": "second-confirmation-question", + "type": "General", + "title": "Does anyone else live here?", + "answers": [ + { + "id": "second-anyone-else", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "add_block": { + "id": "second-add-person", + "type": "ListAddQuestion", + "question": { + "id": "second-add-question", + "type": "General", + "title": "What is the name of the person?", + "answers": [ + { + "id": "second-first-name", + "label": "First name", + "mandatory": true, + "type": "TextField" + }, + { + "id": "second-last-name", + "label": "Last name", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "edit_block": { + "id": "second-edit-person", + "type": "ListEditQuestion", + "question": { + "id": "second-edit-question", + "type": "General", + "title": "What is the name of the person?", + "answers": [ + { + "id": "second-first-name", + "label": "First name", + "mandatory": true, + "type": "TextField" + }, + { + "id": "second-last-name", + "label": "Last name", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "remove_block": { + "id": "second-remove-person", + "type": "ListRemoveQuestion", + "question": { + "id": "second-remove-question", + "type": "General", + "title": "Are you sure you want to remove this person?", + "answers": [ + { + "id": "second-remove-confirmation", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RemoveListItemAndAnswers" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + "summary": { + "title": "Household members", + "item_title": { + "text": "{person_name}", + "placeholders": [ + { + "placeholder": "person_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "second-first-name" + }, + { + "source": "answers", + "identifier": "second-last-name" + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + } + } + }, + { + "type": "Question", + "id": "second-question-block", + "question": { + "id": "second-question", + "title": "Question", + "type": "General", + "answers": [ + { + "id": "second-answer", + "mandatory": false, + "label": "Enter any number", + "type": "Number" + } + ] + }, + "skip_conditions": { + "when": { + "!=": [ + { + "source": "progress", + "selector": "section", + "identifier": "section-2" + }, + "COMPLETED" + ] + } + } + }, + { + "type": "Question", + "id": "second-random-question-enabler-block", + "question": { + "id": "second-random-question-enabler-question", + "title": "Random question enabler", + "description": [ + "Answering this question will enable the random question in the repeated section coming after the list collector." + ], + "type": "General", + "answers": [ + { + "id": "second-random-question-enabler-answer", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + } + } + ] + } + ] + }, + { + "id": "section-4", + "title": "Section 4 - Repeat (Depends on section 3)", + "summary": { "show_on_completion": true }, + "repeat": { + "for_list": "second-people", + "title": { + "text": "{person_name}", + "placeholders": [ + { + "placeholder": "person_name", + "transforms": [ + { + "transform": "concatenate_list", + "arguments": { + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "second-first-name" + }, + { + "source": "answers", + "identifier": "second-last-name" + } + ], + "delimiter": " " + } + } + ] + } + ] + } + }, + "groups": [ + { + "id": "second-dob-group", + "title": "Date of birth", + "blocks": [ + { + "type": "Question", + "id": "second-dob-block", + "question": { + "answers": [ + { + "id": "second-date-of-birth-answer", + "mandatory": false, + "maximum": { + "value": "now" + }, + "minimum": { + "offset_by": { + "years": -115 + }, + "value": "2019-10-13" + }, + "type": "Date" + } + ], + "guidance": { + "contents": [ + { + "description": "For example 31 12 1970" + } + ] + }, + "id": "second-date-of-birth-question", + "title": { + "placeholders": [ + { + "placeholder": "person_name_possessive", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "second-first-name" + }, + { + "source": "answers", + "identifier": "second-last-name" + } + ] + }, + "transform": "concatenate_list" + }, + { + "arguments": { + "string_to_format": { + "source": "previous_transform" + } + }, + "transform": "format_possessive" + } + ] + } + ], + "text": "What is {person_name_possessive} date of birth?" + }, + "type": "General" + } + }, + { + "type": "Question", + "id": "second-other-question-block", + "question": { + "id": "second-other-question", + "answers": [ + { + "id": "second-other-answer", + "mandatory": true, + "label": "Anything", + "type": "Number" + } + ], + "title": { + "placeholders": [ + { + "placeholder": "person_name_possessive", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "second-first-name" + }, + { + "source": "answers", + "identifier": "second-last-name" + } + ] + }, + "transform": "concatenate_list" + }, + { + "arguments": { + "string_to_format": { + "source": "previous_transform" + } + }, + "transform": "format_possessive" + } + ] + } + ], + "text": "Random question about {person_name_possessive}" + }, + "description": ["Shows because section 2 was completed"], + "type": "General" + }, + "skip_conditions": { + "when": { + "!=": [ + { + "source": "progress", + "selector": "section", + "identifier": "section-2" + }, + "COMPLETED" + ] + } + } + } + ] + } + ] + } + ] +} diff --git a/schemas/test/en/test_progress_value_source_section_enabled_hub.json b/schemas/test/en/test_progress_value_source_section_enabled_hub.json new file mode 100644 index 0000000000..e3babd5cb2 --- /dev/null +++ b/schemas/test/en/test_progress_value_source_section_enabled_hub.json @@ -0,0 +1,110 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "001", + "title": "Test progress value source", + "theme": "default", + "description": "A test survey for testing progress value source section enabled in a hub flow", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Hub", + "options": {} + }, + "sections": [ + { + "id": "section-1", + "title": "Section 1", + "groups": [ + { + "blocks": [ + { + "type": "Question", + "id": "s1-b1", + "question": { + "id": "s1-b1-q1", + "title": "Section 1 Question 1", + "description": ["Always shows"], + "type": "General", + "answers": [ + { + "id": "s1-b1-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + } + }, + { + "type": "Question", + "id": "s1-b2", + "question": { + "id": "s1-b2-q1", + "title": "Section 1 Question 2", + "type": "General", + "answers": [ + { + "id": "s1-b2-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + } + } + ], + "id": "group-1" + } + ] + }, + { + "id": "section-2", + "title": "Section 2", + "enabled": { + "when": { + "==": [{ "source": "progress", "selector": "section", "identifier": "section-1" }, "COMPLETED"] + } + }, + "groups": [ + { + "blocks": [ + { + "type": "Question", + "id": "s2-b1", + "question": { + "id": "s2-b1-q1", + "title": "Section 2 Question 1", + "description": ["This question always shows"], + "type": "General", + "answers": [ + { + "id": "s2-b1-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + } + } + ], + "id": "group-2" + } + ] + } + ] +} diff --git a/schemas/test/en/test_progress_value_source_section_enabled_hub_complex.json b/schemas/test/en/test_progress_value_source_section_enabled_hub_complex.json new file mode 100644 index 0000000000..4d647dbdb6 --- /dev/null +++ b/schemas/test/en/test_progress_value_source_section_enabled_hub_complex.json @@ -0,0 +1,251 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "001", + "title": "Test progress value source", + "theme": "default", + "description": "A test survey for testing progress value source section enabled in a hub flow, with a mixture of skip conditions and section enabled conditions, and a mix of block and section references", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Hub", + "options": {} + }, + "sections": [ + { + "id": "section-1", + "title": "Section 1", + "groups": [ + { + "blocks": [ + { + "type": "Question", + "id": "s1-b1", + "question": { + "id": "s1-b1-q1", + "title": "Section 1 Question 1", + "description": ["Always shows. The next question in the section also shows when the answer is not 0"], + "type": "General", + "answers": [ + { + "id": "s1-b1-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + }, + "routing_rules": [ + { + "when": { + "!=": [ + { + "identifier": "s1-b1-q1-a1", + "source": "answers" + }, + 0 + ] + }, + "block": "s1-b2" + }, + { + "section": "End" + } + ] + }, + { + "type": "Question", + "id": "s1-b2", + "question": { + "id": "s1-b2-q1", + "title": "Section 1 Question 2", + "type": "General", + "description": ["Shows if the answer to the previous question is not 0"], + "answers": [ + { + "id": "s1-b2-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + } + } + ], + "id": "group-1" + } + ] + }, + { + "id": "section-2", + "title": "Section 2", + "enabled": { + "when": { + "==": [{ "source": "progress", "selector": "section", "identifier": "section-1" }, "COMPLETED"] + } + }, + "groups": [ + { + "blocks": [ + { + "type": "Question", + "id": "s2-b1", + "question": { + "id": "s2-b1-q1", + "title": "Section 2 Question 1", + "description": ["This question always shows. The next question in the section also shows when the answer is not 0"], + "type": "General", + "answers": [ + { + "id": "s2-b1-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + } + }, + { + "type": "Question", + "id": "s2-b2", + "question": { + "id": "s2-b2-q1", + "title": "Section 2 Question 2", + "type": "General", + "description": ["Always shows"], + "answers": [ + { + "id": "s2-b2-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + }, + "routing_rules": [ + { + "when": { + "!=": [ + { + "identifier": "s2-b1-q1-a1", + "source": "answers" + }, + 0 + ] + }, + "block": "s2-b3" + }, + { + "section": "End" + } + ] + }, + { + "type": "Question", + "id": "s2-b3", + "question": { + "id": "s2-b3-q1", + "title": "Section 2 Question 3", + "type": "General", + "description": ["Shows if the answer to the Section 2 Question 1 is not 0"], + "answers": [ + { + "id": "s2-b3-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + } + } + ], + "id": "group-2" + } + ] + }, + { + "id": "section-3", + "title": "Section 3", + "groups": [ + { + "blocks": [ + { + "type": "Question", + "id": "s3-b1", + "question": { + "id": "s3-b1-q1", + "title": "Section 3 Question 1", + "description": ["Always shows"], + "type": "General", + "answers": [ + { + "id": "s3-b1-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + } + } + ], + "id": "group-3" + } + ] + }, + { + "id": "section-4", + "title": "Section 4", + "enabled": { + "when": { + "and": [ + { + "==": [{ "source": "progress", "selector": "block", "identifier": "s2-b2" }, "COMPLETED"] + }, + { + "==": [{ "source": "progress", "selector": "section", "identifier": "section-2" }, "COMPLETED"] + } + ] + } + }, + "groups": [ + { + "blocks": [ + { + "type": "Question", + "id": "s4-b1", + "question": { + "id": "s4-b1-q1", + "title": "Section 4 Question 1", + "description": ["This section shows if section 2 block 2 is completed, as well as section 2"], + "type": "General", + "answers": [ + { + "id": "s4-b1-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + } + } + ], + "id": "group-4" + } + ] + } + ] +} diff --git a/schemas/test/en/test_progress_value_source_section_enabled_no_hub.json b/schemas/test/en/test_progress_value_source_section_enabled_no_hub.json new file mode 100644 index 0000000000..a9f88839d2 --- /dev/null +++ b/schemas/test/en/test_progress_value_source_section_enabled_no_hub.json @@ -0,0 +1,114 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "001", + "title": "Test progress value source", + "theme": "default", + "description": "A test survey for testing progress value source section enabled in a linear flow", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Linear", + "options": { + "summary": { + "collapsible": false + } + } + }, + "sections": [ + { + "id": "section-1", + "title": "Section 1", + "groups": [ + { + "blocks": [ + { + "type": "Question", + "id": "s1-b1", + "question": { + "id": "s1-b1-q1", + "title": "Section 1 Question 1", + "description": ["Always shows"], + "type": "General", + "answers": [ + { + "id": "s1-b1-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + } + }, + { + "type": "Question", + "id": "s1-b2", + "question": { + "id": "s1-b2-q1", + "title": "Section 1 Question 2", + "type": "General", + "answers": [ + { + "id": "s1-b2-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + } + } + ], + "id": "group-1" + } + ] + }, + { + "id": "section-2", + "title": "Section 2", + "enabled": { + "when": { + "==": [{ "source": "progress", "selector": "section", "identifier": "section-1" }, "COMPLETED"] + } + }, + "groups": [ + { + "blocks": [ + { + "type": "Question", + "id": "s2-b1", + "question": { + "id": "s2-b1-q1", + "title": "Section 2 Question 1", + "description": ["This question always shows"], + "type": "General", + "answers": [ + { + "id": "s2-b1-q1-a1", + "mandatory": true, + "label": "Enter any number", + "type": "Number" + } + ] + } + } + ], + "id": "group-2" + } + ] + } + ] +} diff --git a/schemas/test/en/test_question_definition.json b/schemas/test/en/test_question_definition.json index edd1576229..9233ebff3d 100644 --- a/schemas/test/en/test_question_definition.json +++ b/schemas/test/en/test_question_definition.json @@ -41,33 +41,16 @@ "id": "definition-block", "question": { "id": "question", - "title": "Do you connect a LiFePO4 battery to your photovoltaic system to store surplus energy?", + "title": "Do you connect a LiFePO4 battery to your photovoltaic system to store surplus energy?", "type": "General", - "definitions": [ - { - "title": "What is a photovoltaic system?", - "contents": [ - { - "description": "A typical photovoltaic system employs solar panels, each comprising a number of solar cells, which generate electrical power. PV installations may be ground-mounted, rooftop mounted or wall mounted. The mount may be fixed, or use a solar tracker to follow the sun across the sky." - } - ] - }, - { - "title": "Why use LiFePO4 batteries?", - "contents": [ - { - "title": "3 Benefits of LifePO4 batteries." - }, - { - "list": [ - "LifePO4 batteries have a life span 10 times longer than that of traditional lead acid batteries. This dramatically reduces the need for battery changes.", - "Lithium iron phosphate batteries operate with much lower resistance and consequently recharge at a faster rate.", - "LifeP04 lightweight batteries are lighter than lead acid batteries, usually weighing about 1/4 less." - ] - } - ] - } - ], + "definition": { + "title": "What is a photovoltaic system?", + "contents": [ + { + "description": "A typical photovoltaic system employs solar panels, each comprising a number of solar cells, which generate electrical power. PV installations may be ground-mounted, rooftop mounted or wall mounted. The mount may be fixed, or use a solar tracker to follow the sun across the sky." + } + ] + }, "answers": [ { "type": "Radio", diff --git a/schemas/test/en/test_question_definition_array_type.json b/schemas/test/en/test_question_definition_array_type.json new file mode 100644 index 0000000000..f691d55993 --- /dev/null +++ b/schemas/test/en/test_question_definition_array_type.json @@ -0,0 +1,80 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Other input fields", + "theme": "default", + "description": "A questionnaire to test definitions (array type).", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Linear", + "options": { + "summary": { + "collapsible": false + } + } + }, + "sections": [ + { + "id": "default-section", + "groups": [ + { + "id": "definition-group", + "blocks": [ + { + "type": "Question", + "id": "definition-block", + "question": { + "id": "question", + "title": "Do you connect a LiFePO4 battery to your photovoltaic system to store surplus energy?", + "type": "General", + "definitions": [ + { + "title": "What is a photovoltaic system?", + "contents": [ + { + "description": "A typical photovoltaic system employs solar panels, each comprising a number of solar cells, which generate electrical power. PV installations may be ground-mounted, rooftop mounted or wall mounted. The mount may be fixed, or use a solar tracker to follow the sun across the sky." + } + ] + } + ], + "answers": [ + { + "type": "Radio", + "id": "radio-mandatory-answer", + "mandatory": false, + "options": [ + { + "label": "Yes, I do connect a battery", + "value": "Yes, I do connect a battery" + }, + { + "label": "No, I don’t connect a battery", + "value": "No, I don’t connect a battery" + } + ] + } + ] + } + } + ] + } + ] + } + ] +} diff --git a/schemas/test/en/test_question_description.json b/schemas/test/en/test_question_description.json index b37a74cc4f..4904ec7083 100644 --- a/schemas/test/en/test_question_description.json +++ b/schemas/test/en/test_question_description.json @@ -51,7 +51,6 @@ "label": "What is your name?", "max_length": 20, "mandatory": false, - "q_code": "0", "type": "TextField" } ], diff --git a/schemas/test/en/test_question_guidance.json b/schemas/test/en/test_question_guidance.json index b136959e00..5cc4695019 100644 --- a/schemas/test/en/test_question_guidance.json +++ b/schemas/test/en/test_question_guidance.json @@ -69,7 +69,6 @@ "id": "answer-test-guidance-title", "label": "Text question", "mandatory": false, - "q_code": "0", "type": "TextField" } ] @@ -101,7 +100,6 @@ "id": "answer-test-guidance-description", "label": "Text question", "mandatory": false, - "q_code": "0", "type": "TextField" } ] @@ -135,7 +133,6 @@ "id": "answer-test-guidance-lists", "label": "Text question", "mandatory": false, - "q_code": "0", "type": "TextField" } ] @@ -169,7 +166,6 @@ "id": "answer-test-guidance-content-description", "label": "Text question", "mandatory": false, - "q_code": "0", "type": "TextField" } ] @@ -203,7 +199,6 @@ "id": "answer-test-guidance-content-title", "label": "Text question", "mandatory": false, - "q_code": "0", "type": "TextField" } ] @@ -237,7 +232,6 @@ "id": "answer-test-guidance-content-list", "label": "Text question", "mandatory": false, - "q_code": "0", "type": "TextField" } ] @@ -303,7 +297,6 @@ "id": "answer-test-guidance-all", "label": "Text question", "mandatory": false, - "q_code": "0", "type": "TextField" } ] diff --git a/schemas/test/en/test_radio_checkbox_descriptions.json b/schemas/test/en/test_radio_checkbox_descriptions.json index f0a30ca1bd..9e7011aad6 100644 --- a/schemas/test/en/test_radio_checkbox_descriptions.json +++ b/schemas/test/en/test_radio_checkbox_descriptions.json @@ -66,7 +66,6 @@ "value": "Implementation of changes to marketing concepts or strategies" } ], - "q_code": "10", "type": "Checkbox", "validation": { "messages": {} @@ -108,7 +107,6 @@ "value": "Implementation of changes to marketing concepts or strategies" } ], - "q_code": "20", "type": "Radio", "validation": { "messages": {} diff --git a/schemas/test/en/test_relationships.json b/schemas/test/en/test_relationships.json index 9ceaf00f22..9054e842a7 100644 --- a/schemas/test/en/test_relationships.json +++ b/schemas/test/en/test_relationships.json @@ -193,7 +193,7 @@ "id": "relationship-question", "type": "General", "title": { - "text": "Thinking of {first_person_name}, {second_person_name} is their â€Ļ", + "text": "Thinking of {first_person_name}, {second_person_name} is their â€Ļ", "placeholders": [ { "placeholder": "first_person_name", @@ -261,7 +261,7 @@ "mandatory": true, "type": "Relationship", "playback": { - "text": "{second_person_name} is {first_person_name_possessive} â€Ļ", + "text": "{second_person_name} is {first_person_name_possessive} â€Ļ", "placeholders": [ { "placeholder": "first_person_name_possessive", @@ -336,7 +336,7 @@ "label": "Husband or Wife", "value": "Husband or Wife", "title": { - "text": "Thinking of {first_person_name}, {second_person_name} is their husband or wife", + "text": "Thinking of {first_person_name}, {second_person_name} is their husband or wife", "placeholders": [ { "placeholder": "first_person_name", @@ -399,7 +399,7 @@ ] }, "playback": { - "text": "{second_person_name} is {first_person_name_possessive} husband or wife", + "text": "{second_person_name} is {first_person_name_possessive} husband or wife", "placeholders": [ { "placeholder": "first_person_name_possessive", @@ -474,7 +474,7 @@ "label": "Legally registered civil partner", "value": "Legally registered civil partner", "title": { - "text": "Thinking of {first_person_name}, {second_person_name} is their legally registered civil partner", + "text": "Thinking of {first_person_name}, {second_person_name} is their legally registered civil partner", "placeholders": [ { "placeholder": "first_person_name", @@ -537,7 +537,7 @@ ] }, "playback": { - "text": "{second_person_name} is {first_person_name_possessive} legally registered civil partner", + "text": "{second_person_name} is {first_person_name_possessive} legally registered civil partner", "placeholders": [ { "placeholder": "first_person_name_possessive", @@ -612,7 +612,7 @@ "label": "Son or daughter", "value": "Son or daughter", "title": { - "text": "Thinking of {first_person_name}, {second_person_name} is their son or daughter", + "text": "Thinking of {first_person_name}, {second_person_name} is their son or daughter", "placeholders": [ { "placeholder": "first_person_name", @@ -675,7 +675,7 @@ ] }, "playback": { - "text": "{second_person_name} is {first_person_name_possessive} son or daughter", + "text": "{second_person_name} is {first_person_name_possessive} son or daughter", "placeholders": [ { "placeholder": "first_person_name_possessive", @@ -750,7 +750,7 @@ "label": "Brother or sister", "value": "Brother or sister", "title": { - "text": "Thinking of {first_person_name}, {second_person_name} is their brother or sister", + "text": "Thinking of {first_person_name}, {second_person_name} is their brother or sister", "placeholders": [ { "placeholder": "first_person_name", @@ -813,7 +813,7 @@ ] }, "playback": { - "text": "{second_person_name} is {first_person_name_possessive} brother or sister", + "text": "{second_person_name} is {first_person_name_possessive} brother or sister", "placeholders": [ { "placeholder": "first_person_name_possessive", @@ -889,17 +889,18 @@ } ] }, - "skip_conditions": [ - { - "when": [ + "skip_conditions": { + "when": { + "<": [ { - "list": "people", - "condition": "less than", - "value": 2 - } + "source": "list", + "identifier": "people", + "selector": "count" + }, + 2 ] } - ] + } }, { "id": "relationship-interstitial", @@ -912,17 +913,18 @@ ] }, "type": "Interstitial", - "skip_conditions": [ - { - "when": [ + "skip_conditions": { + "when": { + "<": [ { - "list": "people", - "condition": "less than", - "value": 2 - } + "source": "list", + "identifier": "people", + "selector": "count" + }, + 2 ] } - ] + } } ] } diff --git a/schemas/test/en/test_relationships_primary.json b/schemas/test/en/test_relationships_primary.json index 912e0873d3..2fe0d5149f 100644 --- a/schemas/test/en/test_relationships_primary.json +++ b/schemas/test/en/test_relationships_primary.json @@ -249,7 +249,7 @@ "id": "relationship-question", "type": "General", "title": { - "text": "{second_person_name} is your â€Ļ", + "text": "{second_person_name} is your â€Ļ", "placeholders": [ { "placeholder": "second_person_name", @@ -288,7 +288,7 @@ "mandatory": true, "type": "Relationship", "playback": { - "text": "{second_person_name} is your â€Ļ", + "text": "{second_person_name} is your â€Ļ", "placeholders": [ { "placeholder": "second_person_name", @@ -326,7 +326,7 @@ "label": "Husband or Wife", "value": "Husband or Wife", "title": { - "text": "{second_person_name} is your husband or wife", + "text": "{second_person_name} is your husband or wife", "placeholders": [ { "placeholder": "second_person_name", @@ -360,7 +360,7 @@ ] }, "playback": { - "text": "{second_person_name} is your husband or wife", + "text": "{second_person_name} is your husband or wife", "placeholders": [ { "placeholder": "second_person_name", @@ -398,7 +398,7 @@ "label": "Legally registered civil partner", "value": "Legally registered civil partner", "title": { - "text": "{second_person_name} is your legally registered civil partner", + "text": "{second_person_name} is your legally registered civil partner", "placeholders": [ { "placeholder": "second_person_name", @@ -432,7 +432,7 @@ ] }, "playback": { - "text": "{second_person_name} is your legally registered civil partner", + "text": "{second_person_name} is your legally registered civil partner", "placeholders": [ { "placeholder": "second_person_name", @@ -470,7 +470,7 @@ "label": "Son or daughter", "value": "Son or daughter", "title": { - "text": "{second_person_name} is your son or daughter", + "text": "{second_person_name} is your son or daughter", "placeholders": [ { "placeholder": "second_person_name", @@ -504,7 +504,7 @@ ] }, "playback": { - "text": "{second_person_name} is your son or daughter", + "text": "{second_person_name} is your son or daughter", "placeholders": [ { "placeholder": "second_person_name", @@ -542,7 +542,7 @@ "label": "Brother or sister", "value": "Brother or sister", "title": { - "text": "{second_person_name} is your brother or sister", + "text": "{second_person_name} is your brother or sister", "placeholders": [ { "placeholder": "second_person_name", @@ -576,7 +576,7 @@ ] }, "playback": { - "text": "{second_person_name} is your brother or sister", + "text": "{second_person_name} is your brother or sister", "placeholders": [ { "placeholder": "second_person_name", @@ -614,24 +614,26 @@ } ] }, - "when": [ - { - "list": "people", - "id_selector": "primary_person", - "condition": "equals", - "comparison": { + "when": { + "==": [ + { + "source": "list", + "identifier": "people", + "selector": "primary_person" + }, + { "source": "location", - "id": "list_item_id" + "identifier": "list_item_id" } - } - ] + ] + } }, { "question": { "id": "relationship-question", "type": "General", "title": { - "text": "Thinking of {first_person_name}, {second_person_name} is their â€Ļ", + "text": "Thinking of {first_person_name}, {second_person_name} is their â€Ļ", "placeholders": [ { "placeholder": "first_person_name", @@ -699,7 +701,7 @@ "mandatory": true, "type": "Relationship", "playback": { - "text": "{second_person_name} is {first_person_name_possessive} â€Ļ", + "text": "{second_person_name} is {first_person_name_possessive} â€Ļ", "placeholders": [ { "placeholder": "first_person_name_possessive", @@ -774,7 +776,7 @@ "label": "Husband or Wife", "value": "Husband or Wife", "title": { - "text": "Thinking of {first_person_name}, {second_person_name} is their husband or wife", + "text": "Thinking of {first_person_name}, {second_person_name} is their husband or wife", "placeholders": [ { "placeholder": "first_person_name", @@ -837,7 +839,7 @@ ] }, "playback": { - "text": "{second_person_name} is {first_person_name_possessive} husband or wife", + "text": "{second_person_name} is {first_person_name_possessive} husband or wife", "placeholders": [ { "placeholder": "first_person_name_possessive", @@ -912,7 +914,7 @@ "label": "Legally registered civil partner", "value": "Legally registered civil partner", "title": { - "text": "Thinking of {first_person_name}, {second_person_name} is their legally registered civil partner", + "text": "Thinking of {first_person_name}, {second_person_name} is their legally registered civil partner", "placeholders": [ { "placeholder": "first_person_name", @@ -975,7 +977,7 @@ ] }, "playback": { - "text": "{second_person_name} is {first_person_name_possessive} legally registered civil partner", + "text": "{second_person_name} is {first_person_name_possessive} legally registered civil partner", "placeholders": [ { "placeholder": "first_person_name_possessive", @@ -1050,7 +1052,7 @@ "label": "Son or daughter", "value": "Son or daughter", "title": { - "text": "Thinking of {first_person_name}, {second_person_name} is their son or daughter", + "text": "Thinking of {first_person_name}, {second_person_name} is their son or daughter", "placeholders": [ { "placeholder": "first_person_name", @@ -1113,7 +1115,7 @@ ] }, "playback": { - "text": "{second_person_name} is {first_person_name_possessive} son or daughter", + "text": "{second_person_name} is {first_person_name_possessive} son or daughter", "placeholders": [ { "placeholder": "first_person_name_possessive", @@ -1188,7 +1190,7 @@ "label": "Brother or sister", "value": "Brother or sister", "title": { - "text": "Thinking of {first_person_name}, {second_person_name} is their brother or sister", + "text": "Thinking of {first_person_name}, {second_person_name} is their brother or sister", "placeholders": [ { "placeholder": "first_person_name", @@ -1251,7 +1253,7 @@ ] }, "playback": { - "text": "{second_person_name} is {first_person_name_possessive} brother or sister", + "text": "{second_person_name} is {first_person_name_possessive} brother or sister", "placeholders": [ { "placeholder": "first_person_name_possessive", @@ -1326,30 +1328,33 @@ } ] }, - "when": [ - { - "list": "people", - "id_selector": "primary_person", - "condition": "not equals", - "comparison": { + "when": { + "!=": [ + { + "identifier": "people", + "source": "list", + "selector": "primary_person" + }, + { "source": "location", - "id": "list_item_id" + "identifier": "list_item_id" } - } - ] + ] + } } ], - "skip_conditions": [ - { - "when": [ + "skip_conditions": { + "when": { + "<": [ { - "list": "people", - "condition": "less than", - "value": 2 - } + "identifier": "people", + "source": "list", + "selector": "count" + }, + 2 ] } - ] + } } ] } diff --git a/schemas/test/en/test_relationships_unrelated.json b/schemas/test/en/test_relationships_unrelated.json index 9855319290..ea03d61774 100644 --- a/schemas/test/en/test_relationships_unrelated.json +++ b/schemas/test/en/test_relationships_unrelated.json @@ -193,7 +193,7 @@ "id": "relationship-question", "type": "General", "title": { - "text": "Thinking about {first_person_name}, {second_person_name} is their â€Ļ", + "text": "Thinking about {first_person_name}, {second_person_name} is their â€Ļ", "placeholders": [ { "placeholder": "first_person_name", @@ -261,7 +261,7 @@ "mandatory": true, "type": "Relationship", "playback": { - "text": "{second_person_name} is {first_person_name_possessive} â€Ļ", + "text": "{second_person_name} is {first_person_name_possessive} â€Ļ", "placeholders": [ { "placeholder": "first_person_name_possessive", @@ -336,7 +336,7 @@ "label": "Husband or Wife", "value": "Husband or Wife", "title": { - "text": "Thinking about {first_person_name}, {second_person_name} is their husband or wife", + "text": "Thinking about {first_person_name}, {second_person_name} is their husband or wife", "placeholders": [ { "placeholder": "first_person_name", @@ -399,7 +399,7 @@ ] }, "playback": { - "text": "{second_person_name} is {first_person_name_possessive} husband or wife", + "text": "{second_person_name} is {first_person_name_possessive} husband or wife", "placeholders": [ { "placeholder": "first_person_name_possessive", @@ -474,7 +474,7 @@ "label": "Unrelated", "value": "Unrelated", "title": { - "text": "Thinking about {first_person_name}, {second_person_name} is unrelated to {first_person_name}", + "text": "Thinking about {first_person_name}, {second_person_name} is unrelated to {first_person_name}", "placeholders": [ { "placeholder": "first_person_name", @@ -537,7 +537,7 @@ ] }, "playback": { - "text": "{second_person_name} is unrelated to {first_person_name}", + "text": "{second_person_name} is unrelated to {first_person_name}", "placeholders": [ { "placeholder": "first_person_name", @@ -605,17 +605,18 @@ } ] }, - "skip_conditions": [ - { - "when": [ + "skip_conditions": { + "when": { + "<": [ { - "list": "people", - "condition": "less than", - "value": 2 - } + "identifier": "people", + "source": "list", + "selector": "count" + }, + 2 ] } - ], + }, "unrelated_block": { "type": "UnrelatedQuestion", "id": "related-to-anyone-else", @@ -689,17 +690,19 @@ } ] }, - "when": [ - { - "comparison": { - "id": "list_item_id", + "when": { + "==": [ + { + "identifier": "list_item_id", "source": "location" }, - "condition": "equals", - "id_selector": "first", - "list": "people" - } - ] + { + "source": "list", + "identifier": "people", + "selector": "first" + } + ] + } }, { "question": { @@ -737,7 +740,7 @@ ] } ], - "text": "Are any of these people related to {person_name}?" + "text": "Are any of these people related to {person_name}?" }, "guidance": { "contents": [ @@ -803,17 +806,19 @@ } ] }, - "when": [ - { - "comparison": { - "id": "list_item_id", + "when": { + "!=": [ + { + "identifier": "list_item_id", "source": "location" }, - "condition": "not equals", - "id_selector": "first", - "list": "people" - } - ] + { + "source": "list", + "identifier": "people", + "selector": "first" + } + ] + } } ] } @@ -829,17 +834,18 @@ ] }, "type": "Interstitial", - "skip_conditions": [ - { - "when": [ + "skip_conditions": { + "when": { + "<": [ { - "list": "people", - "condition": "less than", - "value": 2 - } + "source": "list", + "identifier": "people", + "selector": "count" + }, + 2 ] } - ] + } } ] } diff --git a/schemas/test/en/test_repeating_section_summaries.json b/schemas/test/en/test_repeating_section_summaries.json index 9176af0cdf..3ba64c74a9 100644 --- a/schemas/test/en/test_repeating_section_summaries.json +++ b/schemas/test/en/test_repeating_section_summaries.json @@ -228,7 +228,9 @@ { "id": "personal-details-section", "title": "Personal Details", - "summary": { "show_on_completion": true }, + "summary": { + "show_on_completion": true + }, "repeat": { "for_list": "people", "title": { @@ -322,13 +324,15 @@ "title": "What is your date of birth?", "type": "General" }, - "when": [ - { - "condition": "equals", - "id": "proxy-answer", - "value": "No, I’m answering for myself" - } - ] + "when": { + "==": [ + { + "source": "answers", + "identifier": "proxy-answer" + }, + "No, I’m answering for myself" + ] + } }, { "question": { @@ -382,17 +386,19 @@ ] } ], - "text": "What is {person_name_possessive} date of birth?" + "text": "What is {person_name_possessive} date of birth?" }, "type": "General" }, - "when": [ - { - "condition": "equals", - "id": "proxy-answer", - "value": "Yes" - } - ] + "when": { + "==": [ + { + "source": "answers", + "identifier": "proxy-answer" + }, + "Yes" + ] + } } ], "type": "Question" diff --git a/schemas/test/en/test_repeating_sections_with_hub_and_spoke.json b/schemas/test/en/test_repeating_sections_with_hub_and_spoke.json index c8fb6343f0..3d3e74cb38 100644 --- a/schemas/test/en/test_repeating_sections_with_hub_and_spoke.json +++ b/schemas/test/en/test_repeating_sections_with_hub_and_spoke.json @@ -436,13 +436,13 @@ "title": "What is the name of the person?", "answers": [ { - "id": "first-name", + "id": "visitor-first-name", "label": "First name", "mandatory": true, "type": "TextField" }, { - "id": "last-name", + "id": "visitor-last-name", "label": "Last name", "mandatory": true, "type": "TextField" @@ -459,13 +459,13 @@ "title": "What is the name of the person?", "answers": [ { - "id": "first-name", + "id": "visitor-first-name", "label": "First name", "mandatory": true, "type": "TextField" }, { - "id": "last-name", + "id": "visitor-last-name", "label": "Last name", "mandatory": true, "type": "TextField" @@ -516,11 +516,11 @@ "list_to_concatenate": [ { "source": "answers", - "identifier": "first-name" + "identifier": "visitor-first-name" }, { "source": "answers", - "identifier": "last-name" + "identifier": "visitor-last-name" } ] }, @@ -635,13 +635,15 @@ "title": "What is your date of birth?", "type": "General" }, - "when": [ - { - "condition": "equals", - "id": "proxy-answer", - "value": "No, I’m answering for myself" - } - ] + "when": { + "==": [ + { + "source": "answers", + "identifier": "proxy-answer" + }, + "No, I’m answering for myself" + ] + } }, { "question": { @@ -701,17 +703,19 @@ ] } ], - "text": "What is {person_name_possessive} date of birth?" + "text": "What is {person_name_possessive} date of birth?" }, "type": "General" }, - "when": [ - { - "condition": "equals", - "id": "proxy-answer", - "value": "Yes" - } - ] + "when": { + "==": [ + { + "source": "answers", + "identifier": "proxy-answer" + }, + "Yes" + ] + } } ], "type": "Question" @@ -784,13 +788,15 @@ }, "type": "General" }, - "when": [ - { - "condition": "equals", - "id": "proxy-answer", - "value": "No, I’m answering for myself" - } - ] + "when": { + "==": [ + { + "source": "answers", + "identifier": "proxy-answer" + }, + "No, I’m answering for myself" + ] + } }, { "question": { @@ -899,44 +905,44 @@ }, "type": "General" }, - "when": [ - { - "condition": "equals", - "id": "proxy-answer", - "value": "Yes" - } - ] + "when": { + "==": [ + { + "source": "answers", + "identifier": "proxy-answer" + }, + "Yes" + ] + } } ], "routing_rules": [ { - "goto": { - "block": "date-of-birth", - "when": [ + "block": "date-of-birth", + "when": { + "==": [ { - "condition": "equals", - "id": "confirm-date-of-birth-answer", - "value": "No, I need to change my date of birth" - } + "source": "answers", + "identifier": "confirm-date-of-birth-answer" + }, + "No, I need to change my date of birth" ] } }, { - "goto": { - "block": "date-of-birth", - "when": [ + "block": "date-of-birth", + "when": { + "==": [ { - "condition": "equals", - "id": "confirm-date-of-birth-answer", - "value": "No, I need to change their date of birth" - } + "source": "answers", + "identifier": "confirm-date-of-birth-answer" + }, + "No, I need to change their date of birth" ] } }, { - "goto": { - "block": "sex" - } + "block": "sex" } ], "type": "ConfirmationQuestion" @@ -974,23 +980,34 @@ "title": "What is your sex?", "type": "General" }, - "when": [ - { - "condition": "equals", - "id": "proxy-answer", - "value": "No, I’m answering for myself" - }, - { - "condition": "less than or equal to", - "date_comparison": { - "offset_by": { - "years": -16 - }, - "value": "now" + "when": { + "and": [ + { + "==": [ + { + "source": "answers", + "identifier": "proxy-answer" + }, + "No, I’m answering for myself" + ] }, - "id": "date-of-birth-answer" - } - ] + { + "<=": [ + { + "date": ["now", { "years": -16 }] + }, + { + "date": [ + { + "source": "answers", + "identifier": "date-of-birth-answer" + } + ] + } + ] + } + ] + } }, { "question": { @@ -1051,27 +1068,33 @@ ] } ], - "text": "What is {person_name_possessive} sex?" + "text": "What is {person_name_possessive} sex?" }, "type": "General" }, - "when": [ - { - "condition": "equals", - "id": "proxy-answer", - "value": "Yes" - }, - { - "condition": "less than or equal to", - "date_comparison": { - "offset_by": { - "years": -16 - }, - "value": "now" + "when": { + "and": [ + { + "==": [ + { + "source": "answers", + "identifier": "proxy-answer" + }, + "Yes" + ] }, - "id": "date-of-birth-answer" - } - ] + { + "<=": [ + { + "date": ["now", { "years": -16 }] + }, + { + "date": [{ "source": "answers", "identifier": "date-of-birth-answer" }] + } + ] + } + ] + } }, { "question": { @@ -1096,13 +1119,15 @@ "title": "What is your sex?", "type": "General" }, - "when": [ - { - "condition": "equals", - "id": "proxy-answer", - "value": "No, I’m answering for myself" - } - ] + "when": { + "==": [ + { + "source": "answers", + "identifier": "proxy-answer" + }, + "No, I’m answering for myself" + ] + } }, { "question": { @@ -1156,17 +1181,19 @@ ] } ], - "text": "What is {person_name_possessive} sex?" + "text": "What is {person_name_possessive} sex?" }, "type": "General" }, - "when": [ - { - "condition": "equals", - "id": "proxy-answer", - "value": "Yes" - } - ] + "when": { + "==": [ + { + "source": "answers", + "identifier": "proxy-answer" + }, + "Yes" + ] + } } ], "type": "Question" @@ -1192,11 +1219,11 @@ "list_to_concatenate": [ { "source": "answers", - "identifier": "first-name" + "identifier": "visitor-first-name" }, { "source": "answers", - "identifier": "last-name" + "identifier": "visitor-last-name" } ], "delimiter": " " @@ -1251,11 +1278,11 @@ "list_to_concatenate": [ { "source": "answers", - "identifier": "first-name" + "identifier": "visitor-first-name" }, { "source": "answers", - "identifier": "last-name" + "identifier": "visitor-last-name" } ] }, @@ -1272,7 +1299,7 @@ ] } ], - "text": "What is {person_name_possessive} date of birth?" + "text": "What is {person_name_possessive} date of birth?" }, "type": "General" } diff --git a/schemas/test/en/test_new_routing_and.json b/schemas/test/en/test_routing_and.json similarity index 96% rename from schemas/test/en/test_new_routing_and.json rename to schemas/test/en/test_routing_and.json index 6d27ecc22c..89f0ea82b3 100644 --- a/schemas/test/en/test_new_routing_and.json +++ b/schemas/test/en/test_routing_and.json @@ -114,7 +114,7 @@ "contents": [ { "description": { - "text": "You were asked to enter 123 and 321 but you actually entered {answer_1} and {answer_2}.", + "text": "You were asked to enter 123 and 321 but you actually entered {answer_1} and {answer_2}.", "placeholders": [ { "placeholder": "answer_1", @@ -149,7 +149,7 @@ "contents": [ { "description": { - "text": "You were asked to enter 123 and 321 and you entered {answer_1} and {answer_2}.", + "text": "You were asked to enter 123 and 321 and you entered {answer_1} and {answer_2}.", "placeholders": [ { "placeholder": "answer_1", diff --git a/schemas/test/en/test_routing_and_skipping_section_dependencies.json b/schemas/test/en/test_routing_and_skipping_section_dependencies.json index 9e6331159d..0987ace23f 100644 --- a/schemas/test/en/test_routing_and_skipping_section_dependencies.json +++ b/schemas/test/en/test_routing_and_skipping_section_dependencies.json @@ -58,6 +58,71 @@ "title": "Do you want to skip all age questions in all sections", "type": "General" } + }, + { + "type": "Question", + "id": "skip-household-section", + "question": { + "id": "skip-household-section-question", + "title": "Do you want to skip the question about skipping the household summary section?", + "type": "General", + "answers": [ + { + "id": "skip-household-section-answer", + "label": "It will remove the enable section question from the routing path", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ], + "type": "Radio" + } + ] + } + }, + { + "type": "Question", + "id": "enable-section", + "question": { + "id": "enable-section-question", + "title": "Do you want to enable the household summary section?", + "type": "General", + "answers": [ + { + "id": "enable-section-answer", + "label": "Depending on the answer it will enable or disable the household summary section", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ], + "type": "Radio" + } + ] + }, + "skip_conditions": { + "when": { + "==": [ + { + "identifier": "skip-household-section-answer", + "source": "answers" + }, + "Yes" + ] + } + } } ], "id": "skip-age-group" @@ -98,21 +163,19 @@ }, "routing_rules": [ { - "goto": { - "block": "skip-confirmation", - "when": [ + "block": "skip-confirmation", + "when": { + "==": [ + "Yes", { - "id": "skip-age-answer", - "condition": "equals", - "value": "Yes" + "source": "answers", + "identifier": "skip-age-answer" } ] } }, { - "goto": { - "section": "End" - } + "section": "End" } ] }, @@ -187,22 +250,30 @@ "title": "What is your age?", "type": "General" }, - "skip_conditions": [ - { - "when": [ + "skip_conditions": { + "when": { + "and": [ { - "id": "skip-age-answer", - "condition": "equals", - "value": "Yes" + "==": [ + { + "source": "answers", + "identifier": "skip-age-answer" + }, + "Yes" + ] }, { - "id": "skip-confirmation-answer", - "condition": "not equals", - "value": "No" + "!=": [ + { + "source": "answers", + "identifier": "skip-confirmation-answer" + }, + "No" + ] } ] } - ] + } } ], "id": "primary-person-group", @@ -239,30 +310,45 @@ ], "id": "confirmation-group", "title": "Confirmation Question", - "skip_conditions": [ - { - "when": [ + "skip_conditions": { + "when": { + "or": [ { - "id": "skip-confirmation-answer", - "condition": "equals", - "value": "Yes" - } - ] - }, - { - "when": [ + "==": [ + { + "source": "answers", + "identifier": "skip-confirmation-answer" + }, + "Yes" + ] + }, { - "id": "skip-confirmation-answer", - "condition": "equals", - "value": "No" + "==": [ + { + "source": "answers", + "identifier": "skip-confirmation-answer" + }, + "No" + ] } ] } - ] + } } ] }, { + "enabled": { + "when": { + "==": [ + "Yes", + { + "source": "answers", + "identifier": "enable-section-answer" + } + ] + } + }, "id": "household-section", "title": "Household Summary", "summary": { @@ -467,7 +553,7 @@ "question": { "answers": [ { - "id": "repeating-answer", + "id": "repeating-sex-answer", "mandatory": false, "options": [ { @@ -504,22 +590,114 @@ "title": "What age is this person?", "type": "General" }, - "skip_conditions": [ - { - "when": [ + "skip_conditions": { + "when": { + "and": [ { - "id": "skip-age-answer", - "condition": "equals", - "value": "Yes" + "==": [ + { + "source": "answers", + "identifier": "skip-age-answer" + }, + "Yes" + ] }, { - "id": "skip-confirmation-answer", - "condition": "not equals", - "value": "No" + "!=": [ + { + "source": "answers", + "identifier": "skip-confirmation-answer" + }, + "No" + ] } ] } - ] + } + }, + { + "id": "repeating-is-dependent", + "question": { + "answers": [ + { + "id": "repeating-is-dependent-answer", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + }, + { + "label": "Prefer not to say", + "value": "Prefer not to say" + } + ], + "type": "Radio" + } + ], + "id": "repeating-is-dependent-question", + "title": "Is this person dependent on you?", + "type": "General" + }, + "type": "Question" + }, + { + "id": "repeating-is-smoker", + "question": { + "answers": [ + { + "id": "repeating-is-smoker-answer", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + }, + { + "label": "Prefer not to say", + "value": "Prefer not to say" + } + ], + "type": "Radio" + } + ], + "id": "repeating-is-smoker-question", + "title": "Is this person smoke or use nicotine products?", + "type": "General" + }, + "type": "Question", + "skip_conditions": { + "when": { + "or": [ + { + "==": [ + { + "source": "answers", + "identifier": "skip-age-answer" + }, + "Yes" + ] + }, + { + "<=": [ + { + "source": "answers", + "identifier": "repeating-age-answer" + }, + 18 + ] + } + ] + } + } } ] } diff --git a/schemas/test/en/test_routing_and_skipping_section_dependencies_calculated_summary.json b/schemas/test/en/test_routing_and_skipping_section_dependencies_calculated_summary.json new file mode 100644 index 0000000000..adf631db12 --- /dev/null +++ b/schemas/test/en/test_routing_and_skipping_section_dependencies_calculated_summary.json @@ -0,0 +1,338 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "001", + "title": "Routing and Skipping Section Dependencies based on Calculated Summary", + "theme": "default", + "description": "A questionnaire to test routing and skipping rules, when the rule references a different section to its current section", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Hub", + "options": {} + }, + "sections": [ + { + "title": "Calculated Summary Section", + "summary": { "show_on_completion": true }, + "id": "calculated-summary-section", + "groups": [ + { + "blocks": [ + { + "type": "Question", + "id": "first-question-block", + "question": { + "id": "first-question", + "title": "How much do you spend on the following items?", + "description": [ + "If the total is equal to ÂŖ100 a new section will appear on the hub and if it is greater than or equal to ÂŖ100 a dependent question will appear in the dependent question section" + ], + "type": "General", + "answers": [ + { + "id": "milk-answer", + "label": "Milk", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + }, + { + "id": "eggs-answer", + "label": "Eggs", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + }, + { + "id": "bread-answer", + "label": "Bread", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + }, + { + "id": "cheese-answer", + "label": "Cheese", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "Question", + "id": "skip-butter-block", + "question": { + "type": "General", + "id": "skip-butter-block-question", + "title": "Skip optional question about butter so that it doesn’t appear in the Total?", + "answers": [ + { + "type": "Radio", + "id": "skip-butter-block-answer", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + { + "skip_conditions": { + "when": { + "==": [ + { + "identifier": "skip-butter-block-answer", + "source": "answers" + }, + "Yes" + ] + } + }, + "type": "Question", + "id": "butter-block", + "question": { + "id": "butter-question", + "title": "How much do you spend on butter?", + "type": "General", + "answers": [ + { + "id": "butter-answer", + "label": "Butter", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "currency-total-playback", + "title": "We calculate the total of currency values entered to be %(total)s. Is this correct?", + "calculation": { + "calculation_type": "sum", + "answers_to_calculate": ["milk-answer", "eggs-answer", "bread-answer", "cheese-answer", "butter-answer"], + "title": "Grand total of previous values" + } + } + ], + "id": "calculated-summary-group" + } + ] + }, + { + "title": "Dependent question Section", + "summary": { "show_on_completion": true }, + "id": "dependent-question-section", + "groups": [ + { + "blocks": [ + { + "skip_conditions": { + "when": { + ">=": [ + { + "source": "calculated_summary", + "identifier": "currency-total-playback" + }, + 10 + ] + } + }, + "type": "Question", + "id": "fruit", + "question": { + "answers": [ + { + "id": "fruit-answer", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ], + "type": "Radio" + } + ], + "id": "fruit-question", + "title": "Do you like eating fruit", + "type": "General" + } + }, + { + "routing_rules": [ + { + "block": "second-question-block", + "when": { + ">=": [ + { + "source": "calculated_summary", + "identifier": "currency-total-playback" + }, + 100 + ] + } + }, + { + "section": "End" + } + ], + "type": "Question", + "id": "vegetables", + "question": { + "answers": [ + { + "id": "vegetables-answer", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ], + "type": "Radio" + } + ], + "id": "vegetables-question", + "title": "Do you like eating vegetables", + "type": "General" + } + }, + { + "type": "Question", + "id": "second-question-block", + "question": { + "id": "second-question", + "title": "How much do you spend on the following items?", + "type": "General", + "answers": [ + { + "id": "apples-answer", + "label": "Apples", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + }, + { + "id": "bananas-answer", + "label": "Bananas", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + }, + { + "id": "oranges-answer", + "label": "Oranges", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + }, + { + "id": "lemons-answer", + "label": "Lemons", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + } + ], + "id": "dependent-question-group" + } + ] + }, + { + "enabled": { + "when": { + "==": [ + 100, + { + "source": "calculated_summary", + "identifier": "currency-total-playback" + } + ] + } + }, + "title": "Dependent Enabled Section", + "summary": { "show_on_completion": true }, + "id": "dependent-enabled-section", + "groups": [ + { + "blocks": [ + { + "type": "Question", + "id": "desserts", + "question": { + "answers": [ + { + "id": "desserts-answer", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ], + "type": "Radio" + } + ], + "id": "desserts-question", + "title": "Do you like eating desserts", + "type": "General" + } + } + ], + "id": "dependent-enabled-section-group" + } + ] + } + ] +} diff --git a/schemas/test/en/test_routing_and_skipping_section_dependencies_new_calculated_summary.json b/schemas/test/en/test_routing_and_skipping_section_dependencies_new_calculated_summary.json new file mode 100644 index 0000000000..4de0f7aeb3 --- /dev/null +++ b/schemas/test/en/test_routing_and_skipping_section_dependencies_new_calculated_summary.json @@ -0,0 +1,360 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "001", + "title": "Routing and Skipping Section Dependencies based on Calculated Summary", + "theme": "default", + "description": "A questionnaire to test routing and skipping rules, when the rule references a different section to its current section", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Hub", + "options": {} + }, + "sections": [ + { + "title": "Calculated Summary Section", + "summary": { "show_on_completion": true }, + "id": "calculated-summary-section", + "groups": [ + { + "blocks": [ + { + "type": "Question", + "id": "first-question-block", + "question": { + "id": "first-question", + "title": "How much do you spend on the following items?", + "description": [ + "If the total is equal to ÂŖ100 a new section will appear on the hub and if it is less than or equal to ÂŖ10 a dependent question will appear in the dependent question section" + ], + "type": "General", + "answers": [ + { + "id": "milk-answer", + "label": "Milk", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + }, + { + "id": "eggs-answer", + "label": "Eggs", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + }, + { + "id": "bread-answer", + "label": "Bread", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + }, + { + "id": "cheese-answer", + "label": "Cheese", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "Question", + "id": "skip-butter-block", + "question": { + "type": "General", + "id": "skip-butter-block-question", + "title": "Skip optional question about butter so that it doesn’t appear in the Total?", + "answers": [ + { + "type": "Radio", + "id": "skip-butter-block-answer", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + { + "skip_conditions": { + "when": { + "==": [ + { + "identifier": "skip-butter-block-answer", + "source": "answers" + }, + "Yes" + ] + } + }, + "type": "Question", + "id": "butter-block", + "question": { + "id": "butter-question", + "title": "How much do you spend on butter?", + "type": "General", + "answers": [ + { + "id": "butter-answer", + "label": "Butter", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "currency-total-playback", + "title": "We calculate the total of currency values entered to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "answers", + "identifier": "milk-answer" + }, + { + "source": "answers", + "identifier": "eggs-answer" + }, + { + "source": "answers", + "identifier": "bread-answer" + }, + { + "source": "answers", + "identifier": "cheese-answer" + }, + { + "source": "answers", + "identifier": "butter-answer" + } + ] + }, + "title": "Grand total of previous values" + } + } + ], + "id": "calculated-summary-group" + } + ] + }, + { + "title": "Dependent question Section", + "summary": { "show_on_completion": true }, + "id": "dependent-question-section", + "groups": [ + { + "blocks": [ + { + "skip_conditions": { + "when": { + ">=": [ + { + "source": "calculated_summary", + "identifier": "currency-total-playback" + }, + 10 + ] + } + }, + "type": "Question", + "id": "fruit", + "question": { + "answers": [ + { + "id": "fruit-answer", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ], + "type": "Radio" + } + ], + "id": "fruit-question", + "title": "Do you like eating fruit", + "type": "General" + } + }, + { + "routing_rules": [ + { + "block": "second-question-block", + "when": { + ">=": [ + { + "source": "calculated_summary", + "identifier": "currency-total-playback" + }, + 100 + ] + } + }, + { + "section": "End" + } + ], + "type": "Question", + "id": "vegetables", + "question": { + "answers": [ + { + "id": "vegetables-answer", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ], + "type": "Radio" + } + ], + "id": "vegetables-question", + "title": "Do you like eating vegetables", + "type": "General" + } + }, + { + "type": "Question", + "id": "second-question-block", + "question": { + "id": "second-question", + "title": "How much do you spend on the following items?", + "type": "General", + "answers": [ + { + "id": "apples-answer", + "label": "Apples", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + }, + { + "id": "bananas-answer", + "label": "Bananas", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + }, + { + "id": "oranges-answer", + "label": "Oranges", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + }, + { + "id": "lemons-answer", + "label": "Lemons", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + } + ], + "id": "dependent-question-group" + } + ] + }, + { + "enabled": { + "when": { + "==": [ + 100, + { + "source": "calculated_summary", + "identifier": "currency-total-playback" + } + ] + } + }, + "title": "Dependent Enabled Section", + "summary": { "show_on_completion": true }, + "id": "dependent-enabled-section", + "groups": [ + { + "blocks": [ + { + "type": "Question", + "id": "desserts", + "question": { + "answers": [ + { + "id": "desserts-answer", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ], + "type": "Radio" + } + ], + "id": "desserts-question", + "title": "Do you like eating desserts", + "type": "General" + } + } + ], + "id": "dependent-enabled-section-group" + } + ] + } + ] +} diff --git a/schemas/test/en/test_new_routing_answer_comparison.json b/schemas/test/en/test_routing_answer_comparison.json similarity index 100% rename from schemas/test/en/test_new_routing_answer_comparison.json rename to schemas/test/en/test_routing_answer_comparison.json diff --git a/schemas/test/en/test_new_routing_answered_unanswered.json b/schemas/test/en/test_routing_answered_unanswered.json similarity index 99% rename from schemas/test/en/test_new_routing_answered_unanswered.json rename to schemas/test/en/test_routing_answered_unanswered.json index 8737d61180..ad598a5a8b 100644 --- a/schemas/test/en/test_new_routing_answered_unanswered.json +++ b/schemas/test/en/test_routing_answered_unanswered.json @@ -218,7 +218,7 @@ "id": "answered-question-3", "type": "Interstitial", "content": { - "title": "You chose at least 1 slice" + "title": "You chose at least 1 slice" }, "routing_rules": [ { diff --git a/schemas/test/en/test_new_routing_case_insensitive_text_field.json b/schemas/test/en/test_routing_case_insensitive_text_field.json similarity index 100% rename from schemas/test/en/test_new_routing_case_insensitive_text_field.json rename to schemas/test/en/test_routing_case_insensitive_text_field.json index d573f8793d..2b910c9284 100644 --- a/schemas/test/en/test_new_routing_case_insensitive_text_field.json +++ b/schemas/test/en/test_routing_case_insensitive_text_field.json @@ -93,25 +93,25 @@ ] }, { - "id": "country-interstitial-india-or-azerbaijan", + "id": "country-interstitial-georgia", "type": "Interstitial", "content": { - "title": "Condition: Submitted India or Azerbaijan", + "title": "Condition: Submitted Georgia", "contents": [ { - "description": "You submitted India or Azerbaijan." + "description": "You submitted Georgia." } ] } }, { - "id": "country-interstitial-georgia", + "id": "country-interstitial-india-or-azerbaijan", "type": "Interstitial", "content": { - "title": "Condition: Submitted Georgia", + "title": "Condition: Submitted India or Azerbaijan", "contents": [ { - "description": "You submitted Georgia." + "description": "You submitted India or Azerbaijan." } ] } diff --git a/schemas/test/en/test_routing_checkbox_contains.json b/schemas/test/en/test_routing_checkbox_contains.json index 54d879b936..cf16b416ac 100644 --- a/schemas/test/en/test_routing_checkbox_contains.json +++ b/schemas/test/en/test_routing_checkbox_contains.json @@ -70,33 +70,31 @@ }, "routing_rules": [ { - "goto": { - "block": "country-interstitial-all", - "when": [ + "block": "country-interstitial-all", + "when": { + "all-in": [ + ["India", "Azerbaijan", "Liechtenstein"], { - "id": "country-checkbox-answer", - "condition": "contains all", - "values": ["India", "Azerbaijan", "Liechtenstein"] + "identifier": "country-checkbox-answer", + "source": "answers" } ] } }, { - "goto": { - "block": "country-interstitial-any", - "when": [ + "block": "country-interstitial-any", + "when": { + "any-in": [ { - "id": "country-checkbox-answer", - "condition": "contains any", - "values": ["India", "Azerbaijan"] - } + "identifier": "country-checkbox-answer", + "source": "answers" + }, + ["India", "Azerbaijan"] ] } }, { - "goto": { - "section": "End" - } + "section": "End" } ] }, diff --git a/schemas/test/en/test_new_routing_checkbox_contains_all.json b/schemas/test/en/test_routing_checkbox_contains_all.json similarity index 100% rename from schemas/test/en/test_new_routing_checkbox_contains_all.json rename to schemas/test/en/test_routing_checkbox_contains_all.json diff --git a/schemas/test/en/test_new_routing_checkbox_contains_any.json b/schemas/test/en/test_routing_checkbox_contains_any.json similarity index 100% rename from schemas/test/en/test_new_routing_checkbox_contains_any.json rename to schemas/test/en/test_routing_checkbox_contains_any.json diff --git a/schemas/test/en/test_new_routing_checkbox_contains.json b/schemas/test/en/test_routing_checkbox_contains_in.json similarity index 100% rename from schemas/test/en/test_new_routing_checkbox_contains.json rename to schemas/test/en/test_routing_checkbox_contains_in.json diff --git a/schemas/test/en/test_new_routing_checkbox_count.json b/schemas/test/en/test_routing_checkbox_count.json similarity index 97% rename from schemas/test/en/test_new_routing_checkbox_count.json rename to schemas/test/en/test_routing_checkbox_count.json index 67bba4897c..cd351370e2 100644 --- a/schemas/test/en/test_new_routing_checkbox_count.json +++ b/schemas/test/en/test_routing_checkbox_count.json @@ -103,7 +103,7 @@ "contents": [ { "description": { - "text": "You were asked to select 2 or more toppings but you actually selected {answer_count}.", + "text": "You were asked to select 2 or more toppings but you actually selected {answer_count}.", "placeholders": [ { "placeholder": "answer_count", @@ -138,7 +138,7 @@ "contents": [ { "description": { - "text": "You were asked to select 2 or more toppings and you selected {answer_count}.", + "text": "You were asked to select 2 or more toppings and you selected {answer_count}.", "placeholders": [ { "placeholder": "answer_count", diff --git a/schemas/test/en/test_routing_date_equals.json b/schemas/test/en/test_routing_date_equals.json index d827b6c2bb..3101d2760d 100644 --- a/schemas/test/en/test_routing_date_equals.json +++ b/schemas/test/en/test_routing_date_equals.json @@ -43,7 +43,6 @@ { "id": "comparison-date-answer", "mandatory": true, - "q_code": "11", "type": "Date" } ], @@ -105,125 +104,172 @@ }, "routing_rules": [ { - "goto": { - "block": "correct-answer", - "when": [ + "block": "correct-answer", + "when": { + "or": [ { - "id": "single-date-answer", - "condition": "equals", - "date_comparison": { - "id": "comparison-date-answer", - "offset_by": { - "days": -1 + "==": [ + { + "date": [ + { + "source": "answers", + "identifier": "single-date-answer" + } + ] + }, + { + "date": [ + { + "source": "answers", + "identifier": "comparison-date-answer" + }, + { + "days": -1 + } + ] } - } - } - ] - } - }, - { - "goto": { - "block": "correct-answer", - "when": [ + ] + }, { - "id": "single-date-answer", - "condition": "equals", - "date_comparison": { - "id": "comparison-date-answer" - } - } - ] - } - }, - { - "goto": { - "block": "correct-answer", - "when": [ + "==": [ + { + "date": [ + { + "source": "answers", + "identifier": "single-date-answer" + } + ] + }, + { + "date": [ + { + "source": "answers", + "identifier": "comparison-date-answer" + } + ] + } + ] + }, { - "id": "single-date-answer", - "condition": "equals", - "date_comparison": { - "id": "comparison-date-answer", - "offset_by": { - "days": 1 + "==": [ + { + "date": [ + { + "source": "answers", + "identifier": "single-date-answer" + } + ] + }, + { + "date": [ + { + "source": "answers", + "identifier": "comparison-date-answer" + }, + { + "days": 1 + } + ] } - } - } - ] - } - }, - { - "goto": { - "block": "correct-answer", - "when": [ + ] + }, { - "id": "single-date-answer", - "condition": "equals", - "date_comparison": { - "id": "comparison-date-answer", - "offset_by": { - "months": -1 + "==": [ + { + "date": [ + { + "source": "answers", + "identifier": "single-date-answer" + } + ] + }, + { + "date": [ + { + "source": "answers", + "identifier": "comparison-date-answer" + }, + { + "months": -1 + } + ] } - } - } - ] - } - }, - { - "goto": { - "block": "correct-answer", - "when": [ + ] + }, { - "id": "single-date-answer", - "condition": "equals", - "date_comparison": { - "id": "comparison-date-answer", - "offset_by": { - "months": 1 + "==": [ + { + "date": [ + { + "source": "answers", + "identifier": "single-date-answer" + } + ] + }, + { + "date": [ + { + "source": "answers", + "identifier": "comparison-date-answer" + }, + { + "months": 1 + } + ] } - } - } - ] - } - }, - { - "goto": { - "block": "correct-answer", - "when": [ + ] + }, { - "id": "single-date-answer", - "condition": "equals", - "date_comparison": { - "id": "comparison-date-answer", - "offset_by": { - "years": -1 + "==": [ + { + "date": [ + { + "source": "answers", + "identifier": "single-date-answer" + } + ] + }, + { + "date": [ + { + "source": "answers", + "identifier": "comparison-date-answer" + }, + { + "years": -1 + } + ] } - } - } - ] - } - }, - { - "goto": { - "block": "correct-answer", - "when": [ + ] + }, { - "id": "single-date-answer", - "condition": "equals", - "date_comparison": { - "id": "comparison-date-answer", - "offset_by": { - "years": 1 + "==": [ + { + "date": [ + { + "source": "answers", + "identifier": "single-date-answer" + } + ] + }, + { + "date": [ + { + "source": "answers", + "identifier": "comparison-date-answer" + }, + { + "years": 1 + } + ] } - } + ] } ] } }, { - "goto": { - "block": "incorrect-answer" - } + "block": "incorrect-answer" } ] }, @@ -240,9 +286,7 @@ }, "routing_rules": [ { - "goto": { - "section": "End" - } + "section": "End" } ] }, diff --git a/schemas/test/en/test_routing_date_greater_than.json b/schemas/test/en/test_routing_date_greater_than.json index 605a01262d..cecc09776f 100644 --- a/schemas/test/en/test_routing_date_greater_than.json +++ b/schemas/test/en/test_routing_date_greater_than.json @@ -47,7 +47,7 @@ { "id": "single-date-answer", "mandatory": true, - "type": "MonthYearDate" + "type": "Date" } ], "id": "date-questions", @@ -75,23 +75,20 @@ }, "routing_rules": [ { - "goto": { - "block": "correct-answer", - "when": [ + "block": "correct-answer", + "when": { + ">": [ { - "id": "single-date-answer", - "condition": "greater than", - "date_comparison": { - "meta": "return_by" - } + "date": [{ "source": "answers", "identifier": "single-date-answer" }] + }, + { + "date": [{ "source": "metadata", "identifier": "return_by" }] } ] } }, { - "goto": { - "block": "incorrect-answer" - } + "block": "incorrect-answer" } ] }, @@ -127,9 +124,7 @@ }, "routing_rules": [ { - "goto": { - "section": "End" - } + "section": "End" } ] }, diff --git a/schemas/test/en/test_new_routing_date_greater_than_or_equals.json b/schemas/test/en/test_routing_date_greater_than_or_equals.json similarity index 100% rename from schemas/test/en/test_new_routing_date_greater_than_or_equals.json rename to schemas/test/en/test_routing_date_greater_than_or_equals.json diff --git a/schemas/test/en/test_routing_date_less_than.json b/schemas/test/en/test_routing_date_less_than.json index 8ccdd7953d..30925081ff 100644 --- a/schemas/test/en/test_routing_date_less_than.json +++ b/schemas/test/en/test_routing_date_less_than.json @@ -53,23 +53,20 @@ }, "routing_rules": [ { - "goto": { - "block": "correct-answer", - "when": [ + "block": "correct-answer", + "when": { + "<": [ { - "id": "single-date-answer", - "condition": "less than", - "date_comparison": { - "value": "now" - } + "date": [{ "source": "answers", "identifier": "single-date-answer" }] + }, + { + "date": ["now"] } ] } }, { - "goto": { - "block": "incorrect-answer" - } + "block": "incorrect-answer" } ] }, @@ -86,9 +83,7 @@ }, "routing_rules": [ { - "goto": { - "section": "End" - } + "section": "End" } ] }, diff --git a/schemas/test/en/test_new_routing_date_less_than_or_equals.json b/schemas/test/en/test_routing_date_less_than_or_equals.json similarity index 100% rename from schemas/test/en/test_new_routing_date_less_than_or_equals.json rename to schemas/test/en/test_routing_date_less_than_or_equals.json diff --git a/schemas/test/en/test_routing_date_not_equals.json b/schemas/test/en/test_routing_date_not_equals.json index 9f183279e9..0cbb7d5ee8 100644 --- a/schemas/test/en/test_routing_date_not_equals.json +++ b/schemas/test/en/test_routing_date_not_equals.json @@ -44,32 +44,29 @@ "id": "single-date-answer", "label": "Today", "mandatory": true, - "type": "Date" + "type": "MonthYearDate" } ], "id": "date-questions", - "title": "Enter a date other than 28 February 2018", + "title": "Enter a date other than February 2018", "type": "General" }, "routing_rules": [ { - "goto": { - "block": "correct-answer", - "when": [ + "block": "correct-answer", + "when": { + "!=": [ { - "id": "single-date-answer", - "condition": "not equals", - "date_comparison": { - "value": "2018-02-28" - } + "date": [{ "source": "answers", "identifier": "single-date-answer" }] + }, + { + "date": ["2018-02"] } ] } }, { - "goto": { - "block": "incorrect-answer" - } + "block": "incorrect-answer" } ] }, @@ -86,9 +83,7 @@ }, "routing_rules": [ { - "goto": { - "section": "End" - } + "section": "End" } ] }, diff --git a/schemas/test/en/test_new_routing_group.json b/schemas/test/en/test_routing_group.json similarity index 100% rename from schemas/test/en/test_new_routing_group.json rename to schemas/test/en/test_routing_group.json diff --git a/schemas/test/en/test_new_routing_not.json b/schemas/test/en/test_routing_not.json similarity index 100% rename from schemas/test/en/test_new_routing_not.json rename to schemas/test/en/test_routing_not.json diff --git a/schemas/test/en/test_new_routing_not_affected_by_answers_not_on_path.json b/schemas/test/en/test_routing_not_affected_by_answers_not_on_path.json similarity index 100% rename from schemas/test/en/test_new_routing_not_affected_by_answers_not_on_path.json rename to schemas/test/en/test_routing_not_affected_by_answers_not_on_path.json diff --git a/schemas/test/en/test_routing_number_equals.json b/schemas/test/en/test_routing_number_equals.json index 4436eed82d..83c33bf109 100644 --- a/schemas/test/en/test_routing_number_equals.json +++ b/schemas/test/en/test_routing_number_equals.json @@ -6,7 +6,7 @@ "survey_id": "001", "title": "Test Routing Number Equals", "theme": "default", - "description": "A test survey for routing based on an number equals", + "description": "A test survey for routing based on a number equals", "metadata": [ { "name": "user_id", @@ -44,7 +44,7 @@ "id": "answer", "mandatory": true, "type": "Number", - "label": "123" + "label": "Enter 123" } ], "id": "question", @@ -53,21 +53,19 @@ }, "routing_rules": [ { - "goto": { - "block": "correct-answer", - "when": [ + "block": "correct-answer", + "when": { + "==": [ { - "id": "answer", - "condition": "equals", - "value": 123 - } + "source": "answers", + "identifier": "answer" + }, + 123 ] } }, { - "goto": { - "block": "incorrect-answer" - } + "block": "incorrect-answer" } ] }, @@ -79,7 +77,7 @@ "contents": [ { "description": { - "text": "You were asked to enter 123 but you actually entered {answer}.", + "text": "You were asked to enter 123 but you actually entered {answer}.", "placeholders": [ { "placeholder": "answer", @@ -95,9 +93,7 @@ }, "routing_rules": [ { - "goto": { - "section": "End" - } + "section": "End" } ] }, @@ -109,7 +105,7 @@ "contents": [ { "description": { - "text": "You were asked to enter 123 and you entered {answer}.", + "text": "You were asked to enter 123 and you entered {answer}.", "placeholders": [ { "placeholder": "answer", diff --git a/schemas/test/en/test_routing_number_greater_than.json b/schemas/test/en/test_routing_number_greater_than.json index d68b1e4afd..1e5f8552b5 100644 --- a/schemas/test/en/test_routing_number_greater_than.json +++ b/schemas/test/en/test_routing_number_greater_than.json @@ -44,30 +44,28 @@ "id": "answer", "mandatory": true, "type": "Number", - "label": "Greater than 123" + "label": "Enter a number greater than 123" } ], "id": "question", - "title": "Enter the number greater than 123", + "title": "Enter a number greater than 123", "type": "General" }, "routing_rules": [ { - "goto": { - "block": "correct-answer", - "when": [ + "block": "correct-answer", + "when": { + ">": [ { - "id": "answer", - "condition": "greater than", - "value": 123 - } + "source": "answers", + "identifier": "answer" + }, + 123 ] } }, { - "goto": { - "block": "incorrect-answer" - } + "block": "incorrect-answer" } ] }, @@ -75,11 +73,11 @@ "type": "Interstitial", "id": "incorrect-answer", "content": { - "title": "Incorrect answer", + "title": "You did not enter a number greater than 123", "contents": [ { "description": { - "text": "You were asked to enter a number greater than 123 but you entered {answer}.", + "text": "You were asked to enter a number greater than 123 but you actually entered {answer}.", "placeholders": [ { "placeholder": "answer", @@ -95,9 +93,7 @@ }, "routing_rules": [ { - "goto": { - "section": "End" - } + "section": "End" } ] }, @@ -105,11 +101,11 @@ "type": "Interstitial", "id": "correct-answer", "content": { - "title": "Correct answer", + "title": "Correct", "contents": [ { "description": { - "text": "You were asked to enter a number greater than 123 and you entered {answer}.", + "text": "You were asked to enter a number greater than 123 and you entered {answer}.", "placeholders": [ { "placeholder": "answer", diff --git a/schemas/test/en/test_routing_number_greater_than_or_equal.json b/schemas/test/en/test_routing_number_greater_than_or_equal.json index ed149f9527..b0096acdf8 100644 --- a/schemas/test/en/test_routing_number_greater_than_or_equal.json +++ b/schemas/test/en/test_routing_number_greater_than_or_equal.json @@ -4,9 +4,9 @@ "schema_version": "0.0.1", "data_version": "0.0.3", "survey_id": "001", - "title": "Test Routing Number Greater Than or Equal", + "title": "Test Routing Number Greater Than Or Equal To", "theme": "default", - "description": "A test survey for routing based on a number greater than or equal", + "description": "A test survey for routing based on a number greater than or equal to", "metadata": [ { "name": "user_id", @@ -44,7 +44,7 @@ "id": "answer", "mandatory": true, "type": "Number", - "label": "Greater than 123" + "label": "123 or greater" } ], "id": "question", @@ -53,33 +53,19 @@ }, "routing_rules": [ { - "goto": { - "block": "correct-answer", - "when": [ + "block": "correct-answer", + "when": { + ">=": [ { - "id": "answer", - "condition": "greater than", - "value": 123 - } + "source": "answers", + "identifier": "answer" + }, + 123 ] } }, { - "goto": { - "block": "correct-answer", - "when": [ - { - "id": "answer", - "condition": "equals", - "value": 123 - } - ] - } - }, - { - "goto": { - "block": "incorrect-answer" - } + "block": "incorrect-answer" } ] }, @@ -91,7 +77,7 @@ "contents": [ { "description": { - "text": "You were asked to enter a number greater than or equal to 123 but you entered {answer}.", + "text": "You were asked to enter a number greater than or equal to 123 but you entered {answer}.", "placeholders": [ { "placeholder": "answer", @@ -107,9 +93,7 @@ }, "routing_rules": [ { - "goto": { - "section": "End" - } + "section": "End" } ] }, @@ -121,7 +105,7 @@ "contents": [ { "description": { - "text": "You were asked to enter a number greater than or equal to 123 and you entered {answer}.", + "text": "You were asked to enter a number greater than or equal to 123 and you entered {answer}.", "placeholders": [ { "placeholder": "answer", diff --git a/schemas/test/en/test_routing_number_greater_than_or_equal_single_condition.json b/schemas/test/en/test_routing_number_greater_than_or_equal_single_condition.json index 0445e41c06..b16a50963d 100644 --- a/schemas/test/en/test_routing_number_greater_than_or_equal_single_condition.json +++ b/schemas/test/en/test_routing_number_greater_than_or_equal_single_condition.json @@ -53,21 +53,19 @@ }, "routing_rules": [ { - "goto": { - "block": "correct-answer", - "when": [ + "block": "correct-answer", + "when": { + ">=": [ { - "id": "answer", - "condition": "greater than or equal to", - "value": 123 - } + "source": "answers", + "identifier": "answer" + }, + 123 ] } }, { - "goto": { - "block": "incorrect-answer" - } + "block": "incorrect-answer" } ] }, @@ -79,7 +77,7 @@ "contents": [ { "description": { - "text": "You were asked to enter a number greater than or equal to 123 but you entered {answer}.", + "text": "You were asked to enter a number greater than or equal to 123 but you entered {answer}.", "placeholders": [ { "placeholder": "answer", @@ -95,9 +93,7 @@ }, "routing_rules": [ { - "goto": { - "section": "End" - } + "section": "End" } ] }, @@ -109,7 +105,7 @@ "contents": [ { "description": { - "text": "You were asked to enter a number greater than or equal to 123 and you entered {answer}.", + "text": "You were asked to enter a number greater than or equal to 123 and you entered {answer}.", "placeholders": [ { "placeholder": "answer", diff --git a/schemas/test/en/test_routing_number_less_than.json b/schemas/test/en/test_routing_number_less_than.json index 68b73b0bcc..ed5eec019f 100644 --- a/schemas/test/en/test_routing_number_less_than.json +++ b/schemas/test/en/test_routing_number_less_than.json @@ -44,30 +44,28 @@ "id": "answer", "mandatory": true, "type": "Number", - "label": "Less than 123" + "label": "Enter a number less than 123" } ], "id": "question", - "title": "Enter the number less than 123", + "title": "Enter a number less than 123", "type": "General" }, "routing_rules": [ { - "goto": { - "block": "correct-answer", - "when": [ + "block": "correct-answer", + "when": { + "<": [ { - "id": "answer", - "condition": "less than", - "value": 123 - } + "source": "answers", + "identifier": "answer" + }, + 123 ] } }, { - "goto": { - "block": "incorrect-answer" - } + "block": "incorrect-answer" } ] }, @@ -75,11 +73,11 @@ "type": "Interstitial", "id": "incorrect-answer", "content": { - "title": "Incorrect answer", + "title": "You did not enter a number less than 123", "contents": [ { "description": { - "text": "You were asked to enter a number less than 123 but you entered {answer}.", + "text": "You were asked to enter a number less than 123 but you actually entered {answer}.", "placeholders": [ { "placeholder": "answer", @@ -95,9 +93,7 @@ }, "routing_rules": [ { - "goto": { - "section": "End" - } + "section": "End" } ] }, @@ -105,11 +101,11 @@ "type": "Interstitial", "id": "correct-answer", "content": { - "title": "Correct answer", + "title": "Correct", "contents": [ { "description": { - "text": "You were asked to enter a number less than 123 and you entered {answer}.", + "text": "You were asked to enter a number less than 123 and you entered {answer}.", "placeholders": [ { "placeholder": "answer", diff --git a/schemas/test/en/test_routing_number_less_than_or_equal.json b/schemas/test/en/test_routing_number_less_than_or_equal.json index f1514929eb..d45041fd8a 100644 --- a/schemas/test/en/test_routing_number_less_than_or_equal.json +++ b/schemas/test/en/test_routing_number_less_than_or_equal.json @@ -4,9 +4,9 @@ "schema_version": "0.0.1", "data_version": "0.0.3", "survey_id": "001", - "title": "Test Routing Number Less Than or Equal", + "title": "Test Routing Number Less Than Or Equal To", "theme": "default", - "description": "A test survey for routing based on a number less than or equal", + "description": "A test survey for routing based on a number less than or equal to", "metadata": [ { "name": "user_id", @@ -44,7 +44,7 @@ "id": "answer", "mandatory": true, "type": "Number", - "label": "Less than or equal to 123" + "label": "Number" } ], "id": "question", @@ -53,33 +53,19 @@ }, "routing_rules": [ { - "goto": { - "block": "correct-answer", - "when": [ + "block": "correct-answer", + "when": { + "<=": [ { - "id": "answer", - "condition": "less than", - "value": 123 - } + "source": "answers", + "identifier": "answer" + }, + 123 ] } }, { - "goto": { - "block": "correct-answer", - "when": [ - { - "id": "answer", - "condition": "equals", - "value": 123 - } - ] - } - }, - { - "goto": { - "block": "incorrect-answer" - } + "block": "incorrect-answer" } ] }, @@ -91,7 +77,7 @@ "contents": [ { "description": { - "text": "You were asked to enter a number less than or equal to 123 but you entered {answer}.", + "text": "You were asked to enter a number less than or equal to 123 but you entered {answer}.", "placeholders": [ { "placeholder": "answer", @@ -107,9 +93,7 @@ }, "routing_rules": [ { - "goto": { - "section": "End" - } + "section": "End" } ] }, @@ -117,11 +101,11 @@ "type": "Interstitial", "id": "correct-answer", "content": { - "title": "correct answer", + "title": "Correct answer", "contents": [ { "description": { - "text": "You were asked to enter a number less than or equal to 123 and you entered {answer}.", + "text": "You were asked to enter a number less than or equal to 123 and you entered {answer}.", "placeholders": [ { "placeholder": "answer", diff --git a/schemas/test/en/test_routing_number_less_than_or_equal_single_condition.json b/schemas/test/en/test_routing_number_less_than_or_equal_single_condition.json index cadf01cbdb..ef8ba8b256 100644 --- a/schemas/test/en/test_routing_number_less_than_or_equal_single_condition.json +++ b/schemas/test/en/test_routing_number_less_than_or_equal_single_condition.json @@ -53,21 +53,19 @@ }, "routing_rules": [ { - "goto": { - "block": "correct-answer", - "when": [ + "block": "correct-answer", + "when": { + "<=": [ { - "id": "answer", - "condition": "less than or equal to", - "value": 123 - } + "source": "answers", + "identifier": "answer" + }, + 123 ] } }, { - "goto": { - "block": "incorrect-answer" - } + "block": "incorrect-answer" } ] }, @@ -79,7 +77,7 @@ "contents": [ { "description": { - "text": "You were asked to enter a number less than or equal to 123 but you entered {answer}.", + "text": "You were asked to enter a number less than or equal to 123 but you entered {answer}.", "placeholders": [ { "placeholder": "answer", @@ -95,9 +93,7 @@ }, "routing_rules": [ { - "goto": { - "section": "End" - } + "section": "End" } ] }, @@ -109,7 +105,7 @@ "contents": [ { "description": { - "text": "You were asked to enter a number less than or equal to 123 and you entered {answer}.", + "text": "You were asked to enter a number less than or equal to 123 and you entered {answer}.", "placeholders": [ { "placeholder": "answer", diff --git a/schemas/test/en/test_routing_number_not_equals.json b/schemas/test/en/test_routing_number_not_equals.json index 71e176b597..e0c7505133 100644 --- a/schemas/test/en/test_routing_number_not_equals.json +++ b/schemas/test/en/test_routing_number_not_equals.json @@ -53,21 +53,19 @@ }, "routing_rules": [ { - "goto": { - "block": "correct-answer", - "when": [ + "block": "correct-answer", + "when": { + "!=": [ { - "id": "answer", - "condition": "not equals", - "value": 123 - } + "source": "answers", + "identifier": "answer" + }, + 123 ] } }, { - "goto": { - "block": "incorrect-answer" - } + "block": "incorrect-answer" } ] }, @@ -79,7 +77,7 @@ "contents": [ { "description": { - "text": "You were asked not to enter 123 but you entered {answer}.", + "text": "You were asked not to enter 123 but you entered {answer}.", "placeholders": [ { "placeholder": "answer", @@ -95,9 +93,7 @@ }, "routing_rules": [ { - "goto": { - "section": "End" - } + "section": "End" } ] }, @@ -109,7 +105,7 @@ "contents": [ { "description": { - "text": "You were asked not to enter 123 and you entered {answer}.", + "text": "You were asked not to enter 123 and you entered {answer}.", "placeholders": [ { "placeholder": "answer", diff --git a/schemas/test/en/test_routing_on_multiple_select.json b/schemas/test/en/test_routing_on_multiple_select.json index c8dc6baffb..312b654f2d 100644 --- a/schemas/test/en/test_routing_on_multiple_select.json +++ b/schemas/test/en/test_routing_on_multiple_select.json @@ -6,7 +6,7 @@ "survey_id": "0", "title": "Test schema for routing on multiple selected answers", "description": "Test schema for routing on multiple selected answers", - "theme": "census", + "theme": "default", "metadata": [ { "name": "user_id", @@ -15,6 +15,10 @@ { "name": "period_id", "type": "string" + }, + { + "name": "ru_name", + "type": "string" } ], "questionnaire_flow": { @@ -58,28 +62,25 @@ "value": "None" } ], - "q_code": "20", "type": "Checkbox" } ] }, "routing_rules": [ { - "goto": { - "block": "block3", - "when": [ + "block": "block3", + "when": { + "in": [ + "United Kingdom", { - "id": "passports-answer", - "condition": "contains", - "value": "United Kingdom" + "identifier": "passports-answer", + "source": "answers" } ] } }, { - "goto": { - "block": "block2" - } + "block": "block2" } ] }, @@ -95,7 +96,6 @@ "id": "block2-answer", "label": "Question 2", "mandatory": false, - "q_code": "20", "type": "TextField" } ] @@ -113,7 +113,6 @@ "id": "block3-answer", "label": "Question 3", "mandatory": false, - "q_code": "20", "type": "TextField" } ] diff --git a/schemas/test/en/test_new_routing_or.json b/schemas/test/en/test_routing_or.json similarity index 96% rename from schemas/test/en/test_new_routing_or.json rename to schemas/test/en/test_routing_or.json index 8e5984ffc4..0b888a557c 100644 --- a/schemas/test/en/test_new_routing_or.json +++ b/schemas/test/en/test_routing_or.json @@ -114,7 +114,7 @@ "contents": [ { "description": { - "text": "You were asked to enter 123 or 321 but you actually entered {answer_1} and {answer_2}.", + "text": "You were asked to enter 123 or 321 but you actually entered {answer_1} and {answer_2}.", "placeholders": [ { "placeholder": "answer_1", @@ -149,7 +149,7 @@ "contents": [ { "description": { - "text": "You were asked to enter 123 or 321 and you entered {answer_1} and {answer_2}.", + "text": "You were asked to enter 123 or 321 and you entered {answer_1} and {answer_2}.", "placeholders": [ { "placeholder": "answer_1", diff --git a/schemas/test/en/test_new_routing_to_questionnaire_end_multiple_sections.json b/schemas/test/en/test_routing_to_questionnaire_end_multiple_sections.json similarity index 100% rename from schemas/test/en/test_new_routing_to_questionnaire_end_multiple_sections.json rename to schemas/test/en/test_routing_to_questionnaire_end_multiple_sections.json diff --git a/schemas/test/en/test_new_routing_to_questionnaire_end_single_section.json b/schemas/test/en/test_routing_to_questionnaire_end_single_section.json similarity index 100% rename from schemas/test/en/test_new_routing_to_questionnaire_end_single_section.json rename to schemas/test/en/test_routing_to_questionnaire_end_single_section.json diff --git a/schemas/test/en/test_new_routing_to_section_end.json b/schemas/test/en/test_routing_to_section_end.json similarity index 100% rename from schemas/test/en/test_new_routing_to_section_end.json rename to schemas/test/en/test_routing_to_section_end.json diff --git a/schemas/test/en/test_section_enabled_checkbox.json b/schemas/test/en/test_section_enabled_checkbox.json index deb294cc6b..7c60b826c5 100644 --- a/schemas/test/en/test_section_enabled_checkbox.json +++ b/schemas/test/en/test_section_enabled_checkbox.json @@ -85,17 +85,17 @@ { "id": "section-2", "title": "Section 2", - "enabled": [ - { - "when": [ + "enabled": { + "when": { + "in": [ + "Section 2", { - "id": "section-1-answer", - "condition": "contains", - "value": "Section 2" + "source": "answers", + "identifier": "section-1-answer" } ] } - ], + }, "groups": [ { "blocks": [ @@ -126,17 +126,17 @@ { "id": "section-3", "title": "Section 3", - "enabled": [ - { - "when": [ + "enabled": { + "when": { + "in": [ + "Section 3", { - "id": "section-1-answer", - "condition": "contains", - "value": "Section 3" + "source": "answers", + "identifier": "section-1-answer" } ] } - ], + }, "groups": [ { "blocks": [ diff --git a/schemas/test/en/test_section_enabled_hub.json b/schemas/test/en/test_section_enabled_hub.json index a32a3ed80d..5f6e82f73b 100644 --- a/schemas/test/en/test_section_enabled_hub.json +++ b/schemas/test/en/test_section_enabled_hub.json @@ -80,17 +80,17 @@ { "id": "section-2", "title": "Section 2", - "enabled": [ - { - "when": [ + "enabled": { + "when": { + "in": [ + "Section 2", { - "id": "section-1-answer", - "condition": "contains", - "value": "Section 2" + "source": "answers", + "identifier": "section-1-answer" } ] } - ], + }, "groups": [ { "blocks": [ @@ -121,17 +121,17 @@ { "id": "section-3", "title": "Section 3", - "enabled": [ - { - "when": [ + "enabled": { + "when": { + "in": [ + "Section 3", { - "id": "section-1-answer", - "condition": "contains", - "value": "Section 3" + "source": "answers", + "identifier": "section-1-answer" } ] } - ], + }, "groups": [ { "blocks": [ diff --git a/schemas/test/en/test_section_enabled_radio.json b/schemas/test/en/test_section_enabled_radio.json index 47ab94200c..3141ae569f 100644 --- a/schemas/test/en/test_section_enabled_radio.json +++ b/schemas/test/en/test_section_enabled_radio.json @@ -73,17 +73,17 @@ { "id": "section-2", "title": "Section 2", - "enabled": [ - { - "when": [ + "enabled": { + "when": { + "==": [ + "Yes, enable section 2", { - "id": "section-1-answer", - "condition": "equals", - "value": "Yes, enable section 2" + "source": "answers", + "identifier": "section-1-answer" } ] } - ], + }, "groups": [ { "blocks": [ diff --git a/schemas/test/en/test_section_summary.json b/schemas/test/en/test_section_summary.json index 23d82f2d6f..53b66398b3 100644 --- a/schemas/test/en/test_section_summary.json +++ b/schemas/test/en/test_section_summary.json @@ -122,17 +122,17 @@ } ] }, - "skip_conditions": [ - { - "when": [ + "skip_conditions": { + "when": { + "==": [ { - "id": "insurance-type-answer", - "condition": "equals", - "value": "Both" - } + "source": "answers", + "identifier": "insurance-type-answer" + }, + "Both" ] } - ] + } } ] }, diff --git a/schemas/test/en/test_show_section_summary_on_completion.json b/schemas/test/en/test_show_section_summary_on_completion.json index 6d0614c5f7..25317ebfc0 100644 --- a/schemas/test/en/test_show_section_summary_on_completion.json +++ b/schemas/test/en/test_show_section_summary_on_completion.json @@ -23,7 +23,9 @@ ], "questionnaire_flow": { "type": "Hub", - "options": { "required_completed_sections": ["employment-section"] } + "options": { + "required_completed_sections": ["employment-section"] + } }, "sections": [ { @@ -95,20 +97,19 @@ }, "routing_rules": [ { - "goto": { - "group": "checkboxes", - "when": [ + "block": "employment-type", + "when": { + "==": [ { - "id": "employment-status-answer", - "condition": "set" - } + "identifier": "employment-status-answer", + "source": "answers" + }, + null ] } }, { - "goto": { - "block": "employment-type" - } + "group": "checkboxes" } ] }, diff --git a/schemas/test/en/test_new_skip_condition_answer_comparison.json b/schemas/test/en/test_skip_condition_answer_comparison.json similarity index 100% rename from schemas/test/en/test_new_skip_condition_answer_comparison.json rename to schemas/test/en/test_skip_condition_answer_comparison.json diff --git a/schemas/test/en/test_skip_condition_block.json b/schemas/test/en/test_skip_condition_block.json index 8da02177a5..389aa2095e 100644 --- a/schemas/test/en/test_skip_condition_block.json +++ b/schemas/test/en/test_skip_condition_block.json @@ -4,7 +4,7 @@ "schema_version": "0.0.1", "data_version": "0.0.3", "survey_id": "0", - "title": "Skip group", + "title": "Skip block", "theme": "default", "metadata": [ { @@ -33,20 +33,21 @@ "id": "default-section", "groups": [ { - "id": "do-you-want-to-skip-group", - "title": "Do you want to skip the next block?", + "id": "default-group", + "title": "Group 1", "blocks": [ { "type": "Question", "id": "do-you-want-to-skip", "question": { "id": "do-you-want-to-skip-question", - "title": "Do you want to skip?", + "title": "Do you want to skip the next question?", "type": "General", + "description": ["Select “Yes” to skip the next question and go straight to the summary"], "answers": [ { "id": "do-you-want-to-skip-answer", - "label": "Do you want to skip?", + "label": "Select an answer", "mandatory": true, "options": [ { @@ -58,10 +59,7 @@ "value": "No" } ], - "type": "Radio", - "validation": { - "messages": {} - } + "type": "Radio" } ] } @@ -71,44 +69,27 @@ "id": "should-skip", "question": { "id": "should-skip-question", - "title": "Do you want to skip?", + "title": "Why didn’t you skip the block?", "type": "General", "answers": [ { "id": "should-skip-answer", - "label": "Why didn’t you skip the block?", + "label": "Enter your answer", "mandatory": true, "type": "TextArea" } ] }, - "skip_conditions": [ - { - "when": [ + "skip_conditions": { + "when": { + "==": [ { - "id": "do-you-want-to-skip-answer", - "condition": "equals", - "value": "Yes" - } + "source": "answers", + "identifier": "do-you-want-to-skip-answer" + }, + "Yes" ] } - ] - }, - { - "type": "Question", - "id": "a-non-skipped-block", - "question": { - "id": "will-not-be-skipped-question", - "title": "Always ask this question", - "type": "General", - "answers": [ - { - "id": "will-not-be-skipped-answer", - "label": "never skipped", - "mandatory": true, - "type": "TextArea" - } - ] } } ] diff --git a/schemas/test/en/test_skip_condition_group.json b/schemas/test/en/test_skip_condition_group.json index b474e09151..1640b7ee88 100644 --- a/schemas/test/en/test_skip_condition_group.json +++ b/schemas/test/en/test_skip_condition_group.json @@ -33,20 +33,21 @@ "id": "default-section", "groups": [ { - "id": "do-you-want-to-skip-group", - "title": "Do you want to skip the next group?", + "id": "default-group", + "title": "Group 1", "blocks": [ { "type": "Question", "id": "do-you-want-to-skip", "question": { "id": "do-you-want-to-skip-question", - "title": "Do you want to skip?", + "title": "Do you want to skip the next question?", "type": "General", + "description": ["Select “Yes” to skip the next question and go straight to the summary"], "answers": [ { "id": "do-you-want-to-skip-answer", - "label": "Do you want to skip?", + "label": "Select an answer", "mandatory": true, "options": [ { @@ -58,10 +59,7 @@ "value": "No" } ], - "type": "Radio", - "validation": { - "messages": {} - } + "type": "Radio" } ] } @@ -70,30 +68,30 @@ }, { "id": "should-skip-group", - "title": "This question may or may not be skipped", - "skip_conditions": [ - { - "when": [ + "title": "Group 2 (Skippable)", + "skip_conditions": { + "when": { + "==": [ { - "id": "do-you-want-to-skip-answer", - "condition": "equals", - "value": "Yes" - } + "source": "answers", + "identifier": "do-you-want-to-skip-answer" + }, + "Yes" ] } - ], + }, "blocks": [ { "type": "Question", "id": "should-skip", "question": { "id": "should-skip-question", - "title": "Do you want to skip?", + "title": "Why didn’t you skip the group?", "type": "General", "answers": [ { "id": "should-skip-answer", - "label": "Why didn’t you skip the group?", + "label": "Enter your answer", "mandatory": true, "type": "TextArea" } diff --git a/schemas/test/en/test_new_skip_condition_not_set.json b/schemas/test/en/test_skip_condition_not_set.json similarity index 97% rename from schemas/test/en/test_new_skip_condition_not_set.json rename to schemas/test/en/test_skip_condition_not_set.json index 42b18fd527..80a871d6dd 100644 --- a/schemas/test/en/test_new_skip_condition_not_set.json +++ b/schemas/test/en/test_skip_condition_not_set.json @@ -55,7 +55,6 @@ "value": "Eggs" } ], - "q_code": "20", "type": "Radio" } ], @@ -83,7 +82,6 @@ "value": "Coffee" } ], - "q_code": "20", "type": "Radio" } ], diff --git a/schemas/test/en/test_new_skip_condition_set.json b/schemas/test/en/test_skip_condition_set.json similarity index 97% rename from schemas/test/en/test_new_skip_condition_set.json rename to schemas/test/en/test_skip_condition_set.json index 301fa58006..993fb59e34 100644 --- a/schemas/test/en/test_new_skip_condition_set.json +++ b/schemas/test/en/test_skip_condition_set.json @@ -55,7 +55,6 @@ "value": "Eggs" } ], - "q_code": "20", "type": "Radio" } ], @@ -83,7 +82,6 @@ "value": "Coffee" } ], - "q_code": "20", "type": "Radio" } ], diff --git a/schemas/test/en/test_submit_with_custom_submission_text.json b/schemas/test/en/test_submit_with_custom_submission_text.json index 2820d8c857..e4401d211c 100644 --- a/schemas/test/en/test_submit_with_custom_submission_text.json +++ b/schemas/test/en/test_submit_with_custom_submission_text.json @@ -54,7 +54,6 @@ "id": "breakfast-answer", "label": "What is your favourite breakfast food", "mandatory": false, - "q_code": "0", "type": "TextField" } ], diff --git a/schemas/test/en/test_submit_with_summary.json b/schemas/test/en/test_submit_with_summary.json index 5ab6f32ed0..3a8a3af974 100644 --- a/schemas/test/en/test_submit_with_summary.json +++ b/schemas/test/en/test_submit_with_summary.json @@ -109,7 +109,7 @@ "question": { "id": "test-dessert-confirmation-question", "title": { - "text": "Are you sure {dessert} is your favourite?", + "text": "Are you sure {dessert} is your favourite?", "placeholders": [ { "placeholder": "dessert", @@ -157,7 +157,8 @@ "label": "Currency", "mandatory": false, "type": "Currency", - "currency": "GBP" + "currency": "GBP", + "decimal_places": 2 }, { "id": "numbers-unit-answer", diff --git a/schemas/test/en/test_submit_with_summary_custom_submission_text.json b/schemas/test/en/test_submit_with_summary_custom_submission_text.json index 7caca60a08..07943d2b69 100644 --- a/schemas/test/en/test_submit_with_summary_custom_submission_text.json +++ b/schemas/test/en/test_submit_with_summary_custom_submission_text.json @@ -54,7 +54,6 @@ "id": "dessert", "label": "What is your favourite dessert?", "mandatory": true, - "q_code": "30", "type": "TextField" } ] diff --git a/schemas/test/en/test_supplementary_data.json b/schemas/test/en/test_supplementary_data.json new file mode 100644 index 0000000000..8b541a4e09 --- /dev/null +++ b/schemas/test/en/test_supplementary_data.json @@ -0,0 +1,1950 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "123", + "title": "Test Supplementary Data", + "theme": "default", + "description": "A questionnaire to demo using Supplementary data for placeholders, validation and routing in both repeating and non repeating sections.", + "metadata": [ + { + "name": "survey_id", + "type": "string" + }, + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + }, + { + "name": "sds_dataset_id", + "type": "string" + } + ], + "supplementary_data": { + "lists": ["employees", "products"] + }, + "questionnaire_flow": { + "type": "Hub", + "options": { + "required_completed_sections": ["introduction-section"] + } + }, + "post_submission": { + "view_response": true + }, + "sections": [ + { + "id": "introduction-section", + "title": "Introduction", + "groups": [ + { + "id": "introduction-group", + "title": "Introduction Group", + "blocks": [ + { + "id": "loaded-successfully-block", + "type": "Interstitial", + "content": { + "title": "Supplementary Data", + "contents": [ + { + "title": "You have successfully loaded Supplementary data", + "description": { + "text": "List of products: {products}", + "placeholders": [ + { + "placeholder": "products", + "transforms": [ + { + "transform": "format_list", + "arguments": { + "list_to_format": { + "source": "supplementary_data", + "identifier": "products", + "selectors": ["name"] + } + } + } + ] + } + ] + }, + "guidance": { + "contents": [ + { + "description": { + "text": "The purpose of this block, is to test that supplementary data loads successfully, separate to using the supplementary data. The surnames of the employees are: {surnames}.", + "placeholders": [ + { + "placeholder": "surnames", + "transforms": [ + { + "transform": "concatenate_list", + "arguments": { + "list_to_concatenate": [ + { + "source": "supplementary_data", + "identifier": "employees", + "selectors": ["personal_details", "surname"] + } + ], + "delimiter": ", " + } + } + ] + } + ] + } + } + ] + } + } + ] + } + }, + { + "id": "introduction-block", + "type": "Introduction", + "primary_content": [ + { + "id": "business-details", + "title": { + "text": "You are completing this survey for {company_name}", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "supplementary_data", + "identifier": "company_name" + } + } + ] + }, + "contents": [ + { + "description": { + "text": "If the company details or structure have changed contact us on {telephone_number_link}", + "placeholders": [ + { + "placeholder": "telephone_number_link", + "value": { + "source": "supplementary_data", + "identifier": "company_details", + "selectors": ["telephone_number"] + } + } + ] + } + }, + { + "guidance": { + "contents": [ + { + "title": "Guidance for completing this survey", + "list": [ + "The company name, telephone number all come from supplementary data", + "if you picked the supplementary dataset with guidance, there will be a 3rd bullet point below this one, with the supplementary guidance.", + { + "text": "{survey_guidance}", + "placeholders": [ + { + "placeholder": "survey_guidance", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "source": "supplementary_data", + "identifier": "guidance" + } + ] + } + } + ] + } + ] + } + ] + } + ] + } + } + ] + } + ] + } + ] + } + ] + }, + { + "id": "section-1", + "title": "Company Details", + "summary": { + "page_title": "Summary title", + "show_on_completion": true + }, + "groups": [ + { + "id": "introduction", + "title": "Group 1", + "blocks": [ + { + "id": "email-block", + "type": "Question", + "question": { + "id": "email-question", + "type": "General", + "guidance": { + "contents": [ + { + "description": "If you answer no, an additional block will open up allowing entering of a different email" + } + ] + }, + "title": { + "text": "Is {email} still the correct contact email for {company_name}?", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "supplementary_data", + "identifier": "company_name" + } + }, + { + "placeholder": "email", + "value": { + "source": "supplementary_data", + "identifier": "company_details", + "selectors": ["email"] + } + } + ] + }, + "answers": [ + { + "id": "same-email-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + { + "skip_conditions": { + "when": { + "==": [ + { + "source": "answers", + "identifier": "same-email-answer" + }, + "Yes" + ] + } + }, + "type": "Question", + "id": "new-email", + "question": { + "id": "new-email-question", + "type": "General", + "title": { + "text": "What is the new contact email for {company_name}?", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "supplementary_data", + "identifier": "company_name" + } + } + ] + }, + "answers": [ + { + "id": "new-email-answer", + "label": "Contact email", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + { + "type": "Question", + "id": "trading", + "question": { + "id": "trading-question", + "type": "General", + "title": { + "text": "When did {company_name} begin trading?", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "supplementary_data", + "identifier": "company_name" + } + } + ] + }, + "answers": [ + { + "id": "trading-answer", + "label": "Date commenced", + "mandatory": true, + "type": "Radio", + "dynamic_options": { + "values": { + "map": [ + { + "format-date": ["self", "yyyy-MM-dd"] + }, + { + "date-range": [ + { + "date": [ + { + "source": "supplementary_data", + "identifier": "incorporation_date" + } + ] + }, + 7 + ] + } + ] + }, + "transform": { + "format-date": [ + { + "date": ["self"] + }, + "EEEE d MMMM yyyy" + ] + } + } + } + ] + } + }, + { + "type": "Question", + "id": "sales-breakdown-block", + "question": { + "id": "sales-breakdown-question", + "title": { + "text": "How much of the {sales} total UK sales was from Bristol and London?", + "placeholders": [ + { + "placeholder": "sales", + "transforms": [ + { + "transform": "format_currency", + "arguments": { + "number": { + "source": "supplementary_data", + "identifier": "total_uk_sales" + } + } + } + ] + } + ] + }, + "type": "Calculated", + "warning": "These answers must not exceed the reported total from the supplementary data", + "calculations": [ + { + "calculation_type": "sum", + "value": { + "source": "supplementary_data", + "identifier": "total_uk_sales" + }, + "answers_to_calculate": ["sales-bristol-answer", "sales-london-answer"], + "conditions": ["less than", "equals"] + } + ], + "answers": [ + { + "id": "sales-bristol-answer", + "label": "Bristol Sales", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + }, + { + "id": "sales-london-answer", + "label": "London Sales", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "calculated-summary-sales", + "title": "Total value of sales from Bristol and London is calculated to be %(total)s. Is this correct?", + "calculation": { + "title": "Total sales", + "operation": { + "+": [ + { + "source": "answers", + "identifier": "sales-london-answer" + }, + { + "source": "answers", + "identifier": "sales-bristol-answer" + } + ] + } + } + }, + { + "id": "section-1-interstitial", + "type": "Interstitial", + "content": { + "title": { + "text": "Summary of information provided for {company_name}", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "supplementary_data", + "identifier": "company_name" + } + } + ] + }, + "contents": [ + { + "list": [ + { + "text": "Telephone Number: {phone}", + "placeholders": [ + { + "placeholder": "phone", + "value": { + "source": "supplementary_data", + "identifier": "company_details", + "selectors": ["telephone_number"] + } + } + ] + }, + { + "text": "Email: {company_email}", + "placeholders": [ + { + "placeholder": "company_email", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "source": "answers", + "identifier": "new-email-answer" + }, + { + "source": "supplementary_data", + "identifier": "company_details", + "selectors": ["email"] + } + ] + } + } + ] + } + ] + }, + { + "text": "Note Title: {note_title}", + "placeholders": [ + { + "placeholder": "note_title", + "value": { + "source": "supplementary_data", + "identifier": "note", + "selectors": ["title"] + } + } + ] + }, + { + "text": "Note Description: {note_description}", + "placeholders": [ + { + "placeholder": "note_description", + "value": { + "source": "supplementary_data", + "identifier": "note", + "selectors": ["description"] + } + } + ] + }, + { + "text": "Note Example Title: {note_title}", + "placeholders": [ + { + "placeholder": "note_title", + "value": { + "source": "supplementary_data", + "identifier": "note", + "selectors": ["example", "title"] + } + } + ] + }, + { + "text": "Note Example Description: {note_description}", + "placeholders": [ + { + "placeholder": "note_description", + "value": { + "source": "supplementary_data", + "identifier": "note", + "selectors": ["example", "description"] + } + } + ] + }, + { + "text": "Incorporation Date: {incorporation_date}", + "placeholders": [ + { + "placeholder": "incorporation_date", + "transforms": [ + { + "arguments": { + "date_format": "d MMMM yyyy", + "date_to_format": { + "source": "supplementary_data", + "identifier": "incorporation_date" + } + }, + "transform": "format_date" + } + ] + } + ] + }, + { + "text": "Trading start date: {trading_date}", + "placeholders": [ + { + "placeholder": "trading_date", + "transforms": [ + { + "arguments": { + "date_format": "d MMMM yyyy", + "date_to_format": { + "source": "answers", + "identifier": "trading-answer" + } + }, + "transform": "format_date" + } + ] + } + ] + }, + { + "text": "Guidance: {guidance}", + "placeholders": [ + { + "placeholder": "guidance", + "value": { + "source": "supplementary_data", + "identifier": "guidance" + } + } + ] + }, + { + "text": "Total Uk Sales: {sales}", + "placeholders": [ + { + "placeholder": "sales", + "transforms": [ + { + "transform": "format_currency", + "arguments": { + "number": { + "source": "supplementary_data", + "identifier": "total_uk_sales" + } + } + } + ] + } + ] + }, + { + "text": "Bristol sales: {bristol_sales}", + "placeholders": [ + { + "placeholder": "bristol_sales", + "transforms": [ + { + "transform": "format_currency", + "arguments": { + "number": { + "source": "answers", + "identifier": "sales-bristol-answer" + } + } + } + ] + } + ] + }, + { + "text": "London sales: {london_sales}", + "placeholders": [ + { + "placeholder": "london_sales", + "transforms": [ + { + "transform": "format_currency", + "arguments": { + "number": { + "source": "answers", + "identifier": "sales-london-answer" + } + } + } + ] + } + ] + }, + { + "text": "Sum of Bristol and London sales: {total_sales}", + "placeholders": [ + { + "placeholder": "total_sales", + "transforms": [ + { + "transform": "format_currency", + "arguments": { + "number": { + "source": "calculated_summary", + "identifier": "calculated-summary-sales" + } + } + } + ] + } + ] + } + ] + } + ] + } + } + ] + } + ] + }, + { + "id": "section-2", + "title": "Employees", + "groups": [ + { + "id": "employee-reporting", + "blocks": [ + { + "id": "list-collector-employees", + "type": "ListCollectorContent", + "page_title": "Employees", + "for_list": "employees", + "content": { + "title": "Employees", + "contents": [ + { + "definition": { + "title": "Company employees", + "contents": [ + { + "description": "List of previously reported employees." + } + ] + } + }, + { + "description": "You have previously reported on the above employees. Press continue to proceed to the next section where you can add any additional employees." + } + ] + }, + "summary": { + "title": "employees", + "item_title": { + "text": "{employee_name}", + "placeholders": [ + { + "placeholder": "employee_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "supplementary_data", + "identifier": "employees", + "selectors": ["personal_details", "forename"] + }, + { + "source": "supplementary_data", + "identifier": "employees", + "selectors": ["personal_details", "surname"] + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + } + } + } + ] + } + ] + }, + { + "id": "section-3", + "title": "Additional Employees", + "summary": { + "show_on_completion": true, + "items": [ + { + "type": "List", + "for_list": "additional-employees", + "title": "employees", + "add_link_text": "Add another employee", + "empty_list_text": "There are no employees" + } + ], + "show_non_item_answers": true + }, + "groups": [ + { + "id": "additional-employee-reporting", + "blocks": [ + { + "type": "ListCollectorDrivingQuestion", + "id": "any-additional-employees", + "for_list": "additional-employees", + "question": { + "type": "General", + "id": "any-additional-employee-question", + "title": "Do you have any additional employees to report on?", + "guidance": { + "contents": [ + { + "description": "This uses a different employees list, and the items from this list and the supplementary list will then be used in repeating sections" + } + ] + }, + "answers": [ + { + "type": "Radio", + "id": "any-additional-employee-answer", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock", + "params": { + "block_id": "add-additional-employee", + "list_name": "additional-employees" + } + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "routing_rules": [ + { + "section": "End", + "when": { + "==": [ + { + "source": "answers", + "identifier": "any-additional-employee-answer" + }, + "No" + ] + } + }, + { + "block": "list-collector-additional" + } + ] + }, + { + "id": "list-collector-additional", + "type": "ListCollector", + "for_list": "additional-employees", + "question": { + "id": "confirmation-additional-question", + "type": "General", + "title": "Do you need to add any more employees?", + "answers": [ + { + "id": "list-collector-additional-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "add_block": { + "id": "add-additional-employee", + "type": "ListAddQuestion", + "cancel_text": "Don’t need to add any other employees?", + "question": { + "id": "add-additional-question", + "type": "General", + "title": "What is the name of the employee?", + "answers": [ + { + "id": "employee-first-name", + "label": "First name", + "mandatory": true, + "type": "TextField" + }, + { + "id": "employee-last-name", + "label": "Last name", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "edit_block": { + "id": "edit-additional-employee", + "type": "ListEditQuestion", + "cancel_text": "Don’t need to change anything?", + "question": { + "id": "edit-additional-question", + "type": "General", + "title": "What is the name of the employee?", + "answers": [ + { + "id": "employee-first-name", + "label": "First name", + "mandatory": true, + "type": "TextField" + }, + { + "id": "employee-last-name", + "label": "Last name", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "remove_block": { + "id": "remove-additional-employee", + "type": "ListRemoveQuestion", + "cancel_text": "Don’t need to remove this employee?", + "question": { + "id": "remove-additional-question", + "type": "General", + "title": "Are you sure you want to remove this employee?", + "warning": "All of the information about this employee will be deleted", + "answers": [ + { + "id": "remove-confirmation", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RemoveListItemAndAnswers" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + "summary": { + "title": "employees", + "item_title": { + "text": "{employee_name}", + "placeholders": [ + { + "placeholder": "employee_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "employee-first-name" + }, + { + "source": "answers", + "identifier": "employee-last-name" + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + } + } + } + ] + } + ] + }, + { + "enabled": { + "when": { + "and": [ + { + "==": [ + { + "source": "progress", + "selector": "section", + "identifier": "section-2" + }, + "COMPLETED" + ] + }, + { + "==": [ + { + "source": "progress", + "selector": "section", + "identifier": "section-3" + }, + "COMPLETED" + ] + } + ] + } + }, + "id": "section-4", + "title": "Employee Details", + "summary": { + "show_on_completion": true + }, + "repeat": { + "for_list": "employees", + "title": { + "text": "{employee_name}", + "placeholders": [ + { + "placeholder": "employee_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "supplementary_data", + "identifier": "employees", + "selectors": ["personal_details", "forename"] + }, + { + "source": "supplementary_data", + "identifier": "employees", + "selectors": ["personal_details", "surname"] + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + } + }, + "groups": [ + { + "id": "employee-detail-questions", + "blocks": [ + { + "type": "Question", + "id": "length-of-employment", + "question": { + "id": "length-employment-question", + "type": "General", + "title": { + "text": "When did {employee_name} start working for {company_name}?", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "supplementary_data", + "identifier": "company_name" + } + }, + { + "placeholder": "employee_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "supplementary_data", + "identifier": "employees", + "selectors": ["personal_details", "forename"] + }, + { + "source": "supplementary_data", + "identifier": "employees", + "selectors": ["personal_details", "surname"] + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + }, + "answers": [ + { + "id": "employment-start", + "label": { + "text": "Start date at {company_name}", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "supplementary_data", + "identifier": "company_name" + } + } + ] + }, + "mandatory": true, + "type": "Date", + "maximum": { + "value": "now" + }, + "minimum": { + "value": { + "source": "supplementary_data", + "identifier": "incorporation_date" + } + } + } + ] + } + }, + { + "id": "conditional-employee-block", + "type": "Question", + "skip_conditions": { + "when": { + "!=": [ + { + "count": [ + { + "source": "list", + "identifier": "employees" + } + ] + }, + 3 + ] + } + }, + "question": { + "id": "conditional-employee-question", + "guidance": { + "contents": [ + { + "description": "This block is enabled because there are 3 employees in the supplementary dataset" + } + ] + }, + "type": "General", + "title": { + "text": "Has {employee_name} been promoted since starting at {company_name}?", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "supplementary_data", + "identifier": "company_name" + } + }, + { + "placeholder": "employee_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "supplementary_data", + "identifier": "employees", + "selectors": ["personal_details", "forename"] + }, + { + "source": "supplementary_data", + "identifier": "employees", + "selectors": ["personal_details", "surname"] + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + }, + "answers": [ + { + "id": "promoted-yes-no-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + } + ] + } + ] + }, + { + "enabled": { + "when": { + "and": [ + { + "==": [ + { + "source": "progress", + "selector": "section", + "identifier": "section-2" + }, + "COMPLETED" + ] + }, + { + "==": [ + { + "source": "progress", + "selector": "section", + "identifier": "section-3" + }, + "COMPLETED" + ] + } + ] + } + }, + "id": "section-5", + "title": "Additional Employee Details", + "summary": { + "show_on_completion": true + }, + "repeat": { + "for_list": "additional-employees", + "title": { + "text": "{employee_name}", + "placeholders": [ + { + "placeholder": "employee_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "employee-first-name" + }, + { + "source": "answers", + "identifier": "employee-last-name" + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + } + }, + "groups": [ + { + "id": "additional-employee-detail-questions", + "blocks": [ + { + "type": "Question", + "id": "additional-length-of-employment", + "question": { + "id": "additional-length-employment-question", + "type": "General", + "title": { + "text": "When did {employee_name} start working for {company_name}?", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "supplementary_data", + "identifier": "company_name" + } + }, + { + "placeholder": "employee_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "employee-first-name" + }, + { + "source": "answers", + "identifier": "employee-last-name" + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + }, + "answers": [ + { + "id": "additional-employment-start", + "label": { + "text": "Start date at {company_name}", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "supplementary_data", + "identifier": "company_name" + } + } + ] + }, + "mandatory": true, + "type": "Date", + "maximum": { + "value": "now" + }, + "minimum": { + "value": { + "source": "supplementary_data", + "identifier": "incorporation_date" + } + } + } + ] + } + }, + { + "id": "conditional-additional-employee-block", + "type": "Question", + "skip_conditions": { + "when": { + "!=": [ + { + "count": [ + { + "source": "list", + "identifier": "additional-employees" + } + ] + }, + 3 + ] + } + }, + "question": { + "id": "conditional-additional-employee-question", + "guidance": { + "contents": [ + { + "description": "This block is enabled because there are 3 additional employees" + } + ] + }, + "type": "General", + "title": { + "text": "Has {employee_name} been promoted since starting at {company_name}?", + "placeholders": [ + { + "placeholder": "company_name", + "value": { + "source": "supplementary_data", + "identifier": "company_name" + } + }, + { + "placeholder": "employee_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "employee-first-name" + }, + { + "source": "answers", + "identifier": "employee-last-name" + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + }, + "answers": [ + { + "id": "additional-promoted-yes-no-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + } + ] + } + ] + }, + { + "id": "section-6", + "title": "Product details", + "summary": { + "show_on_completion": true, + "items": [ + { + "type": "List", + "for_list": "products", + "title": "Products", + "empty_list_text": "There are no products" + } + ], + "show_non_item_answers": true + }, + "groups": [ + { + "id": "product-reporting", + "blocks": [ + { + "id": "list-collector-products", + "type": "ListCollectorContent", + "for_list": "products", + "page_title": "Products", + "content": { + "title": "Products", + "contents": [ + { + "description": "You have previously provided information for the above products. Please press continue to proceed to questions on value and volume of sales." + } + ] + }, + "repeating_blocks": [ + { + "id": "product-repeating-block-1", + "type": "ListRepeatingQuestion", + "question": { + "id": "product-repeating-block-1-question", + "type": "General", + "guidance": { + "contents": [ + { + "title": { + "text": "{guidance_include}", + "placeholders": [ + { + "placeholder": "guidance_include", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "source": "supplementary_data", + "identifier": "products", + "selectors": ["guidance_include", "title"] + } + ] + } + } + ] + } + ] + }, + "description": { + "text": "{guidance_include_list}", + "placeholders": [ + { + "placeholder": "guidance_include_list", + "transforms": [ + { + "transform": "format_list", + "arguments": { + "list_to_format": { + "source": "supplementary_data", + "identifier": "products", + "selectors": ["guidance_include", "list"] + } + } + } + ] + } + ] + } + }, + { + "title": { + "text": "{guidance_exclude}", + "placeholders": [ + { + "placeholder": "guidance_exclude", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "source": "supplementary_data", + "identifier": "products", + "selectors": ["guidance_exclude", "title"] + } + ] + } + } + ] + } + ] + }, + "description": { + "text": "{guidance_exclude_list}", + "placeholders": [ + { + "placeholder": "guidance_exclude_list", + "transforms": [ + { + "transform": "format_list", + "arguments": { + "list_to_format": { + "source": "supplementary_data", + "identifier": "products", + "selectors": ["guidance_exclude", "list"] + } + } + } + ] + } + ] + } + } + ] + }, + "title": { + "text": "Volume of production and sales for {product_name}", + "placeholders": [ + { + "placeholder": "product_name", + "value": { + "source": "supplementary_data", + "identifier": "products", + "selectors": ["name"] + } + } + ] + }, + "answers": [ + { + "id": "product-volume-sales", + "label": { + "text": "{volume_sales} for {product_name}", + "placeholders": [ + { + "placeholder": "volume_sales", + "value": { + "source": "supplementary_data", + "identifier": "products", + "selectors": ["volume_sales", "label"] + } + }, + { + "placeholder": "product_name", + "value": { + "source": "supplementary_data", + "identifier": "products", + "selectors": ["name"] + } + } + ] + }, + "mandatory": false, + "type": "Unit", + "unit": "mass-kilogram", + "unit_length": "short" + }, + { + "id": "product-volume-total", + "label": { + "text": "{total_volume} for {product_name}", + "placeholders": [ + { + "placeholder": "total_volume", + "value": { + "source": "supplementary_data", + "identifier": "products", + "selectors": ["total_volume", "label"] + } + }, + { + "placeholder": "product_name", + "value": { + "source": "supplementary_data", + "identifier": "products", + "selectors": ["name"] + } + } + ] + }, + "mandatory": false, + "type": "Unit", + "unit": "mass-kilogram", + "unit_length": "short" + } + ] + } + } + ], + "summary": { + "title": "products", + "item_title": { + "text": "{product_name}", + "placeholders": [ + { + "placeholder": "product_name", + "value": { + "source": "supplementary_data", + "identifier": "products", + "selectors": ["name"] + } + } + ] + } + } + }, + { + "type": "CalculatedSummary", + "id": "calculated-summary-volume-sales", + "title": "We calculate the total volume of sales over the previous quarter to be %(total)s. Is this correct?", + "calculation": { + "title": "Total sales volume", + "operation": { + "+": [ + { + "source": "answers", + "identifier": "product-volume-sales" + } + ] + } + } + }, + { + "type": "CalculatedSummary", + "id": "calculated-summary-volume-total", + "title": "We calculate the total volume produced over the previous quarter to be %(total)s. Is this correct?", + "calculation": { + "title": "Total volume produced", + "operation": { + "+": [ + { + "source": "answers", + "identifier": "product-volume-total" + } + ] + } + } + }, + { + "type": "Question", + "id": "dynamic-products", + "skip_conditions": { + "when": { + "==": [ + { + "count": [ + { + "source": "list", + "identifier": "products" + } + ] + }, + 0 + ] + } + }, + "question": { + "dynamic_answers": { + "values": { + "source": "list", + "identifier": "products" + }, + "answers": [ + { + "label": { + "text": "{value_sales} for {product_name}", + "placeholders": [ + { + "placeholder": "value_sales", + "value": { + "source": "supplementary_data", + "identifier": "products", + "selectors": ["value_sales", "label"] + } + }, + { + "placeholder": "product_name", + "value": { + "source": "supplementary_data", + "identifier": "products", + "selectors": ["name"] + } + } + ] + }, + "id": "product-sales-answer", + "type": "Currency", + "mandatory": true, + "currency": "GBP", + "decimal_places": 2 + } + ] + }, + "answers": [ + { + "id": "extra-static-answer", + "label": "Value of sales from other categories", + "type": "Currency", + "mandatory": false, + "currency": "GBP", + "decimal_places": 2 + } + ], + "id": "dynamic-answer-question", + "title": "Sales during the previous quarter", + "type": "General" + } + }, + { + "type": "CalculatedSummary", + "id": "calculated-summary-value-sales", + "title": "We calculate the total value of sales over the previous quarter to be %(total)s. Is this correct?", + "calculation": { + "title": "Total sales value", + "operation": { + "+": [ + { + "source": "answers", + "identifier": "product-sales-answer" + }, + { + "source": "answers", + "identifier": "extra-static-answer" + } + ] + } + } + } + ] + } + ] + }, + { + "id": "section-7", + "title": "Sales targets", + "groups": [ + { + "id": "value-sales-group", + "blocks": [ + { + "id": "product-sales-interstitial", + "type": "Interstitial", + "content": { + "title": "Product value sales", + "contents": [ + { + "guidance": { + "contents": [ + { + "description": "The next block only shows when there are 2 products in the supplementary dataset." + }, + { + "description": "This is to test that section progress updates when swapping between supplementary datasets which remove or add list items" + } + ] + } + } + ] + } + }, + { + "id": "product-question-2-enabled", + "type": "Question", + "skip_conditions": { + "when": { + "!=": [ + { + "count": [ + { + "source": "list", + "identifier": "products" + } + ] + }, + 2 + ] + } + }, + "question": { + "id": "product-2-question", + "guidance": { + "contents": [ + { + "description": "This block is enabled because there are 2 products in the supplementary dataset" + } + ] + }, + "type": "General", + "title": { + "text": "Did the total value sales of {value_sales} over the last quarter meet the target?", + "placeholders": [ + { + "placeholder": "value_sales", + "transforms": [ + { + "transform": "format_currency", + "arguments": { + "number": { + "source": "calculated_summary", + "identifier": "calculated-summary-value-sales" + } + } + } + ] + } + ] + }, + "answers": [ + { + "id": "value-yes-no-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + } + ] + } + ] + }, + { + "id": "section-8", + "title": "Production targets", + "groups": [ + { + "id": "volume-produced-group", + "blocks": [ + { + "id": "product-volume-interstitial", + "type": "Interstitial", + "content": { + "title": "Product volume produced", + "contents": [ + { + "guidance": { + "contents": [ + { + "description": "The next block only shows when there are 3 products in the supplementary dataset." + }, + { + "description": "This is to test that section progress updates when swapping between supplementary datasets which remove or add list items" + } + ] + } + } + ] + } + }, + { + "id": "product-question-3-enabled", + "type": "Question", + "skip_conditions": { + "when": { + "!=": [ + { + "count": [ + { + "source": "list", + "identifier": "products" + } + ] + }, + 3 + ] + } + }, + "question": { + "id": "product-3-question", + "guidance": { + "contents": [ + { + "description": "This block is enabled because there are 3 products in the supplementary dataset" + } + ] + }, + "type": "General", + "title": { + "text": "Did the total volume produced of {volume_produced} over the last quarter meet the target?", + "placeholders": [ + { + "placeholder": "volume_produced", + "transforms": [ + { + "transform": "format_unit", + "arguments": { + "value": { + "source": "calculated_summary", + "identifier": "calculated-summary-volume-total" + }, + "unit": "mass-kilogram", + "unit_length": "short" + } + } + ] + } + ] + }, + "answers": [ + { + "id": "volume-yes-no-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + } + ] + } + ] + } + ] +} diff --git a/schemas/test/en/test_textarea.json b/schemas/test/en/test_textarea.json index 0eef1f36a3..5029fbace6 100644 --- a/schemas/test/en/test_textarea.json +++ b/schemas/test/en/test_textarea.json @@ -50,7 +50,6 @@ "label": "Enter your comments", "rows": 3, "mandatory": false, - "q_code": "0", "type": "TextArea", "max_length": 20, "validation": { diff --git a/schemas/test/en/test_textfield.json b/schemas/test/en/test_textfield.json index 8458c770ca..8ca7bbc3e1 100644 --- a/schemas/test/en/test_textfield.json +++ b/schemas/test/en/test_textfield.json @@ -50,7 +50,6 @@ "label": "What is your name?", "max_length": 20, "mandatory": false, - "q_code": "0", "type": "TextField" } ], diff --git a/schemas/test/en/test_thank_you_census_household.json b/schemas/test/en/test_theme_dbt.json similarity index 50% rename from schemas/test/en/test_thank_you_census_household.json rename to schemas/test/en/test_theme_dbt.json index 00d23e749d..be7981734c 100644 --- a/schemas/test/en/test_thank_you_census_household.json +++ b/schemas/test/en/test_theme_dbt.json @@ -4,11 +4,9 @@ "schema_version": "0.0.1", "data_version": "0.0.3", "survey_id": "0", - "form_type": "H", - "region_code": "GB-WLS", - "title": "Census household test schema", - "theme": "census", - "description": "A questionnaire to test the thank you page for census household", + "title": "Test Department for Business and Trade", + "theme": "dbt", + "description": "A questionnaire to demo the DBT survey theme", "metadata": [ { "name": "user_id", @@ -19,48 +17,57 @@ "type": "string" }, { - "name": "display_address", + "name": "ru_name", "type": "string" } ], "questionnaire_flow": { - "type": "Hub", - "options": { "required_completed_sections": ["household-section"] } + "type": "Linear", + "options": { + "summary": { + "collapsible": false + } + } }, "sections": [ { - "id": "household-section", + "id": "section", "groups": [ { "blocks": [ { "type": "Question", - "id": "household-confirmation", + "id": "radio", "question": { "answers": [ { - "type": "Radio", - "id": "household-confirmation-answer", + "id": "radio-answer", "mandatory": false, "options": [ { - "label": "Yes", - "value": "Yes" + "label": "Bacon", + "value": "Bacon" + }, + { + "label": "Eggs", + "value": "Eggs" }, { - "label": "No", - "value": "No" + "label": "Sausage", + "value": "Sausage" } - ] + ], + "type": "Radio" } ], - "id": "household-confirmation-question", - "title": "Are you aware this is an census household test schema?", + "id": "radio-question", + "title": "What is your favourite breakfast food?", "type": "General" } } ], - "id": "household-group" + "id": "group", + "title": "DBT Theme Test" } ] } diff --git a/schemas/test/en/test_theme_dbt_dsit.json b/schemas/test/en/test_theme_dbt_dsit.json new file mode 100644 index 0000000000..0395f918d4 --- /dev/null +++ b/schemas/test/en/test_theme_dbt_dsit.json @@ -0,0 +1,75 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Test Department for Business and Trade and Department for Science, Innovation and Technology", + "theme": "dbt-dsit", + "description": "A questionnaire to demo the DBT-DSIT survey theme", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Linear", + "options": { + "summary": { + "collapsible": false + } + } + }, + "sections": [ + { + "id": "section", + "groups": [ + { + "blocks": [ + { + "type": "Question", + "id": "radio", + "question": { + "answers": [ + { + "id": "radio-answer", + "mandatory": false, + "options": [ + { + "label": "Bacon", + "value": "Bacon" + }, + { + "label": "Eggs", + "value": "Eggs" + }, + { + "label": "Sausage", + "value": "Sausage" + } + ], + "type": "Radio" + } + ], + "id": "radio-question", + "title": "What is your favourite breakfast food?", + "type": "General" + } + } + ], + "id": "group", + "title": "DBT-DSIT Theme Test" + } + ] + } + ] +} diff --git a/schemas/test/en/test_theme_dbt_dsit_ni.json b/schemas/test/en/test_theme_dbt_dsit_ni.json new file mode 100644 index 0000000000..8a5cf9c642 --- /dev/null +++ b/schemas/test/en/test_theme_dbt_dsit_ni.json @@ -0,0 +1,75 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Test NI Department for Business and Trade and Department for Science, Innovation and Technology", + "theme": "dbt-dsit-ni", + "description": "A questionnaire to demo the DBT-DSIT-NI survey theme", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Linear", + "options": { + "summary": { + "collapsible": false + } + } + }, + "sections": [ + { + "id": "section", + "groups": [ + { + "blocks": [ + { + "type": "Question", + "id": "radio", + "question": { + "answers": [ + { + "id": "radio-answer", + "mandatory": false, + "options": [ + { + "label": "Bacon", + "value": "Bacon" + }, + { + "label": "Eggs", + "value": "Eggs" + }, + { + "label": "Sausage", + "value": "Sausage" + } + ], + "type": "Radio" + } + ], + "id": "radio-question", + "title": "What is your favourite breakfast food?", + "type": "General" + } + } + ], + "id": "group", + "title": "DBT-DSIT-NI Theme Test" + } + ] + } + ] +} diff --git a/schemas/test/en/test_theme_dbt_ni.json b/schemas/test/en/test_theme_dbt_ni.json new file mode 100644 index 0000000000..b8cf5716d0 --- /dev/null +++ b/schemas/test/en/test_theme_dbt_ni.json @@ -0,0 +1,75 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Test NI Department for Business and Trade", + "theme": "dbt-ni", + "description": "A questionnaire to demo the DBT-NI survey theme", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Linear", + "options": { + "summary": { + "collapsible": false + } + } + }, + "sections": [ + { + "id": "section", + "groups": [ + { + "blocks": [ + { + "type": "Question", + "id": "radio", + "question": { + "answers": [ + { + "id": "radio-answer", + "mandatory": false, + "options": [ + { + "label": "Bacon", + "value": "Bacon" + }, + { + "label": "Eggs", + "value": "Eggs" + }, + { + "label": "Sausage", + "value": "Sausage" + } + ], + "type": "Radio" + } + ], + "id": "radio-question", + "title": "What is your favourite breakfast food?", + "type": "General" + } + } + ], + "id": "group", + "title": "DBT-NI Theme Test" + } + ] + } + ] +} diff --git a/schemas/test/en/test_theme_desnz.json b/schemas/test/en/test_theme_desnz.json new file mode 100644 index 0000000000..c1aa83755f --- /dev/null +++ b/schemas/test/en/test_theme_desnz.json @@ -0,0 +1,75 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Test Department for Energy Security and Net Zero", + "theme": "desnz", + "description": "A questionnaire to demo the DESNZ survey theme", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Linear", + "options": { + "summary": { + "collapsible": false + } + } + }, + "sections": [ + { + "id": "section", + "groups": [ + { + "blocks": [ + { + "type": "Question", + "id": "radio", + "question": { + "answers": [ + { + "id": "radio-answer", + "mandatory": false, + "options": [ + { + "label": "Bacon", + "value": "Bacon" + }, + { + "label": "Eggs", + "value": "Eggs" + }, + { + "label": "Sausage", + "value": "Sausage" + } + ], + "type": "Radio" + } + ], + "id": "radio-question", + "title": "What is your favourite breakfast food?", + "type": "General" + } + } + ], + "id": "group", + "title": "DESNZ Theme Test" + } + ] + } + ] +} diff --git a/schemas/test/en/test_theme_desnz_ni.json b/schemas/test/en/test_theme_desnz_ni.json new file mode 100644 index 0000000000..06751903f5 --- /dev/null +++ b/schemas/test/en/test_theme_desnz_ni.json @@ -0,0 +1,75 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Test NI Department for Energy Security and Net Zero", + "theme": "desnz-ni", + "description": "A questionnaire to demo the DESNZ-NI survey theme", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Linear", + "options": { + "summary": { + "collapsible": false + } + } + }, + "sections": [ + { + "id": "section", + "groups": [ + { + "blocks": [ + { + "type": "Question", + "id": "radio", + "question": { + "answers": [ + { + "id": "radio-answer", + "mandatory": false, + "options": [ + { + "label": "Bacon", + "value": "Bacon" + }, + { + "label": "Eggs", + "value": "Eggs" + }, + { + "label": "Sausage", + "value": "Sausage" + } + ], + "type": "Radio" + } + ], + "id": "radio-question", + "title": "What is your favourite breakfast food?", + "type": "General" + } + } + ], + "id": "group", + "title": "DESNZ-NI Theme Test" + } + ] + } + ] +} diff --git a/schemas/test/en/test_theme_health.json b/schemas/test/en/test_theme_health.json new file mode 100644 index 0000000000..e88814e7c6 --- /dev/null +++ b/schemas/test/en/test_theme_health.json @@ -0,0 +1,70 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Test Health Survey", + "theme": "health", + "description": "A questionnaire to demo the health survey theme", + "metadata": [ + { + "name": "qid", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Linear", + "options": { + "summary": { + "collapsible": false + } + } + }, + "post_submission": { + "view_response": true + }, + "sections": [ + { + "id": "section", + "groups": [ + { + "blocks": [ + { + "type": "Question", + "id": "radio", + "question": { + "answers": [ + { + "id": "radio-answer", + "mandatory": false, + "options": [ + { + "label": "Bacon", + "value": "Bacon" + }, + { + "label": "Eggs", + "value": "Eggs" + }, + { + "label": "Sausage", + "value": "Sausage" + } + ], + "type": "Radio" + } + ], + "id": "radio-question", + "title": "What is your favourite breakfast food?", + "type": "General" + } + } + ], + "id": "group", + "title": "Health Theme Test" + } + ] + } + ] +} diff --git a/schemas/test/en/test_theme_northernireland.json b/schemas/test/en/test_theme_northernireland.json index 8b1f02fba7..569698acb7 100644 --- a/schemas/test/en/test_theme_northernireland.json +++ b/schemas/test/en/test_theme_northernireland.json @@ -57,7 +57,6 @@ "value": "Sausage" } ], - "q_code": "20", "type": "Radio" } ], diff --git a/schemas/test/en/test_thank_you_census_individual.json b/schemas/test/en/test_theme_ons_nhs.json similarity index 55% rename from schemas/test/en/test_thank_you_census_individual.json rename to schemas/test/en/test_theme_ons_nhs.json index 34ef182247..e733508d94 100644 --- a/schemas/test/en/test_thank_you_census_individual.json +++ b/schemas/test/en/test_theme_ons_nhs.json @@ -4,11 +4,9 @@ "schema_version": "0.0.1", "data_version": "0.0.3", "survey_id": "0", - "form_type": "I", - "region_code": "GB-WLS", - "title": "Census individual test schema", - "theme": "census", - "description": "A questionnaire to test the thank you page for census individual", + "title": "Test ONS NHS Survey", + "theme": "ons-nhs", + "description": "A questionnaire to demo the ons-nhs survey theme", "metadata": [ { "name": "user_id", @@ -19,7 +17,7 @@ "type": "string" }, { - "name": "display_address", + "name": "ru_name", "type": "string" } ], @@ -33,38 +31,43 @@ }, "sections": [ { - "id": "individual-section", + "id": "section", "groups": [ { "blocks": [ { "type": "Question", - "id": "individual-confirmation", + "id": "radio", "question": { "answers": [ { - "type": "Radio", - "id": "individual-confirmation-answer", + "id": "radio-answer", "mandatory": false, "options": [ { - "label": "Yes", - "value": "Yes" + "label": "Bacon", + "value": "Bacon" }, { - "label": "No", - "value": "No" + "label": "Eggs", + "value": "Eggs" + }, + { + "label": "Sausage", + "value": "Sausage" } - ] + ], + "type": "Radio" } ], - "id": "individual-confirmation-question", - "title": "Are you aware this is an census individual test schema?", + "id": "radio-question", + "title": "What is your favourite breakfast food?", "type": "General" } } ], - "id": "individual-group" + "id": "group", + "title": "ONS NHS Theme Test" } ] } diff --git a/schemas/test/en/test_thank_you_census_communal_establishment.json b/schemas/test/en/test_theme_orr.json similarity index 56% rename from schemas/test/en/test_thank_you_census_communal_establishment.json rename to schemas/test/en/test_theme_orr.json index f689e86667..15aad627c3 100644 --- a/schemas/test/en/test_thank_you_census_communal_establishment.json +++ b/schemas/test/en/test_theme_orr.json @@ -4,11 +4,9 @@ "schema_version": "0.0.1", "data_version": "0.0.3", "survey_id": "0", - "form_type": "C", - "region_code": "GB-WLS", - "title": "Census communal establishment test schema", - "theme": "census", - "description": "A questionnaire to test the thank you page for communal establishment", + "title": "Test Rail and Road Survey", + "theme": "orr", + "description": "A questionnaire to demo the ORR survey theme", "metadata": [ { "name": "user_id", @@ -19,7 +17,7 @@ "type": "string" }, { - "name": "display_address", + "name": "ru_name", "type": "string" } ], @@ -33,38 +31,43 @@ }, "sections": [ { - "id": "communal-establishment-section", + "id": "section", "groups": [ { "blocks": [ { "type": "Question", - "id": "communal-establishment-confirmation", + "id": "radio", "question": { "answers": [ { - "type": "Radio", - "id": "communal-establishment-confirmation-answer", + "id": "radio-answer", "mandatory": false, "options": [ { - "label": "Yes", - "value": "Yes" + "label": "Bacon", + "value": "Bacon" }, { - "label": "No", - "value": "No" + "label": "Eggs", + "value": "Eggs" + }, + { + "label": "Sausage", + "value": "Sausage" } - ] + ], + "type": "Radio" } ], - "id": "communal-establishment-confirmation-question", - "title": "Are you aware this is an census communal establishment test schema?", + "id": "radio-question", + "title": "What is your favourite breakfast food?", "type": "General" } } ], - "id": "communal-establishment-group" + "id": "group", + "title": "ORR Theme Test" } ] } diff --git a/schemas/test/en/test_theme_social.json b/schemas/test/en/test_theme_social.json index 4f94b88b2a..689a74f0b9 100644 --- a/schemas/test/en/test_theme_social.json +++ b/schemas/test/en/test_theme_social.json @@ -9,18 +9,14 @@ "description": "A questionnaire to demo the social survey theme", "metadata": [ { - "name": "user_id", - "type": "string" - }, - { - "name": "period_id", - "type": "string" - }, - { - "name": "ru_name", + "name": "qid", "type": "string" } ], + "post_submission": { + "view_response": true, + "feedback": true + }, "questionnaire_flow": { "type": "Linear", "options": { @@ -57,7 +53,6 @@ "value": "Sausage" } ], - "q_code": "20", "type": "Radio" } ], diff --git a/schemas/test/en/test_theme_ukhsa_ons.json b/schemas/test/en/test_theme_ukhsa_ons.json new file mode 100644 index 0000000000..9c72466d79 --- /dev/null +++ b/schemas/test/en/test_theme_ukhsa_ons.json @@ -0,0 +1,75 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Test UKHSA ONS Survey", + "theme": "ukhsa-ons", + "description": "A questionnaire to demo the UKHSA ONS survey theme", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Linear", + "options": { + "summary": { + "collapsible": false + } + } + }, + "sections": [ + { + "id": "section", + "groups": [ + { + "blocks": [ + { + "type": "Question", + "id": "radio", + "question": { + "answers": [ + { + "id": "radio-answer", + "mandatory": false, + "options": [ + { + "label": "Bacon", + "value": "Bacon" + }, + { + "label": "Eggs", + "value": "Eggs" + }, + { + "label": "Sausage", + "value": "Sausage" + } + ], + "type": "Radio" + } + ], + "id": "radio-question", + "title": "What is your favourite breakfast food?", + "type": "General" + } + } + ], + "id": "group", + "title": "UKHSA ONS Theme Test" + } + ] + } + ] +} diff --git a/schemas/test/en/test_timeout.json b/schemas/test/en/test_timeout.json index 46c3fd45ed..85e23ee42c 100644 --- a/schemas/test/en/test_timeout.json +++ b/schemas/test/en/test_timeout.json @@ -47,7 +47,6 @@ "id": "timeout-answer", "label": "Does the timeout appear?", "mandatory": false, - "q_code": "0", "type": "TextField" } ], diff --git a/schemas/test/en/test_titles_radio_and_checkbox.json b/schemas/test/en/test_titles_radio_and_checkbox.json index bb2f23760c..e40074c8db 100644 --- a/schemas/test/en/test_titles_radio_and_checkbox.json +++ b/schemas/test/en/test_titles_radio_and_checkbox.json @@ -69,17 +69,19 @@ "id": "checkbox-block", "question_variants": [ { - "when": [ - { - "id": "name-answer", - "condition": "equals", - "value": "Peter" - } - ], + "when": { + "==": [ + { + "source": "answers", + "identifier": "name-answer" + }, + "Peter" + ] + }, "question": { "id": "checkbox-question", "type": "General", - "title": "Did Peter make changes to this business?", + "title": "Did Peter make changes to this business?", "answers": [ { "id": "checkbox-answer", @@ -108,17 +110,19 @@ } }, { - "when": [ - { - "id": "name-answer", - "condition": "equals", - "value": "Mary" - } - ], + "when": { + "==": [ + { + "source": "answers", + "identifier": "name-answer" + }, + "Mary" + ] + }, "question": { "id": "checkbox-question", "type": "General", - "title": "Did Mary make changes to this business?", + "title": "Did Mary make changes to this business?", "answers": [ { "id": "checkbox-answer", @@ -147,13 +151,15 @@ } }, { - "when": [ - { - "id": "name-answer", - "condition": "not equals", - "value": "Mary" - } - ], + "when": { + "!=": [ + { + "source": "answers", + "identifier": "name-answer" + }, + "Mary" + ] + }, "question": { "id": "checkbox-question", "type": "General", @@ -192,17 +198,19 @@ "id": "radio-block", "question_variants": [ { - "when": [ - { - "id": "name-answer", - "condition": "equals", - "value": "Peter" - } - ], + "when": { + "==": [ + { + "source": "answers", + "identifier": "name-answer" + }, + "Peter" + ] + }, "question": { "id": "radio-question", "type": "General", - "title": "Is Peter the boss?", + "title": "Is Peter the boss?", "answers": [ { "id": "radio-answer", @@ -235,17 +243,19 @@ } }, { - "when": [ - { - "id": "name-answer", - "condition": "equals", - "value": "Mary" - } - ], + "when": { + "==": [ + { + "source": "answers", + "identifier": "name-answer" + }, + "Mary" + ] + }, "question": { "id": "radio-question", "type": "General", - "title": "Is Mary the boss?", + "title": "Is Mary the boss?", "answers": [ { "id": "radio-answer", @@ -278,18 +288,20 @@ } }, { - "when": [ - { - "id": "name-answer", - "condition": "not equals", - "value": "Mary" - } - ], + "when": { + "!=": [ + { + "source": "answers", + "identifier": "name-answer" + }, + "Mary" + ] + }, "question": { "id": "radio-question", "type": "General", "title": { - "text": "Is {name} the boss?", + "text": "Is {name} the boss?", "placeholders": [ { "placeholder": "name", diff --git a/schemas/test/en/test_unit_patterns.json b/schemas/test/en/test_unit_patterns.json index fe44283bd6..d795d95653 100644 --- a/schemas/test/en/test_unit_patterns.json +++ b/schemas/test/en/test_unit_patterns.json @@ -83,6 +83,17 @@ "maximum": { "value": 1000 } + }, + { + "id": "min-max-miles", + "label": "Length in Miles - Min Max Range", + "mandatory": false, + "type": "Unit", + "unit": "length-mile", + "unit_length": "short", + "minimum": { + "value": -99999999999999 + } } ], "id": "set-length-units-question", @@ -193,7 +204,8 @@ "mandatory": false, "type": "Unit", "unit": "volume-cubic-centimeter", - "unit_length": "short" + "unit_length": "short", + "decimal_places": 6 }, { "id": "cubic-metres", @@ -201,7 +213,8 @@ "mandatory": false, "type": "Unit", "unit": "volume-cubic-meter", - "unit_length": "short" + "unit_length": "short", + "decimal_places": 6 }, { "id": "litres", @@ -209,7 +222,8 @@ "mandatory": false, "type": "Unit", "unit": "volume-liter", - "unit_length": "short" + "unit_length": "short", + "decimal_places": 6 }, { "id": "hectolitres", @@ -217,7 +231,8 @@ "mandatory": false, "type": "Unit", "unit": "volume-hectoliter", - "unit_length": "short" + "unit_length": "short", + "decimal_places": 6 }, { "id": "megalitres", @@ -225,13 +240,33 @@ "mandatory": false, "type": "Unit", "unit": "volume-megaliter", - "unit_length": "short" + "unit_length": "short", + "decimal_places": 6 } ], "id": "set-volume-unit-questions", "title": "Volume Units", "type": "General" } + }, + { + "type": "Question", + "id": "set-weight-units-block", + "question": { + "answers": [ + { + "id": "mass-tonne", + "label": "Mass tonnes", + "mandatory": false, + "type": "Unit", + "unit": "mass-tonne", + "unit_length": "short" + } + ], + "id": "set-weight-unit-questions", + "title": "Weight Units", + "type": "General" + } } ], "id": "test" diff --git a/schemas/test/en/test_validation_sum_against_total_dynamic_answers.json b/schemas/test/en/test_validation_sum_against_total_dynamic_answers.json new file mode 100644 index 0000000000..b2ec85409f --- /dev/null +++ b/schemas/test/en/test_validation_sum_against_total_dynamic_answers.json @@ -0,0 +1,470 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "A test schema to validate a sum of dynamic answers are equal to a given total", + "theme": "default", + "description": "A survey that tests calculated answers against a total", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + + "questionnaire_flow": { + "type": "Hub", + "options": { + "required_completed_sections": ["total-section"] + } + }, + "sections": [ + { + "id": "total-section", + "title": "Total", + "groups": [ + { + "blocks": [ + { + "type": "Question", + "id": "total-block", + "question": { + "guidance": { + "contents": [ + { + "description": "Answer will be used for validation in the next section." + } + ] + }, + "id": "total-question", + "title": "What percentage of your shopping do you do at supermarkets?", + "type": "General", + "answers": [ + { + "id": "total-answer", + "label": "Total", + "mandatory": true, + "type": "Percentage", + "maximum": { + "value": 100 + }, + "decimal_places": 0 + } + ] + } + } + ], + "id": "total-group" + } + ] + }, + { + "id": "dynamic-answers-section", + "title": "Supermarkets", + "summary": { + "show_on_completion": true, + "items": [ + { + "type": "List", + "for_list": "supermarkets", + "title": "Household members", + "add_link_text": "Add another supermarket", + "empty_list_text": "There are no supermarkets" + } + ], + "show_non_item_answers": true + }, + "groups": [ + { + "id": "group", + "blocks": [ + { + "type": "ListCollectorDrivingQuestion", + "id": "any-supermarket", + "for_list": "supermarkets", + "question": { + "type": "General", + "id": "any-supermarket-question", + "title": "Do you need to add any supermarkets?", + "answers": [ + { + "type": "Radio", + "id": "any-supermarket-answer", + "mandatory": true, + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock", + "params": { + "block_id": "add-supermarket", + "list_name": "supermarkets" + } + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "routing_rules": [ + { + "section": "End", + "when": { + "==": [ + { + "source": "answers", + "identifier": "any-supermarket-answer" + }, + "No" + ] + } + }, + { + "block": "list-collector" + } + ] + }, + { + "id": "list-collector", + "type": "ListCollector", + "for_list": "supermarkets", + "question": { + "id": "confirmation-question", + "type": "General", + "title": "Do you need to add any more supermarkets?", + "answers": [ + { + "id": "list-collector-answer", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "add_block": { + "id": "add-supermarket", + "type": "ListAddQuestion", + "cancel_text": "Don’t need to add any other supermarkets?", + "question": { + "guidance": { + "contents": [ + { + "description": "Maximum spending value will be used for each supermarket’s max spending validation and placeholders." + } + ] + }, + "id": "add-question", + "type": "General", + "title": "Which supermarkets do you use for your weekly shopping?", + "answers": [ + { + "id": "supermarket-name", + "label": "Supermarket", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "edit_block": { + "id": "edit-supermarket", + "type": "ListEditQuestion", + "cancel_text": "Don’t need to change anything?", + "question": { + "id": "edit-question", + "type": "General", + "title": "What is the name of the supermarket?", + "answers": [ + { + "id": "supermarket-name", + "label": "Supermarket", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "remove_block": { + "id": "remove-supermarket", + "type": "ListRemoveQuestion", + "cancel_text": "Don’t need to remove this supermarket?", + "question": { + "id": "remove-question", + "type": "General", + "title": "Are you sure you want to remove this supermarket?", + "warning": "All of the information about this supermarket will be deleted", + "answers": [ + { + "id": "remove-confirmation", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RemoveListItemAndAnswers" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + "summary": { + "title": "Supermarkets", + "item_title": { + "text": "{supermarket_name}", + "placeholders": [ + { + "placeholder": "supermarket_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "supermarket-name" + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + } + } + }, + { + "type": "Question", + "id": "dynamic-answer", + "skip_conditions": { + "when": { + "==": [ + { + "count": [ + { + "source": "list", + "identifier": "supermarkets" + } + ] + }, + 0 + ] + } + }, + "question": { + "guidance": { + "contents": [ + { + "description": "Answers are validated against total percentage from previous section." + } + ] + }, + "dynamic_answers": { + "values": { + "source": "list", + "identifier": "supermarkets" + }, + "answers": [ + { + "label": { + "text": "Percentage of shopping at {transformed_value}", + "placeholders": [ + { + "placeholder": "transformed_value", + "value": { + "source": "answers", + "identifier": "supermarket-name" + } + } + ] + }, + "id": "percentage-of-shopping", + "mandatory": false, + "type": "Percentage", + "maximum": { + "value": 100 + }, + "decimal_places": 0 + } + ] + }, + "answers": [ + { + "label": { + "text": "Percentage of shopping elsewhere", + "placeholders": [ + { + "placeholder": "transformed_value", + "value": { + "source": "answers", + "identifier": "supermarket-name" + } + } + ] + }, + "id": "percentage-of-shopping-elsewhere", + "mandatory": false, + "type": "Percentage", + "maximum": { + "value": 100 + }, + "decimal_places": 0 + } + ], + "warning": "These answers must add up to the total provided in the previous section", + "calculations": [ + { + "calculation_type": "sum", + "answer_id": "total-answer", + "answers_to_calculate": ["percentage-of-shopping-elsewhere", "percentage-of-shopping"], + "conditions": ["equals"] + } + ], + "id": "dynamic-answer-question", + "title": "What percent of your shopping do you do at each of the following supermarket?", + "type": "Calculated" + } + }, + { + "type": "Question", + "id": "total-block-other", + "question": { + "guidance": { + "contents": [ + { + "description": "Answer will be used for validation in the next question." + } + ] + }, + "id": "total-question-other", + "title": "Total amount you spend", + "type": "General", + "answers": [ + { + "id": "total-answer-other", + "label": "Total", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "Question", + "id": "dynamic-answer-only", + "skip_conditions": { + "when": { + "==": [ + { + "count": [ + { + "source": "list", + "identifier": "supermarkets" + } + ] + }, + 0 + ] + } + }, + "question": { + "guidance": { + "contents": [ + { + "description": "Answers are validated against total amount from previous question." + } + ] + }, + "dynamic_answers": { + "values": { + "source": "list", + "identifier": "supermarkets" + }, + "answers": [ + { + "label": { + "text": "How much do you spend at {transformed_value}", + "placeholders": [ + { + "placeholder": "transformed_value", + "value": { + "source": "answers", + "identifier": "supermarket-name" + } + } + ] + }, + "id": "spending-amount", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2, + "maximum": { + "value": { + "source": "answers", + "identifier": "total-answer-other" + } + }, + "minimum": { + "value": 0 + } + } + ] + }, + "warning": "These answers must add up to the total provided in the previous question", + "calculations": [ + { + "calculation_type": "sum", + "answer_id": "total-answer-other", + "answers_to_calculate": ["spending-amount"], + "conditions": ["equals"] + } + ], + "id": "dynamic-answer-only-question", + "title": "How much do you spend at each of the following supermarket?", + "type": "Calculated" + } + } + ] + } + ] + } + ] +} diff --git a/schemas/test/en/test_validation_sum_against_total_hub_with_dependent_section.json b/schemas/test/en/test_validation_sum_against_total_hub_with_dependent_section.json index 3beb84110c..7001c9d763 100644 --- a/schemas/test/en/test_validation_sum_against_total_hub_with_dependent_section.json +++ b/schemas/test/en/test_validation_sum_against_total_hub_with_dependent_section.json @@ -138,7 +138,7 @@ "question": { "id": "turnover-breakdown-question", "title": { - "text": "Please breakdown your total turnover of {total_turnover}", + "text": "Please breakdown your total turnover of {total_turnover}", "placeholders": [ { "placeholder": "total_turnover", @@ -200,7 +200,7 @@ "question": { "id": "employees-breakdown-question", "title": { - "text": "Please breakdown your number of employees of {total_employees}", + "text": "Please breakdown your number of employees of {total_employees}", "placeholders": [ { "placeholder": "total_employees", diff --git a/schemas/test/en/test_validation_sum_against_total_repeating_with_dependent_section.json b/schemas/test/en/test_validation_sum_against_total_repeating_with_dependent_section.json index eb92271169..faa5d77f3f 100644 --- a/schemas/test/en/test_validation_sum_against_total_repeating_with_dependent_section.json +++ b/schemas/test/en/test_validation_sum_against_total_repeating_with_dependent_section.json @@ -219,7 +219,7 @@ "id": "total-spending-block", "question": { "id": "total-spending-question", - "title": "What is the maximum spending limit for a household member per month?", + "title": "What is the maximum spending limit for a household member per month (excluding entertainment)?", "type": "General", "answers": [ { @@ -236,6 +236,29 @@ } ] } + }, + { + "type": "Question", + "id": "entertainment-spending-block", + "question": { + "id": "entertainment-spending-question", + "title": "What is the maximum spending limit on entertainment for a household member per month?", + "type": "General", + "answers": [ + { + "id": "entertainment-spending-answer", + "label": "Entertaintment spending", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2, + "minimum": { + "value": 0, + "exclusive": true + } + } + ] + } } ] } @@ -341,7 +364,7 @@ "question": { "id": "spending-breakdown-question", "title": { - "text": "How do you spending the monthly budget of {total_spending}?", + "text": "How do you spending the monthly budget of {total_spending}?", "placeholders": [ { "placeholder": "total_spending", @@ -372,7 +395,7 @@ "answers": [ { "id": "spending-breakdown-1", - "label": "Entertaintment", + "label": "Housing", "mandatory": false, "type": "Currency", "currency": "GBP", @@ -380,7 +403,7 @@ }, { "id": "spending-breakdown-2", - "label": "Shopping", + "label": "Transportation", "mandatory": false, "type": "Currency", "currency": "GBP", @@ -388,6 +411,84 @@ }, { "id": "spending-breakdown-3", + "label": "Loans", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "Question", + "id": "second-spending-breakdown-block", + "question": { + "id": "second-spending-breakdown-question", + "title": { + "text": "How do you spend the monthly entertainment budget of {entertainment}?", + "placeholders": [ + { + "placeholder": "entertainment", + "transforms": [ + { + "transform": "format_currency", + "arguments": { + "number": { + "source": "answers", + "identifier": "entertainment-spending-answer" + } + } + } + ] + } + ] + }, + "type": "Calculated", + "warning": "These answers must add up to the entertainment budget provided in the spending breakdown question", + "calculations": [ + { + "calculation_type": "sum", + "value": { + "source": "answers", + "identifier": "entertainment-spending-answer" + }, + "answers_to_calculate": [ + "second-spending-breakdown-1", + "second-spending-breakdown-2", + "second-spending-breakdown-3", + "second-spending-breakdown-4" + ], + "conditions": ["equals"] + } + ], + "answers": [ + { + "id": "second-spending-breakdown-1", + "label": "Cinema", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + }, + { + "id": "second-spending-breakdown-2", + "label": "Concerts", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + }, + { + "id": "second-spending-breakdown-3", + "label": "Sporting events", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + }, + { + "id": "second-spending-breakdown-4", "label": "Other", "mandatory": false, "type": "Currency", diff --git a/schemas/test/en/test_validation_sum_against_value_source.json b/schemas/test/en/test_validation_sum_against_value_source.json new file mode 100644 index 0000000000..b394a70198 --- /dev/null +++ b/schemas/test/en/test_validation_sum_against_value_source.json @@ -0,0 +1,190 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Calculated question with value sources test survey", + "theme": "default", + "description": "A survey that tests validation against value sources", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Linear", + "options": { + "summary": { + "collapsible": false + } + } + }, + "sections": [ + { + "id": "default-section", + "groups": [ + { + "id": "group", + "title": "Validate sum against answer, calculated summary source", + "blocks": [ + { + "type": "Question", + "id": "total-block", + "question": { + "id": "total-question", + "title": "Total", + "description": ["Enter a number to breakdown in subsequent questions and calculated summary."], + "type": "General", + "answers": [ + { + "id": "total-answer", + "label": "Total", + "mandatory": true, + "type": "Number", + "decimal_places": 2, + "minimum": { + "value": 0, + "exclusive": true + } + } + ] + } + }, + { + "type": "Question", + "id": "breakdown-block", + "question": { + "id": "breakdown-question", + "title": "Breakdown validated against an answer value source", + "description": ["This is a breakdown of the total number from the previous question."], + "type": "Calculated", + "calculations": [ + { + "calculation_type": "sum", + "value": { + "source": "answers", + "identifier": "total-answer" + }, + "answers_to_calculate": ["breakdown-1", "breakdown-2", "breakdown-3", "breakdown-4"], + "conditions": ["equals"] + } + ], + "answers": [ + { + "id": "breakdown-1", + "label": "Breakdown 1", + "mandatory": false, + "decimal_places": 2, + "type": "Number" + }, + { + "id": "breakdown-2", + "label": "Breakdown 2", + "mandatory": false, + "decimal_places": 2, + "type": "Number" + }, + { + "id": "breakdown-3", + "label": "Breakdown 3", + "mandatory": false, + "decimal_places": 2, + "type": "Number" + }, + { + "id": "breakdown-4", + "label": "Breakdown 4", + "mandatory": false, + "decimal_places": 2, + "type": "Number" + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "number-total-playback", + "title": "We calculate the total of number values entered to be %(total)s. Is this correct?", + "calculation": { + "calculation_type": "sum", + "answers_to_calculate": ["breakdown-1", "breakdown-2"], + "title": "Grand total of previous values" + } + }, + { + "type": "Question", + "id": "second-breakdown-block", + "question": { + "id": "second-breakdown-question", + "title": "Breakdown validated against calculated summary value source", + "description": ["This is a breakdown of the grand total from the previous calculated summary."], + "type": "Calculated", + "calculations": [ + { + "calculation_type": "sum", + "value": { + "source": "calculated_summary", + "identifier": "number-total-playback" + }, + "answers_to_calculate": ["second-breakdown-1", "second-breakdown-2", "second-breakdown-3", "second-breakdown-4"], + "conditions": ["equals"] + } + ], + "answers": [ + { + "id": "second-breakdown-1", + "label": "Breakdown 1", + "mandatory": false, + "decimal_places": 2, + "type": "Number" + }, + { + "id": "second-breakdown-2", + "label": "Breakdown 2", + "mandatory": false, + "decimal_places": 2, + "type": "Number" + }, + { + "id": "second-breakdown-3", + "label": "Breakdown 3", + "mandatory": false, + "decimal_places": 2, + "type": "Number" + }, + { + "id": "second-breakdown-4", + "label": "Breakdown 4", + "mandatory": false, + "decimal_places": 2, + "type": "Number" + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "another-number-total-playback", + "title": "We calculate the total of number values entered to be %(total)s. Is this correct?", + "calculation": { + "calculation_type": "sum", + "answers_to_calculate": ["breakdown-1", "breakdown-2", "breakdown-3", "breakdown-4"], + "title": "Another grand total of previous values" + } + } + ] + } + ] + } + ] +} diff --git a/schemas/test/en/test_variants_content.json b/schemas/test/en/test_variants_content.json index bccf527a32..ccec714151 100644 --- a/schemas/test/en/test_variants_content.json +++ b/schemas/test/en/test_variants_content.json @@ -4,9 +4,9 @@ "schema_version": "0.0.1", "data_version": "0.0.3", "survey_id": "0", - "title": "Test Content Variants", + "title": "Test New Content Variants", "theme": "default", - "description": "A questionnaire to test content variants and variant choices", + "description": "A questionnaire to test new content variants and variant choices", "metadata": [ { "name": "user_id", @@ -67,13 +67,15 @@ } ] }, - "when": [ - { - "id": "age-answer", - "condition": "greater than", - "value": 16 - } - ] + "when": { + ">": [ + { + "source": "answers", + "identifier": "age-answer" + }, + 16 + ] + } }, { "content": { @@ -84,13 +86,15 @@ } ] }, - "when": [ - { - "id": "age-answer", - "condition": "less than or equal to", - "value": 16 - } - ] + "when": { + "<=": [ + { + "source": "answers", + "identifier": "age-answer" + }, + 16 + ] + } } ] } diff --git a/schemas/test/en/test_variants_first_item_in_list.json b/schemas/test/en/test_variants_first_item_in_list.json index 9605a532f7..0e3f8f366f 100644 --- a/schemas/test/en/test_variants_first_item_in_list.json +++ b/schemas/test/en/test_variants_first_item_in_list.json @@ -4,9 +4,9 @@ "schema_version": "0.0.1", "data_version": "0.0.3", "survey_id": "0", - "title": "Test Question Variants Using List", + "title": "Test New Question Variants Using List", "theme": "default", - "description": "A questionnaire to test question variants using the first item in a list", + "description": "A questionnaire to test new question variants using the first item in a list", "metadata": [ { "name": "user_id", @@ -236,17 +236,19 @@ "title": "You are the first person in the list", "type": "General" }, - "when": [ - { - "list": "people", - "id_selector": "first", - "condition": "equals", - "comparison": { + "when": { + "==": [ + { + "source": "list", + "identifier": "people", + "selector": "first" + }, + { "source": "location", - "id": "list_item_id" + "identifier": "list_item_id" } - } - ] + ] + } }, { "question": { @@ -271,17 +273,19 @@ "title": "You are not the first person in the list", "type": "General" }, - "when": [ - { - "list": "people", - "id_selector": "first", - "condition": "not equals", - "comparison": { + "when": { + "!=": [ + { + "source": "list", + "identifier": "people", + "selector": "first" + }, + { "source": "location", - "id": "list_item_id" + "identifier": "list_item_id" } - } - ] + ] + } } ], "type": "Question" diff --git a/schemas/test/en/test_variants_question.json b/schemas/test/en/test_variants_question.json index 2bbdeff2f9..17303c9df2 100644 --- a/schemas/test/en/test_variants_question.json +++ b/schemas/test/en/test_variants_question.json @@ -6,7 +6,7 @@ "survey_id": "0", "title": "Test Question Variants", "theme": "default", - "description": "A questionnaire to test question variants", + "description": "A questionnaire to test new question variants", "metadata": [ { "name": "user_id", @@ -66,7 +66,7 @@ "question": { "id": "proxy-question", "title": { - "text": "Are you {person_name}?", + "text": "Are you {person_name}?", "placeholders": [ { "placeholder": "person_name", @@ -136,26 +136,28 @@ "answers": [ { "id": "age-answer", - "mandatory": false, + "mandatory": true, "type": "Number", "label": "Age" } ] }, - "when": [ - { - "id": "proxy-answer", - "condition": "equals", - "value": "Yes, I am" - } - ] + "when": { + "==": [ + { + "source": "answers", + "identifier": "proxy-answer" + }, + "Yes, I am" + ] + } }, { "question": { "id": "age-question", "type": "General", "title": { - "text": "What age is {person_name}?", + "text": "What age is {person_name}?", "placeholders": [ { "placeholder": "person_name", @@ -189,13 +191,15 @@ } ] }, - "when": [ - { - "id": "proxy-answer", - "condition": "equals", - "value": "No, I am answering on their behalf" - } - ] + "when": { + "==": [ + { + "source": "answers", + "identifier": "proxy-answer" + }, + "No, I am answering on their behalf" + ] + } } ] }, @@ -226,18 +230,28 @@ } ] }, - "when": [ - { - "id": "age-answer", - "condition": "greater than", - "value": 16 - }, - { - "id": "proxy-answer", - "condition": "not equals", - "value": "No, I am answering on their behalf" - } - ] + "when": { + "and": [ + { + ">=": [ + { + "source": "answers", + "identifier": "age-answer" + }, + 16 + ] + }, + { + "!=": [ + { + "source": "answers", + "identifier": "proxy-answer" + }, + "No, I am answering on their behalf" + ] + } + ] + } }, { "question": { @@ -262,25 +276,35 @@ } ] }, - "when": [ - { - "id": "age-answer", - "condition": "less than or equal to", - "value": 16 - }, - { - "id": "proxy-answer", - "condition": "not equals", - "value": "No, I am answering on their behalf" - } - ] + "when": { + "and": [ + { + "<=": [ + { + "source": "answers", + "identifier": "age-answer" + }, + 16 + ] + }, + { + "!=": [ + { + "source": "answers", + "identifier": "proxy-answer" + }, + "No, I am answering on their behalf" + ] + } + ] + } }, { "question": { "id": "age-confirmation-question", "type": "General", "title": { - "text": "{person_name} is over 16?", + "text": "{person_name} is over 16?", "placeholders": [ { "placeholder": "person_name", @@ -323,25 +347,35 @@ } ] }, - "when": [ - { - "id": "age-answer", - "condition": "greater than or equal to", - "value": 16 - }, - { - "id": "proxy-answer", - "condition": "equals", - "value": "No, I am answering on their behalf" - } - ] + "when": { + "and": [ + { + ">=": [ + { + "source": "answers", + "identifier": "age-answer" + }, + 16 + ] + }, + { + "==": [ + { + "source": "answers", + "identifier": "proxy-answer" + }, + "No, I am answering on their behalf" + ] + } + ] + } }, { "question": { "id": "age-confirmation-question", "type": "General", "title": { - "text": "{person_name} is under 16?", + "text": "{person_name} is under 16?", "placeholders": [ { "placeholder": "person_name", @@ -384,37 +418,33 @@ } ] }, - "when": [ - { - "id": "age-answer", - "condition": "less than or equal to", - "value": 16 - }, - { - "id": "proxy-answer", - "condition": "equals", - "value": "No, I am answering on their behalf" - } - ] - } - ], - "routing_rules": [ - { - "goto": { - "block": "age-block", - "when": [ + "when": { + "and": [ { - "id": "age-confirm-answer", - "condition": "equals", - "value": "No" + "<=": [ + { + "source": "answers", + "identifier": "age-answer" + }, + 16 + ] + }, + { + "==": [ + { + "source": "answers", + "identifier": "proxy-answer" + }, + "No, I am answering on their behalf" + ] } ] } - }, + } + ], + "routing_rules": [ { - "goto": { - "section": "End" - } + "section": "End" } ] } @@ -476,13 +506,15 @@ } ] }, - "when": [ - { - "id": "currency-answer", - "condition": "equals", - "value": "Sterling" - } - ] + "when": { + "==": [ + { + "source": "answers", + "identifier": "currency-answer" + }, + "Sterling" + ] + } }, { "question": { @@ -500,13 +532,15 @@ } ] }, - "when": [ - { - "id": "currency-answer", - "condition": "equals", - "value": "US Dollars" - } - ] + "when": { + "==": [ + { + "source": "answers", + "identifier": "currency-answer" + }, + "US Dollars" + ] + } } ] }, @@ -530,13 +564,15 @@ } ] }, - "when": [ - { - "id": "currency-answer", - "condition": "equals", - "value": "Sterling" - } - ] + "when": { + "==": [ + { + "source": "answers", + "identifier": "currency-answer" + }, + "Sterling" + ] + } }, { "question": { @@ -554,13 +590,15 @@ } ] }, - "when": [ - { - "id": "currency-answer", - "condition": "equals", - "value": "US Dollars" - } - ] + "when": { + "==": [ + { + "source": "answers", + "identifier": "currency-answer" + }, + "US Dollars" + ] + } } ] } diff --git a/schemas/test/en/test_view_submitted_response.json b/schemas/test/en/test_view_submitted_response.json index adc19efb98..66a5d4c298 100644 --- a/schemas/test/en/test_view_submitted_response.json +++ b/schemas/test/en/test_view_submitted_response.json @@ -35,6 +35,7 @@ "sections": [ { "id": "name-section", + "title": "Name Section", "groups": [ { "blocks": [ diff --git a/schemas/test/en/test_view_submitted_response_repeating_sections.json b/schemas/test/en/test_view_submitted_response_repeating_sections.json new file mode 100644 index 0000000000..b0a015ccd5 --- /dev/null +++ b/schemas/test/en/test_view_submitted_response_repeating_sections.json @@ -0,0 +1,664 @@ +{ + "mime_type": "application/json/ons/eq", + "language": "en", + "schema_version": "0.0.1", + "data_version": "0.0.3", + "survey_id": "0", + "title": "Test View Submitted Response Repeating Sections", + "theme": "default", + "description": "A questionnaire to test the summary pages with repeating section on the view submitted response page", + "metadata": [ + { + "name": "user_id", + "type": "string" + }, + { + "name": "period_id", + "type": "string" + }, + { + "name": "ru_name", + "type": "string" + } + ], + "questionnaire_flow": { + "type": "Hub", + "options": {} + }, + "post_submission": { + "view_response": true + }, + "sections": [ + { + "title": "Personal Details Section", + "id": "name-section", + "groups": [ + { + "blocks": [ + { + "type": "Question", + "id": "name", + "question": { + "answers": [ + { + "id": "name-answer", + "label": "Full name", + "max_length": 20, + "mandatory": false, + "type": "TextField" + } + ], + "id": "name-question", + "title": "What is your name?", + "type": "General" + } + } + ], + "id": "personal-details-group", + "title": "Personal Details" + }, + { + "blocks": [ + { + "type": "Question", + "id": "address", + "question": { + "answers": [ + { + "id": "address-answer", + "label": "Postcode", + "max_length": 20, + "mandatory": false, + "type": "TextField" + } + ], + "id": "address-question", + "title": "What is your address?", + "type": "General" + } + } + ], + "id": "address-details-group", + "title": "Address Details" + } + ] + }, + { + "id": "section", + "title": "Household Section", + "groups": [ + { + "id": "group", + "title": "List", + "blocks": [ + { + "id": "primary-person-list-collector", + "type": "PrimaryPersonListCollector", + "for_list": "people", + "add_or_edit_block": { + "id": "add-or-edit-primary-person", + "type": "PrimaryPersonListAddOrEditQuestion", + "question": { + "id": "primary-person-add-or-edit-question", + "type": "General", + "title": "What is your name?", + "answers": [ + { + "id": "first-name", + "label": "First name", + "mandatory": true, + "type": "TextField" + }, + { + "id": "last-name", + "label": "Last name", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "question": { + "id": "primary-confirmation-question", + "type": "General", + "title": "Do you live here?", + "answers": [ + { + "id": "you-live-here", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + { + "id": "list-collector", + "type": "ListCollector", + "for_list": "people", + "question": { + "id": "confirmation-question", + "type": "General", + "title": "Does anyone else live here?", + "answers": [ + { + "id": "anyone-else", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RedirectToListAddBlock" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + }, + "add_block": { + "id": "add-person", + "type": "ListAddQuestion", + "question": { + "id": "add-question", + "type": "General", + "title": "What is the name of the person?", + "answers": [ + { + "id": "first-name", + "label": "First name", + "mandatory": true, + "type": "TextField" + }, + { + "id": "last-name", + "label": "Last name", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "edit_block": { + "id": "edit-person", + "type": "ListEditQuestion", + "question": { + "id": "edit-question", + "type": "General", + "title": "What is the name of the person?", + "answers": [ + { + "id": "first-name", + "label": "First name", + "mandatory": true, + "type": "TextField" + }, + { + "id": "last-name", + "label": "Last name", + "mandatory": true, + "type": "TextField" + } + ] + } + }, + "remove_block": { + "id": "remove-person", + "type": "ListRemoveQuestion", + "question": { + "id": "remove-question", + "type": "General", + "title": "Are you sure you want to remove this person?", + "answers": [ + { + "id": "remove-confirmation", + "mandatory": true, + "type": "Radio", + "options": [ + { + "label": "Yes", + "value": "Yes", + "action": { + "type": "RemoveListItemAndAnswers" + } + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + "summary": { + "title": "Household members", + "item_title": { + "text": "{person_name}", + "placeholders": [ + { + "placeholder": "person_name", + "transforms": [ + { + "arguments": { + "delimiter": " ", + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "first-name" + }, + { + "source": "answers", + "identifier": "last-name" + } + ] + }, + "transform": "concatenate_list" + } + ] + } + ] + } + } + } + ] + } + ] + }, + { + "id": "questions-section", + "title": "Questions Section", + "summary": { "show_on_completion": true }, + "groups": [ + { + "id": "radio", + "title": "Questions Group", + "blocks": [ + { + "type": "Question", + "id": "skip-first-block", + "question": { + "type": "General", + "id": "skip-first-block-question", + "title": "Skip First Block so it doesn’t appear in Total?", + "answers": [ + { + "type": "Radio", + "id": "skip-first-block-answer", + "mandatory": false, + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ] + } + ] + } + }, + { + "skip_conditions": { + "when": { + "==": [ + { + "identifier": "skip-first-block-answer", + "source": "answers" + }, + "Yes" + ] + } + }, + "type": "Question", + "id": "first-number-block", + "question": { + "id": "first-number-question", + "title": "First Number Question Title", + "type": "General", + "answers": [ + { + "id": "first-number-answer", + "label": "First answer label (optional)", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "skip_conditions": { + "when": { + "==": [ + { + "identifier": "skip-first-block-answer", + "source": "answers" + }, + "Yes" + ] + } + }, + "type": "Question", + "id": "first-and-a-half-number-block", + "question": { + "id": "first-and-a-half-number-question-also-in-total", + "title": "First Number Additional Question Title", + "type": "General", + "answers": [ + { + "id": "first-and-a-half-number-answer-also-in-total", + "label": "First answer label also in total (optional)", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "Question", + "id": "second-number-block", + "question": { + "id": "second-number-question-also-in-total", + "title": "Second Number Additional Question Title", + "type": "General", + "answers": [ + { + "id": "second-number-answer-also-in-total", + "label": "Second answer label also in total (optional)", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "currency-total-playback-1", + "title": "We calculate the total of currency values entered to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "answers", + "identifier": "first-number-answer" + }, + { + "source": "answers", + "identifier": "first-and-a-half-number-answer-also-in-total" + }, + { + "source": "answers", + "identifier": "second-number-answer-also-in-total" + } + ] + }, + "title": "Grand total of previous values" + } + } + ] + } + ] + }, + { + "id": "calculated-summary-section", + "title": "Calculated Summary Section", + "summary": { "show_on_completion": true }, + "repeat": { + "for_list": "people", + "title": { + "text": "{person_name}", + "placeholders": [ + { + "placeholder": "person_name", + "transforms": [ + { + "transform": "concatenate_list", + "arguments": { + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "first-name" + }, + { + "source": "answers", + "identifier": "last-name" + } + ], + "delimiter": " " + } + } + ] + } + ] + } + }, + "groups": [ + { + "title": "Calculated Summary Group", + "blocks": [ + { + "type": "Question", + "id": "third-number-block", + "question": { + "id": "third-number-question", + "title": "Third Number Question Title", + "type": "General", + "answers": [ + { + "id": "third-number-answer", + "label": "Third answer in currency label", + "mandatory": true, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + }, + { + "id": "third-number-answer-also-in-total", + "label": "Third answer label also in currency total (optional)", + "mandatory": false, + "type": "Currency", + "currency": "GBP", + "decimal_places": 2 + } + ] + } + }, + { + "type": "CalculatedSummary", + "id": "currency-total-playback-2", + "title": "We calculate the total of currency values entered to be %(total)s. Is this correct?", + "calculation": { + "operation": { + "+": [ + { + "source": "answers", + "identifier": "third-number-answer" + }, + { + "source": "answers", + "identifier": "third-number-answer-also-in-total" + } + ] + }, + "title": "Grand total of previous values" + } + }, + { + "type": "Question", + "id": "mutually-exclusive-checkbox", + "question": { + "id": "mutually-exclusive-checkbox-question", + "type": "MutuallyExclusive", + "title": "Which answer did you give to question 4 and a half?", + "mandatory": false, + "answers": [ + { + "id": "checkbox-answer", + "instruction": "Select an answer", + "type": "Checkbox", + "mandatory": false, + "options": [ + { + "label": { + "placeholders": [ + { + "placeholder": "answer_value_1", + "value": { + "identifier": "first-and-a-half-number-answer-also-in-total", + "source": "answers" + } + } + ], + "text": "{answer_value_1} - first and a half answer" + }, + "value": "{answer_value_1}" + }, + { + "label": { + "placeholders": [ + { + "placeholder": "calc_value_1", + "value": { + "identifier": "currency-total-playback-1", + "source": "calculated_summary" + } + } + ], + "text": "{calc_value_1} - calculated summary answer (previous section)" + }, + "value": "{calc_value_1}" + }, + { + "label": { + "placeholders": [ + { + "placeholder": "calc_value_2", + "value": { + "identifier": "currency-total-playback-2", + "source": "calculated_summary" + } + } + ], + "text": "{calc_value_2} - calculated summary answer (current section)" + }, + "value": "{calc_value_2}" + }, + { + "label": { + "placeholders": [ + { + "placeholder": "third_answer_value", + "value": { + "identifier": "third-number-answer", + "source": "answers" + } + } + ], + "text": "{third_answer_value} - third answer" + }, + "value": "{third_answer_value}" + } + ] + }, + { + "id": "checkbox-exclusive-answer", + "mandatory": false, + "type": "Checkbox", + "options": [ + { + "label": "I prefer not to say", + "description": "Some description", + "value": "I prefer not to say" + } + ] + } + ] + } + }, + { + "skip_conditions": { + "when": { + ">=": [ + { + "source": "calculated_summary", + "identifier": "currency-total-playback-2" + }, + 50 + ] + } + }, + "type": "Question", + "id": "skippable-block", + "question": { + "answers": [ + { + "id": "skippable-answer", + "mandatory": true, + "type": "Currency", + "label": "Capital expenditure", + "decimal_places": 0, + "currency": "GBP" + } + ], + "id": "skippable-question", + "title": { + "text": "How much did {person_name} spend on fruit?", + "placeholders": [ + { + "placeholder": "person_name", + "transforms": [ + { + "transform": "concatenate_list", + "arguments": { + "list_to_concatenate": [ + { + "source": "answers", + "identifier": "first-name" + }, + { + "source": "answers", + "identifier": "last-name" + } + ], + "delimiter": " " + } + } + ] + } + ] + }, + "type": "General" + } + } + ], + "id": "calculated-summary" + } + ] + } + ] +} diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000000..0d055d48bb --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,40 @@ +# Scripts + +## Script to auto-generate code for integration test + +### Details + +To speed up the process of generating integration tests for Runner, there is a dev-convenience script that records the GET and POST requests of a user journey +and outputs this formatted in the style of an integration test. + +### Overview + +* All POSTs are recorded. To ensure only the necessary GET requests are recorded, additional logic excludes the following GET requests: + * Session tokens + * Initial URL requests for each page load +* Additional logic is in place to ensure that, when navigating backwards in a journey after following links (e.g. 'previous' link), it is recorded correctly. + This is achieved by storing the previous request method at module-level so that it can be used in deciding whether to record or disregard the GET request. +* You will need to manually add your assertions in the generated test file +* When the script is launched, it will create a new file for the schema chosen. If you launch the script again for the same schema, it will overwrite the + previous file output +* The script is intended to be run with schemas with a `test_` prefix, which would suit most scenarios for test generation. If you wish to use a schema without + this prefix, you will need to manually amend the generated names for the file, class, and function to allow pytest to process the test file correctly +* It does **not** handle dynamic answers because these are generated at runtime - you will need to update the output script to handle `list_item_id` separately, + as they will not be known beforehand + +### Usage + +Run the following make command from the project root folder: + +```shell +make generate-integration-test +``` + +This will pause the script and open a browser pointing to the Launcher UI (make sure the application and supporting services are running). Now follow the below +steps: + +1. Choose a schema and launch it - the schema name will be used for the name of the integration test output file +1. Navigate through the survey +1. When you're finished with the journey at any point, return to the command line and hit Enter +1. The output will be shown in the logs, and a formatted file will be created for you in the scripts folder. For example: `scripts/test_checkbox.py` +1. Add your assertions to the test file and move the file into the appropriate `test/integration/` location diff --git a/scripts/extract_translation_templates.py b/scripts/extract_translation_templates.py index 31da93265c..01ded6bf06 100644 --- a/scripts/extract_translation_templates.py +++ b/scripts/extract_translation_templates.py @@ -22,8 +22,8 @@ def get_template_content(filename, ignore_context=False): with open(filename, encoding="UTF-8") as file: return list( filter( - lambda l: all( - not l.startswith(param) for param in line_beginnings_to_ignore + lambda line: all( + not line.startswith(param) for param in line_beginnings_to_ignore ), file.readlines(), ) @@ -40,7 +40,7 @@ def print_filename_results(filename, success=True): def build_static_template(output_filepath): subprocess.run( [ - "pipenv", + "poetry", "run", "pybabel", "extract", diff --git a/scripts/generate_integration_test.py b/scripts/generate_integration_test.py new file mode 100644 index 0000000000..551e129960 --- /dev/null +++ b/scripts/generate_integration_test.py @@ -0,0 +1,152 @@ +from typing import IO, Dict +from urllib.parse import parse_qs, urlparse + +from playwright.sync_api import Playwright, Request, sync_playwright +from structlog import get_logger + +logger = get_logger() + +LAUNCHER_ROOT_URL = "http://localhost:8000" +RUNNER_ROOT_URL = "http://localhost:5000" + +TEST_TEMPLATE = """from tests.integration.integration_test_case import IntegrationTestCase + + +class Test{class_name}(IntegrationTestCase): + def test_{function_name}(self): + self.launchSurveyV2(schema_name="{schema_name}") +""" + +survey_journey: Dict[str, str | bool | None] = { + "previous_request_method": None, + "in_progress": False, + "schema_name": None, +} + +output: Dict[str, str] = {"file_name": ""} + + +def process_runner_request(request: Request) -> None: + with open(output["file_name"], "a", encoding="utf-8") as file: + if request.method == "POST": + process_post(request, file) + + elif request.method == "GET": + process_get(request, file) + + +def process_post(request: Request, file: IO) -> None: + survey_journey["previous_request_method"] = "POST" + + # Playwright Request.post_data comes formatted like a URL query string, so can be parsed + post_data = parse_qs(request.post_data) + del post_data["csrf_token"] + + items = { + answer_id: answer_values[0] if len(answer_values) == 1 else answer_values + for answer_id, answer_values in post_data.items() + } + + # Post items, or empty post for no answers/non-question pages + file.write(generate_method_request(method="post", data=items or "")) + + +def is_recordable_survey_navigation(request: Request) -> bool: + return ( + survey_journey["previous_request_method"] == "GET" + and "session?token" not in request.url + and request.url != f"{RUNNER_ROOT_URL}/questionnaire/" + ) + + +def process_get(request: Request, file: IO) -> None: + """ + We only want to record GET requests in Runner for actions like navigating back in a survey journey. Therefore, we exclude the following: + - the very first GET action of a survey journey, after schema is loaded + - tokens/authentication + """ + has_journey_started = ( + not survey_journey["in_progress"] + and request.url == f"{RUNNER_ROOT_URL}/questionnaire/" + ) + if has_journey_started: + survey_journey["in_progress"] = True + return + + if is_recordable_survey_navigation(request): + path = f'"{urlparse(request.url).path}"' + file.write(generate_method_request(method="get", data=path)) + + elif survey_journey["in_progress"]: + # ensure the request method is captured - allows us to record Runner GET navigation actions on the next pass through + survey_journey["previous_request_method"] = request.method + + +def process_launcher_request(request: Request) -> None: + if request.method != "GET": + return + + if survey_journey["in_progress"]: + # capture launcher urls for sign-out, save etc + with open(output["file_name"], "a", encoding="utf-8") as file: + path = f'"{urlparse(request.url).path}"' + file.write(generate_method_request(method="get", data=path)) + elif "schema_name" in parse_qs(request.url): + # start of journey, so create a skeleton file using the schema name + survey_journey["schema_name"] = parse_qs(request.url)["schema_name"][0] + output["file_name"] = f"./scripts/{survey_journey['schema_name']}.py" + + with open(output["file_name"], "w", encoding="utf-8") as file: + # Type ignore: schema_name is taken as string from query string + class_name = survey_journey["schema_name"].title().replace("_", "") # type: ignore + function_name = survey_journey["schema_name"] + if not class_name.lower().startswith("test"): + class_name = f"Test{class_name}" + function_name = f"test_{survey_journey['schema_name']}" + file.write( + TEST_TEMPLATE.format( + class_name=class_name, + function_name=function_name, + schema_name=survey_journey["schema_name"], + ) + ) + logger.info(request) + + +def generate_method_request(*, method: str, data: dict | str | None = None) -> str: + snippet = f"self.{method}({data})" + logger.info(f'Generating Runner code snippet for HTTP request: "{snippet}"') + return f"\n {snippet}" + + +def request_handler(request: Request) -> None: + if LAUNCHER_ROOT_URL in request.url: + process_launcher_request(request) + elif RUNNER_ROOT_URL in request.url: + process_runner_request(request) + + +def run(pw: Playwright) -> None: + chromium = pw.chromium + browser = chromium.launch( + headless=False, args=["--start-maximized"], channel="chrome" + ) + page = browser.new_page(no_viewport=True) + page.goto(LAUNCHER_ROOT_URL) + + page.on("request", request_handler) + + input( + "Script is paused. Start navigating through the browser for the journey & press Enter when finished to capture" + " the output and add into a test file\n" + ) + browser.close() + logger.info( + "Integration test generated successfully", + integration_test_file=output["file_name"], + ) + + +if __name__ == "__main__": + with sync_playwright() as playwright: + run(playwright) diff --git a/scripts/merge_profiles.py b/scripts/merge_profiles.py index 4f5c4e6ea3..452bb27c59 100644 --- a/scripts/merge_profiles.py +++ b/scripts/merge_profiles.py @@ -15,4 +15,5 @@ else: stats.add(profiling_dir + p) -stats.dump_stats("combined_profile.prof") +if stats: + stats.dump_stats("combined_profile.prof") diff --git a/scripts/run_lint_python.sh b/scripts/run_lint_python.sh index f11af5b553..44dd02c096 100755 --- a/scripts/run_lint_python.sh +++ b/scripts/run_lint_python.sh @@ -18,8 +18,8 @@ function display_result { fi } -flake8 --max-complexity 10 --count -display_result $? 1 "Flake 8 code style check" +ruff check . +display_result $? 1 "Ruff code style check (including isort)" # pylint bit encodes the exit code to allow you to figure out which category has failed. # https://docs.pylint.org/en/1.6.0/run.html#exit-codes @@ -28,9 +28,6 @@ display_result $? 1 "Flake 8 code style check" find . -type f -name "*.py" | xargs pylint --reports=n --output-format=colorized --rcfile=.pylintrc -j 0 display_result $? 1 "Pylint linting check" -isort --check . -display_result $? 1 "isort linting check" - ./scripts/run_mypy.sh display_result $? 1 "Mypy type check" diff --git a/scripts/run_mypy.sh b/scripts/run_mypy.sh index d3bdb34e1e..774b61faa3 100755 --- a/scripts/run_mypy.sh +++ b/scripts/run_mypy.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash -pipenv run mypy app +poetry run mypy app scripts diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh index 9707b3f5df..7c5609a22a 100755 --- a/scripts/run_tests.sh +++ b/scripts/run_tests.sh @@ -15,7 +15,7 @@ echo "Running python lint tests" ./scripts/run_lint_python.sh echo "Running js lint tests" -yarn lint +npm run lint echo "Running unit tests" ./scripts/run_tests_unit.sh diff --git a/scripts/run_tests_functional.sh b/scripts/run_tests_functional.sh deleted file mode 100755 index f02dfa5069..0000000000 --- a/scripts/run_tests_functional.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash -# -# Run functional tests -# -# NOTE: This script expects to be run from the project root with -# ./scripts/run_tests_functional.sh - -set -o pipefail - -function display_result { - RESULT=$1 - EXIT_STATUS=$2 - TEST=$3 - - if [ $RESULT -ne 0 ]; then - echo -e "\033[31m$TEST failed\033[0m" - exit $EXIT_STATUS - else - echo -e "\033[32m$TEST passed\033[0m" - fi -} - -# Run Functional tests -echo "Generating functional test pages" -yarn generate_pages - -echo "Running front end functional tests" -yarn test_functional $1 - -display_result $? 5 "Front end functional tests" - diff --git a/scripts/run_validator.sh b/scripts/run_validator.sh index 6e554b7704..e97dce745e 100755 --- a/scripts/run_validator.sh +++ b/scripts/run_validator.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash tag=latest -TAG=${tag} docker-compose -f docker-compose-schema-validator.yml up -d +TAG=${tag} docker compose -f docker-compose-schema-validator.yml up -d diff --git a/scripts/validate_test_schemas.py b/scripts/validate_test_schemas.py new file mode 100644 index 0000000000..445280a41a --- /dev/null +++ b/scripts/validate_test_schemas.py @@ -0,0 +1,135 @@ +import json +import logging +import os +import re +import subprocess +import sys +import time +from concurrent.futures import ThreadPoolExecutor, as_completed + +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) + + +def check_connection(): + for connection_attempts in range(4, 0, -1): + response = subprocess.run( + [ + "curl", + "-so", + "/dev/null", + "-w", + "%{http_code}", + "http://localhost:5002/status", + ], + capture_output=True, + text=True, + check=False, + ).stdout.strip() + + if response == "200": + return + + logging.error("\033[31m---Error: Schema Validator Not Reachable---\033[0m") + logging.error("\033[31mHTTP Status: %s\033[0m", response) + + if connection_attempts == 1: + logging.info("Exiting...\n") + sys.exit(1) + + logging.info("Retrying...\n") + time.sleep(5) + + +def get_schemas() -> list[str]: + if len(sys.argv) == 1 or sys.argv[1] == "--local": + file_path = "./schemas/test/en" + schemas = [ + os.path.join(file_path, f) + for f in os.listdir(file_path) + if f.endswith(".json") + ] + logging.info("--- Testing Schemas in %s ---", file_path) + else: + schema = sys.argv[1] + schemas = [schema] + logging.info("--- Testing %s Schema ---", schema) + return schemas + + +def validate_schema(schema_path): + try: + result = subprocess.run( + [ + "curl", + "-s", + "-w", + "HTTPSTATUS:%{http_code}", + "-X", + "POST", + "-H", + "Content-Type: application/json", + "-d", + f"@{schema_path}", + "http://localhost:5001/validate", + ], + capture_output=True, + text=True, + check=True, + ) + return schema_path, result.stdout + except subprocess.CalledProcessError as e: + logging.info("Error validating schema %s: %s", schema_path, e) + return schema_path, None + + +def main(): + # pylint: disable=broad-exception-caught + passed = 0 + failed = 0 + + check_connection() + schemas = get_schemas() + + with ThreadPoolExecutor(max_workers=20) as executor: + future_to_schema = { + executor.submit(validate_schema, schema): schema for schema in schemas + } + for future in as_completed(future_to_schema): + schema = future_to_schema[future] + try: + schema_path, result = future.result() + # Extract HTTP body + http_body = re.sub(r"HTTPSTATUS:.*", "", result) + + # Convert HTTP body to JSON + http_body_json = json.loads(http_body) + + # Format JSON + formatted_json = json.dumps(http_body_json, indent=4) + + # Extract HTTP status code + result_response = re.search(r"HTTPSTATUS:(\d+)", result)[1] + + if result_response == "200" and http_body_json == {}: + logging.info("\033[32m%s: PASSED\033[0m", schema_path) + passed += 1 + else: + logging.error("\033[31m%s: FAILED\033[0m", schema_path) + logging.error( + "\033[31mHTTP Status @ /validate: %s\033[0m", result_response + ) + logging.error("\033[31mHTTP Status: %s\033[0m", formatted_json) + failed += 1 + except Exception as e: + logging.error("\033[31mError processing %s: %s\033[0m", schema, e) + failed += 1 + + logging.info("\033[32m%s passed\033[0m - \033[31m%s failed\033[0m", passed, failed) + if passed != len(schemas): + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/validate_test_schemas.sh b/scripts/validate_test_schemas.sh deleted file mode 100755 index 9a601c626f..0000000000 --- a/scripts/validate_test_schemas.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env bash - -green="$(tput setaf 2)" -red="$(tput setaf 1)" -default="$(tput sgr0)" -checks=4 - -until [ "$checks" == 0 ]; do - response="$(curl -so /dev/null -w '%{http_code}' http://localhost:5002/status)" - - if [ "$response" != "200" ]; then - echo "${red}---Error: Schema Validator Not Reachable---" - echo "HTTP Status: $response" - if [ "$checks" != 1 ]; then - echo -e "Retrying...${default}\\n" - sleep 5 - else - echo -e "Exiting...${default}\\n" - exit 1 - fi - (( checks-- )) - else - (( checks=0 )) - fi - -done - -exit=0 - -if [ $# -eq 0 ] || [ "$1" == "--local" ]; then - file_path="./schemas/test/en" -else - file_path="$1" -fi - -echo "--- Testing Schemas in $file_path ---" -failed=0 -passed=0 - -for schema in $(find $file_path -name '*.json'); do - - result="$(curl -s -w 'HTTPSTATUS:%{http_code}' -X POST -H "Content-Type: application/json" -d @"$schema" http://localhost:5001/validate | tr -d '\n')" - result_response="${result//*HTTPSTATUS:/}" - result_body=$(echo "${result//HTTPSTATUS:*/}" | python -m json.tool) - - if [ "$result_response" == "200" ] && [ "$result_body" == "{}" ]; then - echo -e "${green}$schema - PASSED${default}" - (( passed++ )) - else - echo -e "\\n${red}$schema - FAILED" - echo "HTTP Status @ /validate: [$result_response]" - echo -e "Error: [$result_body]${default}\\n" - (( failed++ )) - exit=1 - fi - -done - -echo -e "\\n${green}$passed Passed${default} - ${red}$failed Failed${default}" - -exit "$exit" diff --git a/setup.cfg b/setup.cfg index 8af620026f..7624317db1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,3 +6,12 @@ filterwarnings= ignore:.*formatargspec.*:DeprecationWarning ignore:.*isAlive.*:PendingDeprecationWarning +[flake8] +# Ignore node_modules and cloned repos when not in a virtual environment +exclude = node_modules/*,tests/*,src/* +max-line-length = 160 +inline-quotes = double +multiline-quotes = double +docstring-quotes = double +avoid-escape = True +ignore = E704,W503,E203,E902 diff --git a/templates/answersummary.html b/templates/answersummary.html index 31e3bb7e24..0c73b74864 100644 --- a/templates/answersummary.html +++ b/templates/answersummary.html @@ -1 +1 @@ -{% extends 'summary.html' %} +{% extends "summary.html" %} diff --git a/templates/assets/images/dbt-logo-stacked.svg b/templates/assets/images/dbt-logo-stacked.svg new file mode 100644 index 0000000000..de0b99d4a7 --- /dev/null +++ b/templates/assets/images/dbt-logo-stacked.svg @@ -0,0 +1,39 @@ + + Department for Business and Trade logo + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/assets/images/desnz-logo-stacked.svg b/templates/assets/images/desnz-logo-stacked.svg new file mode 100644 index 0000000000..3a76910b53 --- /dev/null +++ b/templates/assets/images/desnz-logo-stacked.svg @@ -0,0 +1,116 @@ + + Department for Energy Security and Net Zero + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/assets/images/dsit-logo-stacked.svg b/templates/assets/images/dsit-logo-stacked.svg new file mode 100644 index 0000000000..b9a805f7d5 --- /dev/null +++ b/templates/assets/images/dsit-logo-stacked.svg @@ -0,0 +1,12 @@ + + Department for Science, Innovation and Technology logo + + + + + + + + + diff --git a/templates/assets/images/finance-ni-logo-stacked.svg b/templates/assets/images/finance-ni-logo-stacked.svg new file mode 100644 index 0000000000..1164744f4e --- /dev/null +++ b/templates/assets/images/finance-ni-logo-stacked.svg @@ -0,0 +1,9 @@ + + Northern Ireland Department of Finance logo + + + + + + + diff --git a/templates/assets/images/finance-ni-logo.svg b/templates/assets/images/finance-ni-logo.svg new file mode 100644 index 0000000000..9001a47e2a --- /dev/null +++ b/templates/assets/images/finance-ni-logo.svg @@ -0,0 +1,16 @@ + diff --git a/templates/assets/images/finance-ni-mobile-logo.svg b/templates/assets/images/finance-ni-mobile-logo.svg new file mode 100644 index 0000000000..f4a6a5e3f8 --- /dev/null +++ b/templates/assets/images/finance-ni-mobile-logo.svg @@ -0,0 +1,16 @@ + diff --git a/templates/assets/images/nhs-logo.svg b/templates/assets/images/nhs-logo.svg new file mode 100644 index 0000000000..c05d83269b --- /dev/null +++ b/templates/assets/images/nhs-logo.svg @@ -0,0 +1,9 @@ + + National Heath Service + + + + + + + diff --git a/templates/assets/images/ons-logo-stacked-mb.svg b/templates/assets/images/ons-logo-stacked-mb.svg new file mode 100644 index 0000000000..b918000216 --- /dev/null +++ b/templates/assets/images/ons-logo-stacked-mb.svg @@ -0,0 +1,12 @@ + + Office for National Statistics + + + + + + + + + + diff --git a/templates/assets/images/ons-logo-stacked.svg b/templates/assets/images/ons-logo-stacked.svg new file mode 100644 index 0000000000..d7056700ab --- /dev/null +++ b/templates/assets/images/ons-logo-stacked.svg @@ -0,0 +1,12 @@ + + Office for National Statistics + + + + + + + + + + diff --git a/templates/assets/images/orr-logo.svg b/templates/assets/images/orr-logo.svg new file mode 100644 index 0000000000..5a93c4aeb1 --- /dev/null +++ b/templates/assets/images/orr-logo.svg @@ -0,0 +1,13 @@ + diff --git a/templates/assets/images/orr-mobile-logo.svg b/templates/assets/images/orr-mobile-logo.svg new file mode 100644 index 0000000000..aaf428ef5d --- /dev/null +++ b/templates/assets/images/orr-mobile-logo.svg @@ -0,0 +1,13 @@ + diff --git a/templates/assets/images/ukhsa-logo-stacked.svg b/templates/assets/images/ukhsa-logo-stacked.svg new file mode 100644 index 0000000000..ea4fa0ddec --- /dev/null +++ b/templates/assets/images/ukhsa-logo-stacked.svg @@ -0,0 +1,717 @@ + + UK Health Security Agency + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/calculatedsummary.html b/templates/calculatedsummary.html index 6fb734b2cc..f909cfff0d 100644 --- a/templates/calculatedsummary.html +++ b/templates/calculatedsummary.html @@ -1,31 +1,5 @@ -{% extends 'layouts/_questionnaire.html' %} -{% from "components/button/_macro.njk" import onsButton %} -{% from "components/panel/_macro.njk" import onsPanel %} +{% extends "layouts/_calculatedsummary.html" %} -{% set save_on_signout = true %} - -{% block form_content %} -

{{content.summary.title}}

- - {% call onsPanel({ - "classes": "ons-u-mb-l" - }) %} -

{{ _("Please review your answers and confirm these are correct") }}

- {% endcall %} - -
- {% include 'partials/summary/summary.html' %} -
-{% endblock -%} - -{% block submit_button %} - {{ - onsButton({ - "text": _("Yes, I confirm these are correct"), - "submitType": 'timer', - "attributes": { - "data-qa": "btn-submit" - } - }) - }} -{% endblock %} +{% block form_title %} +

{{ content.summary.title }}

+{% endblock form_title %} diff --git a/templates/census-thank-you.html b/templates/census-thank-you.html deleted file mode 100644 index 507a793f87..0000000000 --- a/templates/census-thank-you.html +++ /dev/null @@ -1,64 +0,0 @@ -{% extends 'layouts/_base.html' %} -{% from 'components/panel/_macro.njk' import onsPanel %} -{% set hide_sign_out_button = content.hide_sign_out_button %} -{% set sign_out_url = content.sign_out_url %} - -{% block main %} - {% set form = content.form %} - {% if form and form.mapped_errors %} - {% set error_title = _("There is a problem with this page") %} - {% include 'partials/error-panel.html' %} - {% endif %} - - {% call onsPanel({ - "type": "success", - "classes": "ons-u-mb-s", - "iconType": "check", - "iconSize": "xl" - }) - %} -

- {% if content.form_type == "I" %} - {{ _("Thank you for completing your census") }} - {% elif content.form_type %} - {{ _("Thank you for completing the census") }} - {% else %} - {{ _("Thank you for completing the survey") }} - {% endif %} -

- - {% if content.form_type %} - {% if content.form_type == "I" %} -

{{ _("Your individual census has been submitted for {display_address}").format(display_address = content.display_address) }}

- {% elif content.form_type == "H" %} -

{{ _("Your census has been submitted for the household at {display_address}").format(display_address = content.display_address) }}

- {% elif content.form_type == "C" %} -

{{ _("Your census has been submitted for the accommodation at {display_address}").format(display_address = content.display_address) }}

-

{{ _("Anyone staying at this accommodation for at least 6 months needs to fill in their own individual census, including staff. Your Census Officer will provide you with census forms for your residents.") }}

- {% endif %} - {% endif %} - {% endcall %} - - - {% call onsPanel({ - "type": "bare", - "classes": "ons-u-mb-s", - "iconType": "lock" - }) - %} -

{{ _("Your personal information is protected by law and will be kept confidential") }}

- {% endcall %} - - - {% if form %} -
-

{{ _("Get confirmation email") }}

-

{{ _("If you would like to be sent confirmation that you have completed your census, enter your email address") }}

- {% include 'partials/email-form.html' %} - {% endif %} - - {% if content.show_feedback_call_to_action %} - {% include 'partials/feedback-call-to-action.html' %} - {% endif %} - -{% endblock %} diff --git a/templates/confirm-email.html b/templates/confirm-email.html index 15124058a5..49c1ad15a5 100644 --- a/templates/confirm-email.html +++ b/templates/confirm-email.html @@ -1,5 +1,7 @@ -{% extends 'layouts/_base.html' %} -{% import 'macros/helpers.html' as helpers %} +{% extends "layouts/_base.html" %} + +{% import "macros/helpers.html" as helpers %} + {% from "components/button/_macro.njk" import onsButton %} {% set hide_sign_out_button = content.hide_sign_out_button %} @@ -7,29 +9,36 @@ {% set sign_out_url = content.sign_out_url %} {% block main %} - {% set form = content.form %} - {% if form.mapped_errors %} - {% set error_title = _("There is a problem with your answer") %} - {% include 'partials/error-panel.html' %} - {% endif %} - -

{{ content.question.title }}

-

{{ content.question.description}}

- {%- for answer in question.answers -%} - {% set question_title = answer.title %} - {% include 'partials/answer.html' %} - {%- endfor -%} - - {{ - onsButton({ - "text": _("Continue"), - "submitType": 'timer', - "classes": "ons-u-mt-xl", - "attributes": { - "data-qa": "btn-submit" - } - }) - }} -

- -{% endblock %} + {% set form = content.form %} + {% if form.mapped_errors %} + {% set error_title = _("There is a problem with your answer") %} + {% include "partials/error-panel.html" %} + {% endif %} +

{{ content.question.title }}

+
+

{{ content.question.description }}

+
+ {%- for answer in question.answers -%} + {% set question_title = answer.title %} + {% include "partials/answer.html" %} + {%- endfor -%} + {# djlint:off #} + {{ + onsButton({ + "text": _("Continue"), + "variants": 'timer', + "classes": "ons-u-mt-xl", + "attributes": { + "data-qa": "btn-submit", + "data-ga-category": "Button", + "data-ga-action": "Submit", + "data-ga-label": "Confirm", + "data-ga-page": "Confirmation Email", + "data-ga": "click", + } + }) + }} + {# djlint:on #} +
+
+{% endblock main %} diff --git a/templates/confirmation-email-sent.html b/templates/confirmation-email-sent.html index 88d00b4b52..b57cd431f9 100644 --- a/templates/confirmation-email-sent.html +++ b/templates/confirmation-email-sent.html @@ -1,37 +1,47 @@ -{% extends 'layouts/_base.html' %} -{% from 'components/panel/_macro.njk' import onsPanel %} +{% extends "layouts/_base.html" %} + +{% from "components/panel/_macro.njk" import onsPanel %} + {% set page_title = _("Confirmation email sent") %} {% set hide_sign_out_button = content.hide_sign_out_button %} {% set sign_out_url = content.sign_out_url %} {% block main %} - {% call onsPanel({ - "spacious": true, - "type": "success", - "classes": "ons-u-mb-s", - "iconType": "check", - "iconSize": "xl" - }) - %} -

{{ _("A confirmation email has been sent to {email}").format(email = content.email) }}

- {% endcall %} - - {% call onsPanel({ - "type": "bare", - "classes": "ons-u-mb-s", - "iconType": "lock" - }) - %} -

{{ _("The email will be sent from census.2021@notifications.service.gov.uk") }}

- {% endcall %} - - {% if content.show_send_another_email_guidance %} -

{{ _("Didn't receive an email?") }}

-

{{ _("It can take a few minutes for the email to arrive. If it doesn't arrive, check your junk mail, or you can send another confirmation email.").format(send_confirmation_email_url = content.send_confirmation_email_url)}}

- {% endif %} - - {% if content.show_feedback_call_to_action %} - {% include 'partials/feedback-call-to-action.html' %} - {% endif %} - -{% endblock %} + {# djlint:off #} + {% call + onsPanel({ + "spacious": true, + "variant": "success", + "classes": "ons-u-mb-s", + "iconType": "check", + "iconSize": "xl" + }) + %} +

+ {{ _("A confirmation email has been sent to {email}").format(email = content.email) }} +

+ {% endcall %} + {% call + onsPanel({ + "variant": "bare", + "classes": "ons-u-mb-s", + "iconType": "lock" + }) + %} +

+ {{ _("The email will be sent from census.2021@notifications.service.gov.uk") }} +

+ {% endcall %} + {# djlint:on #} + {% if content.show_send_another_email_guidance %} +

+ {{ _("Didn’t receive an email?") }} +

+

+ {{ _("It can take a few minutes for the email to arrive. If it doesn’t arrive, check your junk mail, or you can send another confirmation email.").format(send_confirmation_email_url = content.send_confirmation_email_url) }} +

+ {% endif %} + {% if content.show_feedback_call_to_action %} + {% include "partials/feedback-call-to-action.html" %} + {% endif %} +{% endblock main %} diff --git a/templates/confirmation-email.html b/templates/confirmation-email.html index 3fe38c7dfc..7c4f8e41db 100644 --- a/templates/confirmation-email.html +++ b/templates/confirmation-email.html @@ -1,14 +1,14 @@ -{% extends 'layouts/_base.html' %} +{% extends "layouts/_base.html" %} + {% set hide_sign_out_button = content.hide_sign_out_button %} {% set sign_out_url = content.sign_out_url %} {% block main %} - {% set form = content.form %} - {% if form.mapped_errors %} - {% set error_title = _("There is a problem with this page") %} - {% include 'partials/error-panel.html' %} - {% endif %} - -

{{ _("Send a confirmation email") }}

- {% include 'partials/email-form.html' %} -{% endblock %} + {% set form = content.form %} + {% if form.mapped_errors %} + {% set error_title = _("There is a problem with this page") %} + {% include "partials/error-panel.html" %} + {% endif %} +

{{ _("Send a confirmation email") }}

+ {% include "partials/confirmation-email-form.html" %} +{% endblock main %} diff --git a/templates/confirmationquestion.html b/templates/confirmationquestion.html index 7a03464a9b..5e03e2e2a8 100644 --- a/templates/confirmationquestion.html +++ b/templates/confirmationquestion.html @@ -1 +1 @@ -{% extends 'question.html' %} +{% extends "question.html" %} diff --git a/templates/errors/401.html b/templates/errors/401.html index 09ef4fb66a..91a3e7a4c7 100644 --- a/templates/errors/401.html +++ b/templates/errors/401.html @@ -1,14 +1,27 @@ -{% extends 'layouts/_base.html' %} +{% extends "errors/_base.html" %} {% set page_title = _("Page is not available") %} +{% set sign_in = _("You will need to sign back in to access your account").format(url = business_logout_url) %} +{% set access_code = _("To access this page you need to re-enter your access code.").format(url = other_logout_url) %} +{% set business_sign_in = _("If you are completing a business survey, you need to sign back in to your account.").format(url = business_logout_url) %} +{% set other_access_code = _("If you started your survey using an access code, you need to re-enter your code.").format(url = other_logout_url) %} {% block main %} -

{{ _("Sorry, you need to sign in again") }}

-

{{ _("This is because you have either:") }}

-
    -
  • {{ _("been inactive for 45 minutes and your session has timed out to protect your information") }}
  • -
  • {{ _("followed a link to a page you are not signed in to") }}
  • -
  • {{ _("followed a link to a survey that has already been submitted") }}
  • -
-

{{ _("You will need to sign back in to access your account").format(url = account_service_log_out_url) }}

-{% endblock %} +

{{ _("Sorry, you need to sign in again") }}

+

{{ _("This is because you have either:") }}

+
    +
  • {{ _("been inactive for 45 minutes and your session has timed out to protect your information") }}
  • +
  • {{ _("followed a link to a page you are not signed in to") }}
  • +
  • {{ _("followed a link to a survey that has already been submitted") }}
  • +
+ {% if survey_type and (survey_type in SURVEY_TYPES_BUSINESS or survey_type in SURVEY_TYPES_DEFAULT) %} +

{{ sign_in }}

+ {% elif survey_type and survey_type in SURVEY_TYPES_SOCIAL or survey_type in SURVEY_TYPES_HEALTH %} +

{{ access_code }}

+ {% else %} +

{{ _("Business surveys") }}

+

{{ business_sign_in }}

+

{{ _("All other surveys") }}

+

{{ other_access_code }}

+ {% endif %} +{% endblock main %} diff --git a/templates/errors/403.html b/templates/errors/403.html index 56cc2bab2c..701b059157 100644 --- a/templates/errors/403.html +++ b/templates/errors/403.html @@ -1,10 +1,20 @@ -{% extends 'layouts/_base.html' %} +{% extends "errors/_base.html" %} {% set page_title = _("Sorry, there is a problem") %} +{% set all_contact_us = _("For further help, please contact us.").format(url=contact_us_url) %} +{% set business_contact_us = _("If you are completing a business survey and you need further help, please contact us.").format(url=business_contact_us_url) %} +{% set other_contact_us = _("If you started your survey using an access code and you need further help, please contact us.").format(url=other_contact_us_url) %} {% block main %} -

{{ _("Sorry, there is a problem") }}

-

{{ _("You may need to update your browser to a newer version.") }}

-

{{ _("If the problem still occurs, try using a different browser or device.") }}

-

{{ _("For further help, please contact us.").format(url=contact_us_url) }}

-{% endblock %} +

{{ _("Sorry, there is a problem") }}

+

{{ _("You may need to update your browser to a newer version.") }}

+

{{ _("If the problem still occurs, try using a different browser or device.") }}

+ {% if survey_type in SURVEY_TYPES_ALL %} +

{{ all_contact_us }}

+ {% else %} +

{{ _("Business surveys") }}

+

{{ business_contact_us }}

+

{{ _("All other surveys") }}

+

{{ other_contact_us }}

+ {% endif %} +{% endblock main %} diff --git a/templates/errors/404.html b/templates/errors/404.html index a89a707565..c6b35b38da 100644 --- a/templates/errors/404.html +++ b/templates/errors/404.html @@ -1,10 +1,21 @@ -{% extends 'layouts/_base.html' %} +{% extends "errors/_base.html" %} {% set page_title = _("Page not found") %} +{% set all_contact_us = _("If the web address is correct or you selected a link or button, contact us for more help.").format(url=contact_us_url) %} +{% set business_contact_us = _("If you are completing a business survey, please contact us.").format(url=business_contact_us_url) %} +{% set other_contact_us = _("If you started your survey using an access code, please contact us.").format(url=other_contact_us_url) %} {% block main %} -

{{ _("Page not found") }}

-

{{ _("If you entered a web address, check it is correct.") }}

-

{{ _("If you pasted the web address, check you copied the whole address.") }}

-

{{ _("If the web address is correct or you selected a link or button, contact us for more help.").format(url=contact_us_url) }}

-{% endblock %} +

{{ _("Page not found") }}

+

{{ _("If you entered a web address, check it is correct.") }}

+

{{ _("If you pasted the web address, check you copied the whole address.") }}

+ {% if survey_type in SURVEY_TYPES_ALL %} +

{{ all_contact_us }}

+ {% else %} +

{{ _("If the web address is correct or you selected a link or button, please see the following help links.") }}

+

{{ _("Business surveys") }}

+

{{ business_contact_us }}

+

{{ _("All other surveys") }}

+

{{ other_contact_us }}

+ {% endif %} +{% endblock main %} diff --git a/templates/errors/500.html b/templates/errors/500.html index 20b6a3610c..0572870fdf 100644 --- a/templates/errors/500.html +++ b/templates/errors/500.html @@ -1,10 +1,28 @@ -{% extends 'layouts/_base.html' %} +{% extends "errors/_base.html" %} {% set page_title = _("An error has occurred") %} +{% set business_sign_in = _("If you have attempted to submit your survey, you should check that this was successful. To do this, sign in to your business survey account.").format(url = business_logout_url) %} +{% set contact_us = _("If you need more help, contact us.").format(url=contact_us_url) %} +{% set access_code = _("If you have attempted to submit your survey, you should check that this was successful. To do this, re-enter your code.").format(url = other_logout_url) %} +{% set business_contact_us = _("If you need more help, contact us about business surveys.").format(url=business_contact_us_url) %} +{% set other_contact_us = _("If you need more help, contact us about all other surveys.").format(url=other_contact_us_url) %} {% block main %} -

{{ _("Sorry, there is a problem with this service") }}

-

{{ _("Try again later.") }}

-

{{ _("If you have started a survey, your answers have been saved.") }}

-

{{ _("Contact us if you need to speak to someone about your survey.").format(url = contact_us_url) }}

-{% endblock %} +

{{ _("Sorry, there is a problem with this service") }}

+

{{ _("Try again later.") }}

+

{{ _("If you have started a survey, your answers have been saved.") }}

+ {% if survey_type and (survey_type in SURVEY_TYPES_BUSINESS or survey_type in SURVEY_TYPES_DEFAULT) %} +

{{ business_sign_in }}

+

{{ contact_us }}

+ {% elif survey_type and survey_type in SURVEY_TYPES_SOCIAL or survey_type in SURVEY_TYPES_HEALTH %} +

{{ access_code }}

+

{{ contact_us }}

+ {% else %} +

{{ _("Business surveys") }}

+

{{ business_sign_in }}

+

{{ business_contact_us }}

+

{{ _("All other surveys") }}

+

{{ access_code }}

+

{{ other_contact_us }}

+ {% endif %} +{% endblock main %} diff --git a/templates/errors/_base.html b/templates/errors/_base.html new file mode 100644 index 0000000000..048b29c2bd --- /dev/null +++ b/templates/errors/_base.html @@ -0,0 +1,7 @@ +{% extends "layouts/_base.html" %} + +{% set SURVEY_TYPES_BUSINESS = ["northernireland", "business", "dbt", "dbt-ni", "dbt-dsit", "dbt-dsit-ni", "orr", "desnz", "desnz-ni"] %} +{% set SURVEY_TYPES_DEFAULT = ["default"] %} +{% set SURVEY_TYPES_SOCIAL = ["social"] %} +{% set SURVEY_TYPES_HEALTH = ["health", "ukhsa-ons", "ons-nhs"] %} +{% set SURVEY_TYPES_ALL = SURVEY_TYPES_BUSINESS + SURVEY_TYPES_DEFAULT + SURVEY_TYPES_SOCIAL + SURVEY_TYPES_HEALTH %} diff --git a/templates/errors/error.html b/templates/errors/error.html index f729a96a25..d460b57132 100644 --- a/templates/errors/error.html +++ b/templates/errors/error.html @@ -1,9 +1,9 @@ -{% extends 'layouts/_base.html' %} +{% extends "layouts/_base.html" %} {% block main %} -

{{ heading }}

-{%- if retry_message and retry_url -%} -

{{ retry_message.format(retry_url=retry_url) }}

-{%- endif -%} -

{{ contact_us_message.format(contact_us_url=contact_us_url) }}

-{% endblock %} +

{{ heading }}

+ {%- if retry_message and retry_url -%} +

{{ retry_message.format(retry_url=retry_url) }}

+ {%- endif -%} +

{{ contact_us_message.format(contact_us_url=contact_us_url) }}

+{% endblock main %} diff --git a/templates/errors/previously-submitted.html b/templates/errors/previously-submitted.html index 59080fada2..275bf2e368 100644 --- a/templates/errors/previously-submitted.html +++ b/templates/errors/previously-submitted.html @@ -1,9 +1,10 @@ -{% extends 'layouts/_base.html' %} +{% extends "layouts/_base.html" %} {% set page_title = _("Submission Complete") %} +{% set return_previous = _("Return to previous page").format(url = thank_you_url) %} {% block main %} -

{{ _("This page is no longer available") }}

-

{{ _("Your survey has been submitted") }}

-

{{ _("Return to previous page").format(url = thank_you_url) }}

-{% endblock %} +

{{ _("This page is no longer available") }}

+

{{ _("Your survey has been submitted") }}

+

{{ return_previous }}

+{% endblock main %} diff --git a/templates/errors/submission-failed.html b/templates/errors/submission-failed.html index 0c72c391b6..d734596093 100644 --- a/templates/errors/submission-failed.html +++ b/templates/errors/submission-failed.html @@ -1,11 +1,22 @@ -{% extends 'layouts/_base.html' %} +{% extends "errors/_base.html" %} {% set hide_sign_out_button = True %} - {% set page_title = _("Sorry, there is a problem") %} +{% set resubmit_survey = _("You can try to submit your survey again").format(url=url_for('questionnaire.get_questionnaire')) %} +{% set contact_us = _("If this problem keeps happening, please contact us for help.").format(url=contact_us_url) %} +{% set business_contact_us = _("If you are completing a business survey, please contact us.").format(url=business_contact_us_url) %} +{% set other_contact_us = _("If you started your survey using an access code, please contact us.").format(url=other_contact_us_url) %} {% block main %} -

{{ _("Sorry, there is a problem") }}

-

{{ _("You can try to submit your census again").format(url=url_for('questionnaire.get_questionnaire')) }}

-

{{ _("If this problem keeps happening, please contact us for help.").format(url=contact_us_url) }}

-{% endblock %} +

{{ _("Sorry, there is a problem") }}

+

{{ resubmit_survey }}

+ {% if survey_type and (survey_type in SURVEY_TYPES_ALL) %} +

{{ contact_us }}

+ {% else %} +

{{ _("If this problem keeps happening, please see the following help links.") }}

+

{{ _("Business surveys") }}

+

{{ business_contact_us }}

+

{{ _("All other surveys") }}

+

{{ other_contact_us }}

+ {% endif %} +{% endblock main %} diff --git a/templates/feedback-sent.html b/templates/feedback-sent.html index 0b177ae3d7..92424fc9f3 100644 --- a/templates/feedback-sent.html +++ b/templates/feedback-sent.html @@ -1,34 +1,43 @@ -{% extends 'layouts/_base.html' %} -{% from 'components/panel/_macro.njk' import onsPanel %} -{% from 'components/button/_macro.njk' import onsButton %} +{% extends "layouts/_base.html" %} + +{% from "components/panel/_macro.njk" import onsPanel %} +{% from "components/button/_macro.njk" import onsButton %} {% set hide_sign_out_button = content.hide_sign_out_button %} {% set page_title = _("Feedback sent") %} {% set sign_out_url = content.sign_out_url %} {% block main %} - {% call onsPanel({ - "spacious": true, - "type": "success", - "classes": "ons-u-mb-s", - "iconType": "check", - "iconSize": "xl" - }) - %} - -

{{ _("Thank you for your feedback") }}

-

{{ _("Your comments will help us make improvements to our surveys. We are not able to reply to comments, but we appreciate your feedback") }}

- - {% endcall %} - - {{ - onsButton({ - "attributes": {"data-qa": "btn-done"}, - "type": "button", - "text": _("Done"), - "classes": "ons-u-mb-m", - "url": url_for('post_submission.get_thank_you') - }) - }} - -{% endblock %} + {# djlint:off #} + {% call + onsPanel({ + "spacious": true, + "variant": "success", + "classes": "ons-u-mb-s", + "iconType": "check", + "iconSize": "xl" + }) + %} +

{{ _("Thank you for your feedback") }}

+

+ {{ _("Your comments will help us make improvements to our surveys. We are not able to reply to comments, but we appreciate your feedback") }} +

+ {% endcall %} + {{ + onsButton({ + "type": "button", + "text": _("Done"), + "classes": "ons-u-mb-m", + "url": url_for('post_submission.get_thank_you'), + "attributes": { + "data-qa": "btn-done", + "data-ga-category": "Button", + "data-ga-action": "Submit", + "data-ga-label": "Done", + "data-ga-page": "Feedback", + "data-ga": "click", + } + }) + }} + {# djlint:on #} +{% endblock main %} diff --git a/templates/feedback.html b/templates/feedback.html index a2391977cb..8d2b4abfa7 100644 --- a/templates/feedback.html +++ b/templates/feedback.html @@ -1,48 +1,57 @@ -{% extends 'layouts/_base.html' %} -{% import 'macros/helpers.html' as helpers %} +{% extends "layouts/_base.html" %} + +{% import "macros/helpers.html" as helpers %} + {% from "components/button/_macro.njk" import onsButton %} {% set hide_sign_out_button = content.hide_sign_out_button %} {% set question = content.question %} {% set sign_out_url = content.sign_out_url %} {% set breadcrumbs = { - "ariaLabel": 'Back', - "itemsList": [ - { - "url": url_for("post_submission.get_thank_you"), - "id": "top-previous", - "text": _("Back"), - "attributes": { - "data-ga": 'click', - "data-ga-category": 'Navigation', - "data-ga-action": 'Previous link click' - } - } - ] + "ariaLabel": 'Back', + "itemsList": [ + { + "url": url_for("post_submission.get_thank_you"), + "id": "top-previous", + "text": _("Back"), + "attributes": { + "data-ga": 'click', + "data-ga-category": 'Link', + "data-ga-action": 'Navigate', + "data-ga-label": "Previous", + "data-ga-page": "Feedback", + } + } + ] } %} {% block main %} - {% set form = content.form %} - {% if form.mapped_errors %} - {% set error_title = ngettext('There is a problem with your feedback', 'There are %(num)s problems with your feedback', form.mapped_errors | length) %} - {% include 'partials/error-panel.html' %} - {% endif %} - -

{{ content.question.title }}

- {%- for answer in question.answers -%} - {% include 'partials/answer.html' %} - {%- endfor -%} - - {{ - onsButton({ - "text": _("Send feedback"), - "submitType": 'timer', - "classes": "ons-u-mt-xl", - "attributes": { - "data-qa": "btn-submit" - } - }) - }} -

- -{% endblock %} + {% set form = content.form %} + {% if form.mapped_errors %} + {% set error_title = ngettext('There is a problem with your feedback', 'There are %(num)s problems with your feedback', form.mapped_errors | length) %} + {% include "partials/error-panel.html" %} + {% endif %} +

{{ content.question.title }}

+ {%- for answer in question.answers -%} + {% include "partials/answer.html" %} + {%- endfor -%} + {# djlint:off #} + {{ + onsButton({ + "text": _("Send feedback"), + "variants": 'timer', + "classes": "ons-u-mt-xl", + "attributes": { + "data-qa": "btn-submit", + "data-ga-category": "Button", + "data-ga-action": "Submit", + "data-ga-label": "Send", + "data-ga-page": "Feedback", + "data-ga": "click", + } + }) + }} + {# djlint:on #} +
+
+{% endblock main %} diff --git a/templates/grandcalculatedsummary.html b/templates/grandcalculatedsummary.html new file mode 100644 index 0000000000..4ecb236c77 --- /dev/null +++ b/templates/grandcalculatedsummary.html @@ -0,0 +1,5 @@ +{% extends "layouts/_calculatedsummary.html" %} + +{% block form_title %} +

{{ content.summary.title }}

+{% endblock form_title %} diff --git a/templates/hub.html b/templates/hub.html index 1cb83d3a6e..fe1d942d21 100644 --- a/templates/hub.html +++ b/templates/hub.html @@ -1,26 +1,28 @@ -{% extends 'layouts/_submit.html' %} +{% extends "layouts/_submit.html" %} {% from "components/summary/_macro.njk" import onsSummary %} {% block pre_submit_button_content %} - {% if content.individual_response_url %} - {% set title = _("If you can’t answer someone else’s questions") %} - {% include 'partials/individual-response-guidance.html' %} - {% endif %} + {% if content.individual_response_url %} + {% set title = _("If you can’t answer someone else’s questions") %} + {% include "partials/individual-response-guidance.html" %} + {% endif %} {% endblock pre_submit_button_content %} {% block summary %} - {% if content.rows %} - {{ onsSummary({ - "hub": true, - "classes": "ons-u-mt-m", - "summaries": [{ - "groups": [{ - "headers": ["Name of section or person", "Section progress", "Access section"], - "rows": content.rows - }] - }] - }) - }} - {% endif %} + {% if content.rows %} + {# djlint:off #} + {{ + onsSummary({ + "variant": "hub", + "classes": "ons-u-mt-m", + "summaries": [{ + "groups": [{ + "rows": content.rows + }] + }] + }) + }} + {# djlint:on #} + {% endif %} {% endblock summary %} diff --git a/templates/individual_response/confirmation-post.html b/templates/individual_response/confirmation-post.html index ba0418b190..99d0bab548 100644 --- a/templates/individual_response/confirmation-post.html +++ b/templates/individual_response/confirmation-post.html @@ -1,27 +1,40 @@ -{% extends 'layouts/_base.html' %} -{% from 'components/button/_macro.njk' import onsButton %} -{% from 'components/panel/_macro.njk' import onsPanel %} +{% extends "layouts/_base.html" %} + +{% from "components/button/_macro.njk" import onsButton %} +{% from "components/panel/_macro.njk" import onsPanel %} {% set save_on_signout = true %} {% block main %} - {% call onsPanel({ - "type": "success", - "classes": "ons-u-mb-m", - "iconType": "check", - "iconSize": "l" - }) %} -

- {{ _("A letter has been sent to Individual Resident at {display_address}").format(display_address=display_address) }} -

-

{{ _("The letter with an individual access code should arrive soon for them to complete their own census") }}

+ {# djlint:off #} + {% call + onsPanel({ + "variant": "success", + "classes": "ons-u-mb-m", + "iconType": "check", + "iconSize": "l" + }) + %} +

+ {{ _("A letter has been sent to Individual Resident at {display_address}").format(display_address=display_address) }} +

+

{{ _("The letter with an individual access code should arrive soon for them to complete their own census") }}

{% endcall %} {{ onsButton({ "text": _("Continue"), - "submitType": 'timer', + "variants": 'timer', "classes": 'ons-u-mb-m ons-u-mt-l', - "attributes": {'data-qa': 'btn-submit'} + "attributes": { + 'data-qa': 'btn-submit', + "data-ga-category": "Button", + "data-ga-action": "Submit", + "data-ga-label": "Confirmation Post Continue", + "data-ga-page": "Individual Response", + "data-ga": "click", + } }) }} -{% endblock %} + {# djlint:on #} +{% endblock main %} diff --git a/templates/individual_response/confirmation-text-message.html b/templates/individual_response/confirmation-text-message.html index 7640297dc2..6b35af4b7b 100644 --- a/templates/individual_response/confirmation-text-message.html +++ b/templates/individual_response/confirmation-text-message.html @@ -1,37 +1,53 @@ -{% extends 'layouts/_base.html' %} -{% from 'components/button/_macro.njk' import onsButton %} -{% from 'components/panel/_macro.njk' import onsPanel %} +{% extends "layouts/_base.html" %} + +{% from "components/button/_macro.njk" import onsButton %} +{% from "components/panel/_macro.njk" import onsPanel %} {% set save_on_signout = true %} {% block main %} - {% call onsPanel({ - "type": "success", - "classes": "ons-u-mb-m", - "iconType": "check", - "iconSize": "l" - }) %} -

- {{ _("We have sent a text to {mobile_number}").format(mobile_number=mobile_number) }} -

-

{{ _("The text message with an individual access code should arrive soon for them to complete their own census") }}

+ {# djlint:off #} + {% call + onsPanel({ + "variant": "success", + "classes": "ons-u-mb-m", + "iconType": "check", + "iconSize": "l" + }) + %} +

+ {{ _("We have sent a text to {mobile_number}").format(mobile_number=mobile_number) }} +

+

+ {{ _("The text message with an individual access code should arrive soon for them to complete their own census") }} +

{% endcall %} - - {% call onsPanel({ - "type": "bare", - "classes": "ons-u-mb-s", - "iconType": "lock" + {% call + onsPanel({ + "variant": "bare", + "classes": "ons-u-mb-s", + "iconType": "lock" }) %} -

{{ _("The text will be sent from Census2021") }}

+

+ {{ _("The text will be sent from Census2021") }} +

{% endcall %} - {{ onsButton({ "text": _("Continue"), - "submitType": 'timer', + "variants": 'timer', "classes": 'ons-u-mb-m ons-u-mt-l', - "attributes": {'data-qa': 'btn-submit'} + "attributes": { + 'data-qa': 'btn-submit', + "data-ga-category": "Button", + "data-ga-action": "Submit", + "data-ga-label": "Confirmation Text Message Continue", + "data-ga-page": "Individual Response", + "data-ga": "click", + } }) }} -{% endblock %} + {# djlint:on #} +{% endblock main %} diff --git a/templates/individual_response/interstitial.html b/templates/individual_response/interstitial.html index 2cd76abc88..b069758ec4 100644 --- a/templates/individual_response/interstitial.html +++ b/templates/individual_response/interstitial.html @@ -1,20 +1,31 @@ -{% extends 'layouts/_base.html' %} +{% extends "layouts/_base.html" %} + {% from "components/button/_macro.njk" import onsButton %} {% set save_on_signout = true %} {% block main %} -

{{_("If you can't answer questions for others in your household") }}

-

{{_("You can ask the people you live with to answer their own questions by sharing the household access code with them.") }}

-

{{_("If this is not possible, you can request a separate census for them to complete.") }}

+

{{ _("If you can't answer questions for others in your household") }}

+

+ {{ _("You can ask the people you live with to answer their own questions by sharing the household access code with them.") }} +

+

{{ _("If this is not possible, you can request a separate census for them to complete.") }}

+ {# djlint:off #} {{ onsButton({ "text": _("Request separate census"), - "submitType": 'timer', - "classes": 'ons-u-mb-m ons-u-mt-s', + "variants": "timer", + "classes": "ons-u-mb-m ons-u-mt-s", "url": next_location_url, - "attributes": {'data-qa': 'btn-submit'} + "attributes": { + "data-qa": "btn-submit", + "data-ga-category": "Button", + "data-ga-action": "Submit", + "data-ga-label": "Request Separate Census", + "data-ga": "click", + } }) }} -{% endblock %} + {# djlint:on #} +{% endblock main %} diff --git a/templates/individual_response/question.html b/templates/individual_response/question.html index dd6325e111..16497fad06 100644 --- a/templates/individual_response/question.html +++ b/templates/individual_response/question.html @@ -1,4 +1,4 @@ -{% extends 'question.html' %} +{% extends "question.html" %} {% set save_on_signout = true %} {% set question = content.block.question %} @@ -6,6 +6,8 @@ {% block after_submit_button_content %} {% if show_contact_us_guidance %} -

{{ _("To request a census in a different format or for further help, please contact us").format(contact_us_url=contact_us_url) }}

+

+ {{ _("To request a census in a different format or for further help, please contact us").format(contact_us_url=contact_us_url) }} +

{% endif %} -{% endblock %} +{% endblock after_submit_button_content %} diff --git a/templates/interstitial.html b/templates/interstitial.html index 783255aa09..c59498b0ba 100644 --- a/templates/interstitial.html +++ b/templates/interstitial.html @@ -1,40 +1,45 @@ -{% extends 'layouts/_questionnaire.html' %} -{% from 'macros/helpers.html' import format_paragraphs %} +{% extends "layouts/_questionnaire.html" %} -{% from 'macros/helpers.html' import interviewer_note %} +{% from "macros/helpers.html" import format_paragraphs %} +{% from "macros/helpers.html" import interviewer_note %} {% set save_on_signout = true %} - {% set continue_button_text = _("Continue") %} {% block form_content %} - {% set interstitial_instruction = format_paragraphs(block.content.instruction) %} - {% if block.interviewer_only %} -

{{ interviewer_note(block.content.title) }}

- {% else %} -

{{ block.content.title }}

- {% endif %} - {% if interstitial_instruction %} -
{{ interstitial_instruction | safe }}
- {% endif %} - {% set contents = block.content.contents %} - {% include 'partials/contents.html' %} - {% if content.individual_response_url %} - {% set title = _("If you can’t answer questions for this person") %} - {% include 'partials/individual-response-guidance.html' %} - {% endif %} - + {% set interstitial_instruction = format_paragraphs(block.content.instruction) %} + {% if block.interviewer_only %} +

{{ interviewer_note(block.content.title) }}

+ {% else %} +

{{ block.content.title }}

+ {% endif %} + {% if interstitial_instruction %} +
{{ interstitial_instruction | safe }}
+ {% endif %} + {% set contents = block.content.contents %} + {% include "partials/contents.html" %} + {% if content.individual_response_url %} + {% set title = _("If you can’t answer questions for this person") %} + {% include "partials/individual-response-guidance.html" %} + {% endif %} {% endblock form_content %} {% block submit_button %} + {# djlint:off #} {{ onsButton({ "text": continue_button_text | default(_("Save and continue")), - "submitType": 'timer', + "variants": 'timer', "classes": "ons-u-mt-l", "attributes": { - "data-qa": "btn-submit" + "data-qa": "btn-submit", + "data-ga-category": "Button", + "data-ga-action": "Submit", + "data-ga-label": "Save and Continue", + "data-ga-page": "Interstitial", + "data-ga": "click", } }) }} -{% endblock %} + {# djlint:on #} +{% endblock submit_button %} diff --git a/templates/introduction.html b/templates/introduction.html index 3327110483..30d3d638a6 100644 --- a/templates/introduction.html +++ b/templates/introduction.html @@ -1,42 +1,42 @@ -{% extends 'layouts/_base.html' %} +{% extends "layouts/_base.html" %} -{% import 'macros/helpers.html' as helpers %} +{% import "macros/helpers.html" as helpers %} {% set form = content.form %} - {# Test failing because section title is missing #} {# {% set page_title =
+ ' - ' + page_title %} #} - {% set page_title = _("Introduction") %} {% block main %} - {% block intro_content %} - {% if content.block.primary_content %} - {% for content_block in content.block.primary_content %} - {% include 'partials/introduction/basic.html' %} - {% endfor %} - {% endif %} - - {%- if legal_basis -%} -

{{ _("Your response is legally required") }}

-

{{ legal_basis }}

- {%- endif -%} - - {% block start_survey %} - {% include 'partials/introduction/start-survey.html' %} - {% endblock start_survey %} - - {% if content.block.preview_content %} - {% set intro = content.block.preview_content %} - {% include 'partials/introduction/preview.html' %} - {% endif %} - - {% if content.block.secondary_content %} - {% for content_block in content.block.secondary_content %} - {% include 'partials/introduction/basic.html' %} - {% endfor %} - {% endif %} + {% if content.block.primary_content %} + {% for content_block in content.block.primary_content %} + {% set title_tag = "h1" %} + {% include "partials/introduction/basic.html" %} + {% endfor %} + {% endif %} + {% if preview_enabled %} +
+ {{ _("View the questions you will be asked in this survey").format(url=url_for('questionnaire.get_preview')) }} +
+ {% endif %} + {%- if legal_basis -%} +

{{ _("Your response is legally required") }}

+

{{ legal_basis }}

+ {%- endif -%} + + {% block start_survey %} + {% include "partials/introduction/start-survey.html" %} + {% endblock start_survey %} + {% if content.block.preview_content %} + {% set intro = content.block.preview_content %} + {% include "partials/introduction/preview.html" %} + {% endif %} + {% if content.block.secondary_content %} + {% for content_block in content.block.secondary_content %} + {% set title_tag = "h2" %} + {% include "partials/introduction/basic.html" %} + {% endfor %} + {% endif %} {% endblock intro_content %} - -{% endblock %} +{% endblock main %} diff --git a/templates/layouts/_base.html b/templates/layouts/_base.html index 05737e4d41..fa7c601f73 100644 --- a/templates/layouts/_base.html +++ b/templates/layouts/_base.html @@ -1,166 +1,156 @@ {% extends "layout/_template.njk" %} + {% from "components/cookies-banner/_macro.njk" import onsCookiesBanner %} {% from "components/skip-to-content/_macro.njk" import onsSkipToContent %} {% from "components/timeout-modal/_macro.njk" import onsTimeoutModal %} -{% set form = { - "attributes": { - "autocomplete": "off", - "novalidate": null - } -} %} - {% if previous_location_url %} - {% set breadcrumbs = { - "ariaLabel": 'Previous', - "itemsList": [ - { - "url": previous_location_url, - "id": "top-previous", - "text": _("Previous"), - "attributes": { - "data-ga": 'click', - "data-ga-category": 'Navigation', - "data-ga-action": 'Previous link click' - } - } - ] - } %} + {# djlint:off #} + {% set breadcrumbs = { + "ariaLabel": 'Previous', + "itemsList": [ + { + "url": previous_location_url, + "id": "top-previous", + "text": _("Previous"), + "attributes": { + "data-ga": "click", + "data-ga-category": "Link", + "data-ga-action": "Navigate", + "data-ga-label": "Previous", + "data-ga-page": "Questionnaire", + } + } + ] + } %} + {# djlint:on #} {% endif %} - {% if survey_title %} - {% set full_page_title = page_title ~ " - " ~ survey_title %} + {% set full_page_title = page_title ~ " - " ~ survey_title %} {% else %} - {% set full_page_title = page_title %} + {% set full_page_title = page_title %} {% endif %} - {% set pageConfig = { - "title": full_page_title, - "header": page_header, - "serviceLinks": service_links, - "footer": footer, - "language": languages, - "cdn": { - "url": cdn_url - }, - "breadcrumbs": breadcrumbs, - "cspNonce": csp_nonce + "title": full_page_title, + "pageColNumber": 8, + "footer": footer, + "cdn": { + "url": cdn_url + }, + "breadcrumbs": breadcrumbs, + "cspNonce": csp_nonce } %} - {% if theme %} - {% do pageConfig.update({"theme":theme}) %} + {% do pageConfig.update({"theme":theme}) %} {% endif %} - -{% include 'layouts/configs/_save-sign-out-button.html' %} - - +{% include "layouts/configs/_header.html" %} {# if there is not a previous link add extra margin top to the page #} {% if not previous_location_url %} - {% set pageClasses = pageClasses + " ons-u-mt-m" if pageClasses else "ons-u-mt-m" %} + {% set pageClasses = pageClasses + " ons-u-mt-m" if pageClasses else "ons-u-mt-m" %} {% endif %} {% block preHeader %} - {% if include_csrf_token %} - - {% endif %} - - {% if cookie_settings_url %} - {{ - onsCookiesBanner({ - "secondaryButtonUrl": cookie_settings_url, - "statementTitle": _('Tell us whether you accept cookies'), - "statementText": _("We use cookies to collect information about how you use census.gov.uk. We use this information to make the website work as well as possible and improve our services.").format(cookie_settings_url=cookie_settings_url), - "confirmationText": _("You’ve accepted all cookies. You can change your cookie preferences at any time.").format(cookie_settings_url=cookie_settings_url), - "primaryButtonText": _('Accept all cookies'), - "secondaryButtonText": _('Set cookie preferences'), - "confirmationButtonText": _('Hide'), - }) - }} - {% endif %} -{% endblock %} - + {# djlint:off #} + {% if include_csrf_token %} + + {% endif %} + {% if cookie_settings_url and cookie_domain %} + {{ onsCookiesBanner({ + "secondaryButtonUrl": cookie_settings_url, + "statementTitle": _('Tell us whether you accept cookies') , + "statementText": _("We use cookies to collect information about how you use {cookie_domain}. We use this information to make the website work as well as possible and improve our services.").format(cookie_settings_url=cookie_settings_url, cookie_domain=cookie_domain), + "confirmationText": _("You’ve accepted all cookies. You can change your cookie preferences at any time.").format(cookie_settings_url=cookie_settings_url), + "primaryButtonText": _('Accept all cookies'), + "secondaryButtonText": _('Set cookie preferences'), + "confirmationButtonText": _('Hide'), + 'lang': language_code + }) + }} + {% endif %} + {# djlint:on #} +{% endblock preHeader %} {% block head %} - {% if google_tag_manager_id and google_tag_manager_auth %} - - - - - - - {% endif %} -{% endblock %} - - -{% block bodyStart %} - {% if google_tag_manager_id and google_tag_manager_auth %} - - - - {% endif %} -{% endblock %} - - -{% block skipLink %} - {{ - onsSkipToContent({ - "url": "#main-content", - "text": _("Skip to main content") - }) - }} -{% endblock %} - + {# djlint:off #} + {% if google_tag_id %} + + + {% endif %} + {# djlint:on #} +{% endblock head %} + +{% block bodyStart %}
{% endblock bodyStart %} + + {% block skipLink %} + {# djlint:off #} + {{ + onsSkipToContent({ + "url": "#main-content", + "text": _("Skip to main content") + }) + }} + {# djlint:on #} + {% endblock skipLink %} + + {% block bodyEnd %}
{% endblock bodyEnd %} {% block scripts %} - {% if config['EQ_ENABLE_LIVE_RELOAD'] %} - - {% endif %} -{% endblock %} + {% if config['EQ_ENABLE_LIVE_RELOAD'] %} + + {% endif %} +{% endblock scripts %} {% block preFooter %} - {% if session_expires_at %} +{# djlint:off #} + {% if session_expires_at %} + {{ - onsTimeoutModal({ - "showModalTimeInSeconds": 60, - "serverSessionExpiryEndpoint": url_for('session.session_expiry'), - "sessionExpiresAt": session_expires_at, - "redirectUrl": url_for('session.get_session_expired'), - "title": _("You will be signed out soon"), - "textFirstLine": _("It appears you have been inactive for a while."), - "countdownText": _("To protect your information, your progress will be saved and you will be signed out in"), - "countdownExpiredText": _("You are being signed out"), - "btnText": _("Continue survey"), - "minutesTextSingular": _("minute"), - "minutesTextPlural": _("minutes"), - "secondsTextSingular": _("second"), - "secondsTextPlural": _("seconds"), - "endWithFullStop": true - }) + onsTimeoutModal({ + "showModalTimeInSeconds": 60, + "serverSessionExpiryEndpoint": url_for('session.session_expiry') , + "sessionExpiresAt": session_expires_at, + "redirectUrl": url_for('session.get_session_expired'), + "title": _("You will be signed out soon"), + "textFirstLine": _("It appears you have been inactive for a while."), + "countdownText": _("To protect your information, your progress will be saved and you will be signed out in"), + "countdownExpiredText": _("You are being signed out"), + "btnText": _("Continue survey"), + "minutesTextSingular": _("minute"), + "minutesTextPlural": _("minutes"), + "secondsTextSingular": _("second"), + "secondsTextPlural": _("seconds"), + "endWithFullStop": true + }) }} - {% endif %} -{% endblock %} + {% endif %} +{# djlint:on #} +{% endblock preFooter %} diff --git a/templates/layouts/_calculatedsummary.html b/templates/layouts/_calculatedsummary.html new file mode 100644 index 0000000000..07e2681d7d --- /dev/null +++ b/templates/layouts/_calculatedsummary.html @@ -0,0 +1,30 @@ +{% extends "layouts/_questionnaire.html" %} + +{% from "components/button/_macro.njk" import onsButton %} + +{% set save_on_signout = true %} + +{% block form_content %} + {% block form_title %} + {% endblock form_title %} +
{% include "partials/summary/summary.html" %}
+{% endblock form_content -%} + +{% block submit_button %} + {# djlint:off #} + {{ + onsButton({ + "text": _("Yes, I confirm this is correct"), + "variants": 'timer', + "attributes": { + "data-qa": "btn-submit", + "data-ga-category": "Button", + "data-ga-action": "Submit", + "data-ga-label": "Confirm", + "data-ga-page": "Calculated Summary", + "data-ga": "click", + } + }) + }} + {# djlint:on #} +{% endblock submit_button %} diff --git a/templates/layouts/_questionnaire.html b/templates/layouts/_questionnaire.html index ef8b0407c0..c1f4563e6e 100644 --- a/templates/layouts/_questionnaire.html +++ b/templates/layouts/_questionnaire.html @@ -1,41 +1,51 @@ -{% extends 'layouts/_base.html' %} +{% extends "layouts/_base.html" %} + {% from "components/button/_macro.njk" import onsButton %} -{% from "components/cookies-banner/_macro.njk" import onsCookiesBanner %} {% set block = content.block %} {% set form = content.form %} {% set last_viewed_question_guidance = content.last_viewed_question_guidance %} {% block main %} - - - {% block form_errors %}{% endblock %} + - {% block form_content %} -
- {% if last_viewed_question_guidance %} - {% include 'partials/last_viewed_question_guidance.html' %} - {% endif %} - {% include 'partials/block.html' %} -
- {% endblock %} + {% block form_errors %} + {% endblock form_errors %} - {% block submit_button %} - {{ - onsButton({ - "submitType": "timer", - "text": continue_button_text | default(_("Save and continue")), - "attributes": { - "data-qa": "btn-submit" - } - }) - }} - {% endblock %} + {% block form_content %} +
+ {% if last_viewed_question_guidance %} + {% include "partials/last_viewed_question_guidance.html" %} + {% endif %} + {% include "partials/block.html" %} +
+ {% endblock form_content %} - {% block after_submit_button_content %} - {% if content.return_to_hub_url %} -

{{ _("Choose another section and return to this later") }}

- {% endif %} - {% endblock %} + {% block submit_button %} + {# djlint:off #} + {{ + onsButton({ + "variants": 'timer', + "text": continue_button_text | default(_("Save and continue") ), + "attributes": { + "data-qa": "btn-submit", + "data-ga-category": "Button", + "data-ga-action": "Submit", + "data-ga-label": "Save and Continue", + "data-ga-page": "Questionnaire", + "data-ga": "click", + } + }) + }} + {# djlint:on #} + {% endblock submit_button %} -{% endblock %} + {% block after_submit_button_content %} + {% if content.return_to_hub_url %} +

+ {{ _("Choose another section and return to this later") }} +

+ {% endif %} + {% endblock after_submit_button_content %} +{% endblock main %} diff --git a/templates/layouts/_submit.html b/templates/layouts/_submit.html index f9a0c078a4..6737b944b1 100644 --- a/templates/layouts/_submit.html +++ b/templates/layouts/_submit.html @@ -1,4 +1,4 @@ -{% extends 'layouts/_base.html' %} +{% extends "layouts/_base.html" %} {% from "components/panel/_macro.njk" import onsPanel %} {% from "components/button/_macro.njk" import onsButton %} @@ -6,42 +6,50 @@ {% set save_on_signout = true %} {% block main %} - {% block page_title %} -

{{ content.title }}

- {% endblock page_title %} - - {% block post_title_panel %} - {% if content.warning %} - {% call onsPanel({ "type": "warn"}) %} -

{{ content.warning }}

- {% endcall %} + {% block page_title %} +

{{ content.title }}

+ {% endblock page_title %} + + {% block post_title_panel %} + {% if content.warning %} + {# djlint:off #} + {% call onsPanel({ "variant": "warn" }) %} +

{{ content.warning }}

+ {% endcall %} + {# djlint:on #} + {% endif %} + {% endblock post_title_panel %} + + {% block summary %} + {% endblock summary %} + {% if content.guidance %} +
+

+ {{ content.guidance }} +

+
{% endif %} - {% endblock post_title_panel %} - - {% block summary %} - {% endblock summary %} - - {% if content.guidance %} -
-

- {{ content.guidance }} -

-
- {% endif %} - - {% block pre_submit_button_content %} - {% endblock pre_submit_button_content %} - {% block submit_button %} - {{ - onsButton({ - "text": content.submit_button, - "submitType": 'timer', - "classes": 'ons-u-mb-m ons-u-mt-' ~ ("s" if content.guidance else "xl"), - "attributes": { - 'data-qa': 'btn-submit' - } - }) - }} - {% endblock submit_button %} -{% endblock %} + {% block pre_submit_button_content %} + {% endblock pre_submit_button_content %} + + {% block submit_button %} + {# djlint:off #} + {{ + onsButton({ + "text": content.submit_button, + "variants": 'timer', + "classes": 'ons-u-mb-m ons-u-mt-' ~ ("s" if content.guidance else "xl"), + "attributes": { + "data-qa": "btn-submit", + "data-ga-category": "Button", + "data-ga-action": "Submit", + "data-ga-label": "Submit", + "data-ga-page": "Questionnaire", + "data-ga": "click", + } + }) + }} + {# djlint:on #} + {% endblock submit_button %} +{% endblock main %} diff --git a/templates/layouts/configs/_header.html b/templates/layouts/configs/_header.html new file mode 100644 index 0000000000..b38383e5e3 --- /dev/null +++ b/templates/layouts/configs/_header.html @@ -0,0 +1,43 @@ +{% if current_user.is_authenticated and not hide_sign_out_button %} + {% if save_on_signout %} + {% set sign_out_url = url_for('session.get_sign_out', internal_redirect=true) %} + {% set header_button_text = sign_out_button_text %} + {% set header_button_data_qa = "btn-save-sign-out" %} + {% set header_button_data_ga_action = "Save and Exit" %} + {% set header_button_data_ga_label = "Save and Exit Survey" %} + {% else %} + {% set sign_out_url = url_for('session.get_sign_out', todo=true) %} + {% set header_button_text = _("Exit") %} + {% set header_button_data_qa = "btn-exit" %} + {% set header_button_data_ga_action = "Exit" %} + {% set header_button_data_ga_label = "Exit Survey" %} + {% endif %} + {# djlint:off #} + {% set signout_button = { + "text": header_button_text, + "url": sign_out_url, + "attributes": { + "data-qa": header_button_data_qa, + "data-ga-category": "Button", + "data-ga-action": header_button_data_ga_action, + "data-ga-label": header_button_data_ga_label, + "data-ga-page": "Questionnaire", + "data-ga": "click", + }, + "iconType": "exit", + "iconPosition": "after" + } %} + {# djlint:on #} +{% endif %} +{# djlint:off #} +{% do pageConfig | setAttribute("header", { + "title": survey_title, + "signoutButton": signout_button, + "language": languages, + "serviceLinks": service_links, + "mastheadLogo": { + "large": masthead_logo, + "small": masthead_logo_mobile + } +}) %} +{# djlint:on #} diff --git a/templates/layouts/configs/_save-sign-out-button.html b/templates/layouts/configs/_save-sign-out-button.html deleted file mode 100644 index faa7b6f3ca..0000000000 --- a/templates/layouts/configs/_save-sign-out-button.html +++ /dev/null @@ -1,36 +0,0 @@ -{% if current_user.is_authenticated and not hide_sign_out_button %} - {% set sign_out_url = url_for('session.get_sign_out', todo=true) %} - - {% if save_on_signout %} - {% do pageConfig | setAttribute("signoutButton", { - "text": sign_out_button_text, - "name": "btn-save-sign-out", - "url": sign_out_url, - "attributes": { - "data-qa": "btn-save-sign-out", - "data-ga": "click", - "data-ga-category": "Navigation", - "data-ga-action": "Save and sign out click" - }, - "iconType": "exit", - "iconPosition": "after" - }) %} - - {% else %} - {% do pageConfig | setAttribute("signoutButton", { - "text": _("Exit"), - "name": "btn-exit", - "url": sign_out_url, - "attributes": { - "data-qa": "btn-exit", - "data-ga": "click", - "data-ga-category": "Navigation", - "data-ga-action": "Exit click" - }, - "iconType": "exit", - "iconPosition": 'after' - }) %} - - {% endif %} - -{% endif %} diff --git a/templates/list-action.html b/templates/list-action.html index bc8f87adbb..67a7a4f4bb 100644 --- a/templates/list-action.html +++ b/templates/list-action.html @@ -1,12 +1,13 @@ -{% extends 'question.html' %} +{% extends "question.html" %} {% block after_submit_button_content %} - {% if previous_location_url %} -

- {% if content.block.cancel_text %} - {{ content.block.cancel_text }}
- {% endif %} - {{ _("Cancel and return to the previous page") }} -

- {% endif %} + {% if previous_location_url %} +

+ {% if content.block.cancel_text %} + {{ content.block.cancel_text }} +
+ {% endif %} + {{ _("Cancel and return to the previous page") }} +

+ {% endif %} {% endblock after_submit_button_content %} diff --git a/templates/listaddquestion.html b/templates/listaddquestion.html index d8516f06da..475fefb203 100644 --- a/templates/listaddquestion.html +++ b/templates/listaddquestion.html @@ -1 +1 @@ -{% extends 'list-action.html' %} +{% extends "list-action.html" %} diff --git a/templates/listcollector.html b/templates/listcollector.html index 118134653c..2f61a13acd 100644 --- a/templates/listcollector.html +++ b/templates/listcollector.html @@ -1 +1 @@ -{% include 'question.html' %} +{% include "question.html" %} diff --git a/templates/listcollectorcontent.html b/templates/listcollectorcontent.html new file mode 100644 index 0000000000..e7a23f646e --- /dev/null +++ b/templates/listcollectorcontent.html @@ -0,0 +1,30 @@ +{% extends "layouts/_questionnaire.html" %} + +{% import "macros/helpers.html" as helpers %} + +{% from "components/list/_macro.njk" import onsList %} + +{% set save_on_signout = true %} +{% set continue_button_text = _("Continue") %} +{% set title = content.title %} +{% set contents = content.contents %} + +{% block form_content %} +

{{ title }}

+ {%- if content.list and content.list.list_items -%} + {% set list = content.list.list_items %} +
+ {# djlint:off #} + {% set itemsList = map_list_config(list) %} + {{ + onsList({ + "variants": "summary", + "iconPosition": "before", + "itemsList": itemsList, + }) + }} + {# djlint:on #} +
+ {%- endif -%} + {% include "partials/contents.html" %} +{% endblock form_content %} diff --git a/templates/listcollectordrivingquestion.html b/templates/listcollectordrivingquestion.html index 118134653c..2f61a13acd 100644 --- a/templates/listcollectordrivingquestion.html +++ b/templates/listcollectordrivingquestion.html @@ -1 +1 @@ -{% include 'question.html' %} +{% include "question.html" %} diff --git a/templates/listeditquestion.html b/templates/listeditquestion.html index d8516f06da..475fefb203 100644 --- a/templates/listeditquestion.html +++ b/templates/listeditquestion.html @@ -1 +1 @@ -{% extends 'list-action.html' %} +{% extends "list-action.html" %} diff --git a/templates/listremovequestion.html b/templates/listremovequestion.html index f905bbdde1..b1294137a4 100644 --- a/templates/listremovequestion.html +++ b/templates/listremovequestion.html @@ -1,7 +1,7 @@ -{% extends 'list-action.html' %} +{% extends "list-action.html" %} {% if content.individual_response_enabled %} - {% set show_individual_response_guidance = True %} + {% set show_individual_response_guidance = True %} {% endif %} {% block after_submit_button_content %} diff --git a/templates/listrepeatingquestion.html b/templates/listrepeatingquestion.html new file mode 100644 index 0000000000..475fefb203 --- /dev/null +++ b/templates/listrepeatingquestion.html @@ -0,0 +1 @@ +{% extends "list-action.html" %} diff --git a/templates/macros/helpers.html b/templates/macros/helpers.html index 4d3744d670..6283c21509 100644 --- a/templates/macros/helpers.html +++ b/templates/macros/helpers.html @@ -1,14 +1,16 @@ {% macro include_with(path, data, dataHandle='data') %} - {% with dataHandle = data %}{% include path %}{% endwith %} + {% with dataHandle = data %} + {% include path %} + {% endwith %} {% endmacro %} - {%- macro format_paragraphs(paragraphs) -%} - {%- for paragraph in paragraphs -%} -

{{paragraph}}

- {%- else -%} - {%- endfor -%} + {%- for paragraph in paragraphs -%} + {%- if paragraph -%} +

{{ paragraph }}

+ {%- endif -%} + {%- else -%} + {%- endfor -%} {%- endmacro -%} - {%- macro interviewer_note(title=None) -%} -{{_("Interviewer note:")}}{{ title }} + {{ _("Interviewer note:") }}{{ " " + title }} {%- endmacro -%} diff --git a/templates/multiple_survey.html b/templates/multiple_survey.html index eb0d75ab30..40efd3cb98 100644 --- a/templates/multiple_survey.html +++ b/templates/multiple_survey.html @@ -1,19 +1,25 @@ -{% extends 'layouts/_base.html' %} +{% extends "layouts/_base.html" %} + {% from "components/panel/_macro.njk" import onsPanel %} {% set page_title = _("Information") %} {% block main %} -

{{ _("Information") }}

- - {% call onsPanel({ - "type": "error", - "spacious": true, - "attributes": { - "data-qa": "multiple-survey-error" - } - }) %} -

{{ _("Unfortunately you can only complete one survey at a time") }}.

-

{{ _("Close this window to continue with your current survey") }}.

- {% endcall %} -{% endblock %} +

{{ _("Information") }}

+ {# djlint:off #} + {% call + onsPanel({ + "variant": "error", + "spacious": true, + "attributes": { + "data-qa": "multiple-survey-error" + } + }) + %} +

+ {{ _("Unfortunately you can only complete one survey at a time") }}. +

+

{{ _("Close this window to continue with your current survey") }}.

+ {% endcall %} + {# djlint:on #} +{% endblock main %} diff --git a/templates/partials/answer-guidance.html b/templates/partials/answer-guidance.html index 67f75393eb..e1d9941748 100644 --- a/templates/partials/answer-guidance.html +++ b/templates/partials/answer-guidance.html @@ -1,33 +1,27 @@ -{% from "components/collapsible/_macro.njk" import onsCollapsible %} +{% from "components/details/_macro.njk" import onsDetails %} -{% call onsCollapsible({ - "id": "answer-guidance-" ~ answer.id, - "classes": "ons-u-mt-s", - "title": _(answer_guidance.schema_item.show_guidance), - "headingAttributes": { - "data-ga": "click", - "data-ga-category": "Answer guidance", - "data-ga-action": "Open panel", - "data-ga-label": _(answer_guidance.schema_item.show_guidance), - "data-qa": "answer-guidance-" ~ answer.id ~ "-title" - }, - "contentAttributes": { - "data-qa": "answer-guidance-" ~ answer.id ~ "-content" - }, - "button": { - "close": _("Hide this"), - "contextSuffix": "content", - "attributes": { - "data-ga": "click", - "data-ga-category": "Answer guidance", - "data-ga-action": "Close panel", - "data-ga-label": _(answer_guidance.schema_item.show_guidance), - "data-qa": "answer-guidance-" ~ answer.id ~ "-button" - } - } -}) %} -
- {% set content_block = answer_guidance.schema_item %} - {% include 'partials/content-block.html' %} -
+{# djlint:off #} +{% call + onsDetails({ + "id": "answer-guidance-" ~ answer.id, + "classes": "ons-u-mt-s ons-u-mb-m", + "title": _(answer_guidance.schema_item.show_guidance), + "headingAttributes": { + "data-ga": "click", + "data-ga-category": "Accordion", + "data-ga-action": "View", + "data-ga-label": "Answer Guidance", + "data-ga-page": "Questionnaire", + "data-qa": "answer-guidance-" ~ answer.id ~ "-title", + }, + "contentAttributes": { + "data-qa": "answer-guidance-" ~ answer.id ~ "-content" + } + }) +%} +
+ {% set content_block = answer_guidance.schema_item %} + {% include "partials/content-block.html" %} +
{% endcall %} +{# djlint:on #} diff --git a/templates/partials/answer.html b/templates/partials/answer.html index 0de9ee3f49..d4ca1a78fa 100644 --- a/templates/partials/answer.html +++ b/templates/partials/answer.html @@ -1,34 +1,40 @@ -{% import 'macros/helpers.html' as helpers %} +{% import "macros/helpers.html" as helpers %} + {% set form = content.form %} {% set errors = form.answer_errors[answer.id] %} {% from "components/error/_macro.njk" import onsError %} {% if render_guidance != False %} - {%- set answer_guidance %} - {% if answer.guidance %} - {% with answer_guidance = { - 'id': answer.id, - 'label': answer.label, - 'schema_item': answer.guidance - } %} - {% include 'partials/answer-guidance.html' %} - {% endwith %} - {% endif %} - {% endset -%} + {# djlint:off #} + {%- set answer_guidance %} + {% if answer.guidance %} + {% with answer_guidance = { + "id": answer.id, + "label": answer.label, + "schema_item": answer.guidance + } %} + {% include "partials/answer-guidance.html" %} + {% endwith %} + {% endif %} + {% endset -%} + {# djlint:on #} {% endif %} - {% if errors | length > 0 %} - {% set error = { - "text": errors[0], - "id": answer.id ~ '-error', - "attributes": { - "data-ga": "error", - "data-ga-category": "Error", - "data-ga-action": answer.type, - "data-ga-label": question.id - } - } %} + {# djlint:off #} + {% set error = { + "text": errors[0], + "id": answer.id ~ '-error', + "attributes": { + "data-ga": "error", + "data-ga-category": "Error", + "data-ga-action": "Display", + "data-ga-label": "Answer Error", + "data-ga-type": answer.type, + "data-ga-identifier": question.id, + "data-ga-page": "Questionnaire", + } + } %} + {# djlint:on #} {% endif %} - -{% include 'partials/answers/' ~ answer.type|lower ~ '.html' %} +{% include "partials/answers/" ~ answer.type|lower ~ ".html" %} {{ answer_guidance }} diff --git a/templates/partials/answers/address.html b/templates/partials/answers/address.html index d3512efcb1..382880aff5 100644 --- a/templates/partials/answers/address.html +++ b/templates/partials/answers/address.html @@ -1,7 +1,6 @@ {% from "components/address-input/_macro.njk" import onsAddressInput %} {% set address_form = form.fields[answer.id] %} - {% set config = { "id": answer.id, "dontWrap": true, @@ -23,48 +22,48 @@ "value": address_form.postcode._value() | e }, "uprn": { - "value": address_form.uprn._value() | e + "value": address_form.uprn._value() | e } } %} - +{# djlint:off #} {% if answer.lookup_options and address_lookup_api_url %} - {% set config = config | setAttributes({ - "label": { - "text": _("Enter address or postcode and select from results") - }, - "searchButton": _("Search for an address"), - "manualLinkText": _("Manually enter address"), - "isEditable": true, - "mandatory": answer.mandatory, - "APIDomain": address_lookup_api_url, - "APIDomainBearerToken": content.address_lookup_api_auth_token, - "instructions": _("Use up and down keys to navigate suggestions once you’ve typed more than two characters. Use the enter key to select a suggestion. Touch device users, explore by touch or with swipe gestures."), - "ariaYouHaveSelected": _("You have selected"), - "ariaMinChars": _("Enter 3 or more characters for suggestions."), - "ariaOneResult": _("There is one suggestion available."), - "ariaNResults": _("There are {n} suggestions available."), - "ariaLimitedResults": _("Results have been limited to 10 suggestions. Type more characters to improve your search"), - "ariaGroupedResults": _("There are {n} for {x}"), - "groupCount": _("{n} addresses"), - "moreResults": _("Enter more of the address to improve results"), - "resultsTitle": _("Select an address"), - "noResults": _("No results found. Try entering a different part of the address"), - "tooManyResults": _("{n} results found. Enter more of the address to improve results"), - "typeMore": _("Enter more of the address to get results"), - "autocomplete": "new-password", - "errorTitle": ngettext('There is a problem with your answer', 'There are %(num)s problems with your answer', 1), - "errorMessageEnter": _("Enter an address"), - "errorMessageSelect": _("Select or manually enter an address"), - "errorMessageAPI": _("Sorry, there was a problem loading addresses"), - "errorMessageAPILinkText": _("Enter address manually"), - "options": { - "regionCode": answer.lookup_options.region_code | lower, - "oneYearAgo": answer.lookup_options.one_year_ago if answer.lookup_options.one_year_ago is defined, - "addressType": answer.lookup_options.address_type | lower - } - }) %} + {% set config = config | setAttributes({ + "label": { + "text": _("Enter address or postcode and select from results") + }, + "searchButton": _("Search for an address"), + "manualLinkText": _("Manually enter address"), + "isEditable": true, + "mandatory": answer.mandatory, + "APIDomain": address_lookup_api_url, + "APIDomainBearerToken": content.address_lookup_api_auth_token, + "instructions": _("Use up and down keys to navigate suggestions once you’ve typed more than two characters. Use the enter key to select a suggestion. Touch device users, explore by touch or with swipe gestures."), + "ariaYouHaveSelected": _("You have selected"), + "ariaMinChars": _("Enter 3 or more characters for suggestions."), + "ariaOneResult": _("There is one suggestion available."), + "ariaNResults": _("There are {n} suggestions available."), + "ariaLimitedResults": _("Results have been limited to 10 suggestions. Type more characters to improve your search"), + "ariaGroupedResults": _("There are {n} for {x}"), + "groupCount": _("{n} addresses"), + "moreResults": _("Enter more of the address to improve results"), + "resultsTitle": _("Select an address"), + "noResults": _("No results found. Try entering a different part of the address"), + "tooManyResults": _("{n} results found. Enter more of the address to improve results"), + "typeMore": _("Enter more of the address to get results"), + "autocomplete": "new-password", + "errorTitle": ngettext('There is a problem with your answer', 'There are %(num)s problems with your answer', 1), + "errorMessageEnter": _("Enter an address"), + "errorMessageSelect": _("Select or manually enter an address"), + "errorMessageAPI": _("Sorry, there was a problem loading addresses"), + "errorMessageAPILinkText": _("Enter address manually"), + "options": { + "regionCode": answer.lookup_options.region_code | lower, + "oneYearAgo": answer.lookup_options.one_year_ago if answer.lookup_options.one_year_ago is defined, + "addressType": answer.lookup_options.address_type | lower + } + }) %} {% else %} - {% set config = config | setAttribute("manualEntry", true) %} + {% set config = config | setAttribute("manualEntry", true) %} {% endif %} - {{ onsAddressInput(config) }} +{# djlint:on #} diff --git a/templates/partials/answers/checkbox.html b/templates/partials/answers/checkbox.html index f485c5ea4a..599aa5d2ef 100644 --- a/templates/partials/answers/checkbox.html +++ b/templates/partials/answers/checkbox.html @@ -1,16 +1,16 @@ {% from "components/checkboxes/_macro.njk" import onsCheckboxes %} {% set config = { - "legend": answer.label, - "dontWrap": not answer.label, - "id": answer.id, - "checkboxes": map_select_config(form, answer), - "mutuallyExclusive": mutuallyExclusive, - "error": error + "legend": answer.label, + "dontWrap": not answer.label, + "id": answer.id, + "checkboxes": map_select_config(form, answer), + "mutuallyExclusive": mutuallyExclusive, + "error": error } %} - {% if answer.instruction != None and answer.options|length > 1 %} - {% set config = config | setAttribute("checkboxesLabel", answer.instruction or _("Select all that apply")) %} + {% set config = config | setAttribute("checkboxesLabel", answer.instruction or _("Select all that apply")) %} {% endif %} - +{# djlint:off #} {{ onsCheckboxes(config) }} +{# djlint:on #} diff --git a/templates/partials/answers/currency.html b/templates/partials/answers/currency.html index f2587c2f7e..f46c3e1bd6 100644 --- a/templates/partials/answers/currency.html +++ b/templates/partials/answers/currency.html @@ -1,26 +1,30 @@ {% from "components/input/_macro.njk" import onsInput %} {% set input = form.fields[answer['id']] %} - -{{ onsInput({ - "id": answer.id, - "type": "number", - "name": answer.id, - "value": input._value() | e, - "label": { - "id": answer.id ~ "-label", - "text": answer.label, - "description": answer.description - }, - "prefix": { - "title": answer.currency, - "text": get_currency_symbol(answer.currency) - }, - "attributes": { - "data-qa": "input-text" - }, - "dontWrap": true if mutuallyExclusive else false, - "mutuallyExclusive": mutuallyExclusive, - "width": get_width_for_number(answer), - "error": error -}) }} +{# djlint:off #} +{{ + onsInput({ + "id": answer.id, + "type": "number", + "name": answer.id, + "value": input._value() | e, + "label": { + "id": answer.id ~ "-label", + "text": answer.label, + "description": answer.description + }, + "prefix": { + "id": answer.id ~ "-type", + "title": answer.currency, + "text": get_currency_symbol(answer.currency) + }, + "attributes": { + "data-qa": "input-text" + }, + "dontWrap": true if mutuallyExclusive else false, + "mutuallyExclusive": mutuallyExclusive, + "width": get_width_for_number(answer), + "error": error + }) +}} +{# djlint:on #} diff --git a/templates/partials/answers/date.html b/templates/partials/answers/date.html index f2ac2b1c0e..c1e1a80c92 100644 --- a/templates/partials/answers/date.html +++ b/templates/partials/answers/date.html @@ -1,64 +1,60 @@ {%- from "components/date-input/_macro.njk" import onsDateInput -%} - {%- set day_field = form.fields[answer['id']]['day'] -%} {%- set month_field = form.fields[answer['id']]['month'] -%} {%- set year_field = form.fields[answer['id']]['year'] -%} - +{# djlint:off #} {%- set config = { - "id": answer.id, - "legendOrLabel": answer.label, - "dontWrap": not answer.label, - "description": answer.description, - "attributes": { + "id": answer.id, + "legendOrLabel": answer.label, + "dontWrap": not answer.label, + "description": answer.description, + "attributes": { "data-qa": "widget-date" - }, - "mutuallyExclusive": mutuallyExclusive, - "error": error + }, + "mutuallyExclusive": mutuallyExclusive, + "error": error } -%} - {%- if day_field -%} - {%- set config = config | setAttributes({ - "day": { - "label": { - "text": _("Day"), - "attributes": { - "data-qa": "label-day" + {%- set config = config | setAttributes({ + "day": { + "label": { + "text": _("Day"), + "attributes": { + "data-qa": "label-day" + } + }, + "name": day_field.name, + "value": day_field._value() | e } - }, - "name": day_field.name, - "value": day_field._value() | e - } - }) -%} + }) -%} {%- endif -%} - {%- if month_field -%} - {%- set config = config | setAttributes({ - "month": { - "label": { - "text": _("Month"), - "attributes": { - "data-qa": "label-month" + {%- set config = config | setAttributes({ + "month": { + "label": { + "text": _("Month"), + "attributes": { + "data-qa": "label-month" + } + }, + "name": month_field.name, + "value": month_field._value() | e } - }, - "name": month_field.name, - "value": month_field._value() | e - } - }) -%} + }) -%} {%- endif -%} - {%- if year_field -%} - {%- set config = config | setAttributes({ - "year": { - "label": { - "text": _("Year"), - "attributes": { - "data-qa": "label-year" + {%- set config = config | setAttributes({ + "year": { + "label": { + "text": _("Year"), + "attributes": { + "data-qa": "label-year" + } + }, + "name": year_field.name, + "value": year_field._value() | e } - }, - "name": year_field.name, - "value": year_field._value() | e - } - }) -%} + }) -%} {%- endif -%} - {{ onsDateInput(config) }} +{# djlint:on #} diff --git a/templates/partials/answers/dropdown.html b/templates/partials/answers/dropdown.html index 3a41e089fa..de7a4c6dec 100644 --- a/templates/partials/answers/dropdown.html +++ b/templates/partials/answers/dropdown.html @@ -1,15 +1,18 @@ {% from "components/select/_macro.njk" import onsSelect %} {% set select = form.fields[answer['id']] %} - -{{ onsSelect({ - "id": answer.id, - "name": select.name, - "label": { - "id": answer.id ~ "-label", - "text": answer.label, - "description": answer.description - }, - "options": map_dropdown_config(select), - "error": error -}) }} +{# djlint:off #} +{{ + onsSelect({ + "id": answer.id, + "name": select.name, + "label": { + "id": answer.id ~ "-label", + "text": answer.label, + "description": answer.description + }, + "options": map_dropdown_config(select), + "error": error + }) +}} +{# djlint:on #} diff --git a/templates/partials/answers/duration.html b/templates/partials/answers/duration.html index b02ee699a5..289038c629 100644 --- a/templates/partials/answers/duration.html +++ b/templates/partials/answers/duration.html @@ -1,32 +1,37 @@ {% from "components/duration/_macro.njk" import onsDuration %} {% set config = { - "id": answer.id, - "dontWrap": not answer.label, - "mutuallyExclusive": mutuallyExclusive, - "error": error, - "legendOrLabel": answer.label + "id": answer.id, + "dontWrap": not answer.label, + "mutuallyExclusive": mutuallyExclusive, + "error": error, + "legendOrLabel": answer.label } %} - {% set years = form.fields[answer.id].years %} {% set months = form.fields[answer.id].months %} - +{# djlint:off #} {% if years %} - {% set config = config | setAttribute("field1", { - "id": years.id, - "name": years.name, - "value": years.data if years.data is not none else '', - "suffix": _(years.label.text) - }) %} + {% set config = config | setAttribute("field1", { + "id": years.id, + "name": years.name, + "value": years.data if years.data is not none else '', + "suffix": { + "id": years.id ~ "-type", + "text": _(years.label.text) + } + }) %} {% endif %} - {% if months %} - {% set config = config | setAttribute("field2", { - "id": months.id, - "name": months.name, - "value": months.data if months.data is not none else '', - "suffix": _(months.label.text) - }) %} -{% endif %} + {% set config = config | setAttribute("field2", { + "id": months.id, + "name": months.name, + "value": months.data if months.data is not none else '', + "suffix": { + "id": months.id ~ "-type", + "text": _(months.label.text) + } + }) %} +{% endif %} {{ onsDuration(config) }} +{# djlint:on #} diff --git a/templates/partials/answers/mobilenumber.html b/templates/partials/answers/mobilenumber.html index 1458a568d0..d2c06ceaf4 100644 --- a/templates/partials/answers/mobilenumber.html +++ b/templates/partials/answers/mobilenumber.html @@ -1,23 +1,23 @@ {% from "components/input/_macro.njk" import onsInput %} {% set input = form.fields[answer.id] if input is undefined else input %} - {% set config = { - "id": answer.id, - "name": input.name, - "type": "tel", - "autocomplete": "tel", - "width": "8", - "value": input._value() | e, - "label": { - "id": answer.id ~ "-label", - "text": answer.label, - "description": answer.description - }, - "attributes": { - "data-qa": "input-text" - }, - "error": error + "id": answer.id, + "name": input.name, + "type": "tel", + "autocomplete": "tel", + "width": "15", + "value": input._value() | e, + "label": { + "id": answer.id ~ "-label", + "text": answer.label, + "description": answer.description + }, + "attributes": { + "data-qa": "input-text" + }, + "error": error } %} - +{# djlint:off #} {{ onsInput(config) }} +{# djlint:on #} diff --git a/templates/partials/answers/monthyeardate.html b/templates/partials/answers/monthyeardate.html index 8cfd76628e..43599a6b96 100644 --- a/templates/partials/answers/monthyeardate.html +++ b/templates/partials/answers/monthyeardate.html @@ -1 +1 @@ -{% include 'partials/answers/date.html' %} +{% include "partials/answers/date.html" %} diff --git a/templates/partials/answers/number.html b/templates/partials/answers/number.html index 899d1533bb..8782cbfc3d 100644 --- a/templates/partials/answers/number.html +++ b/templates/partials/answers/number.html @@ -1,22 +1,25 @@ {% from "components/input/_macro.njk" import onsInput %} {% set input = form.fields[answer['id']] %} - -{{ onsInput({ - "id": answer.id, - "type": "number", - "label": { - "id": answer.id ~ "-label", - "text": answer.label, - "description": answer.description - }, - "value": input._value() | e, - "name": answer.id, - "attributes": { - "data-qa": "input-text" - }, - "dontWrap": true if mutuallyExclusive else false, - "mutuallyExclusive": mutuallyExclusive, - "width": get_width_for_number(answer), - "error": error -}) }} +{# djlint:off #} +{{ + onsInput({ + "id": answer.id, + "type": "number", + "label": { + "id": answer.id ~ "-label", + "text": answer.label, + "description": answer.description + }, + "value": input._value() | e, + "name": answer.id, + "attributes": { + "data-qa": "input-text" + }, + "dontWrap": true if mutuallyExclusive else false, + "mutuallyExclusive": mutuallyExclusive, + "width": get_width_for_number(answer), + "error": error + }) +}} +{# djlint:on #} diff --git a/templates/partials/answers/percentage.html b/templates/partials/answers/percentage.html index 03b2021efd..942d9f0bfa 100644 --- a/templates/partials/answers/percentage.html +++ b/templates/partials/answers/percentage.html @@ -1,26 +1,31 @@ {% from "components/input/_macro.njk" import onsInput %} {% set input = form.fields[answer['id']] %} - -{{ onsInput({ - "id": answer.id, - "classes": "js-totaliser-input-calculated" if answer.calculated, - "type": "number", - "label": { - "id": answer.id ~ "-label", - "text": answer.label, - "description": answer.description - }, - "value": input._value() | e, - "name": input.name, - "attributes": { - "data-qa": "input-text" - }, - "suffix": { - "title": "%" - }, - "dontWrap": true if mutuallyExclusive else false, - "mutuallyExclusive": mutuallyExclusive, - "width": get_width_for_number(answer), - "error": error -}) }} +{# djlint:off #} +{{ + onsInput({ + "id": answer.id, + "classes": "js-totaliser-input-calculated" if answer.calculated, + "type": "number", + "label": { + "id": answer.id ~ "-label", + "text": answer.label, + "description": answer.description + }, + "value": input._value() | e, + "name": input.name, + "attributes": { + "data-qa": "input-text" + }, + "suffix": { + "id": answer.id ~ "-type", + "title": "Percent", + "text": "%" + }, + "dontWrap": true if mutuallyExclusive else false, + "mutuallyExclusive": mutuallyExclusive, + "width": get_width_for_number(answer), + "error": error + }) +}} +{# djlint:on #} diff --git a/templates/partials/answers/radio.html b/templates/partials/answers/radio.html index ef7cd5f995..a3e8a44bca 100644 --- a/templates/partials/answers/radio.html +++ b/templates/partials/answers/radio.html @@ -1,22 +1,21 @@ {% from "components/radios/_macro.njk" import onsRadios %} - {% set config = { - "id": answer.id, - "name": answer.id, - "legend": answer.label, - "dontWrap": not answer.label, - "radios": map_select_config(form, answer), - "error": error + "id": answer.id, + "name": answer.id, + "legend": answer.label, + "dontWrap": not answer.label, + "radios": map_select_config(form, answer), + "error": error } %} - +{# djlint:off #} {% if answer.voluntary %} - {% set config = config | setAttribute("clearRadios", { - "text": _("Clear selection"), - "name": "action[clear_radios]", - "ariaClearText": "You can clear your answer using the clear selection button after the radio inputs", - "ariaClearedText": "You have cleared your answer" - }) %} + {% set config = config | setAttribute("clearRadios", { + "text": _("Clear selection"), + "name": "action[clear_radios]", + "ariaClearText": "You can clear your answer using the clear selection button after the radio inputs", + "ariaClearedText": "You have cleared your answer" + }) %} {% endif %} - {{ onsRadios(config) }} +{# djlint:off #} diff --git a/templates/partials/answers/relationship.html b/templates/partials/answers/relationship.html index 268f9c92ea..1aebf82d5b 100644 --- a/templates/partials/answers/relationship.html +++ b/templates/partials/answers/relationship.html @@ -1,10 +1,14 @@ {% from "components/relationships/_macro.njk" import onsRelationships %} -{{ onsRelationships({ - "id": answer.id, - "name": answer.id, - "dontWrap": true, - "playback": answer["playback"], - "radios": map_relationships_config(form, answer), - "error": error -}) }} +{# djlint:off #} +{{ + onsRelationships({ + "id": answer.id, + "name": answer.id, + "dontWrap": true, + "playback": answer["playback"], + "radios": map_relationships_config(form, answer) , + "error": error + }) +}} +{# djlint:oN #} diff --git a/templates/partials/answers/textarea.html b/templates/partials/answers/textarea.html index 0bec0a1864..a101f2f474 100644 --- a/templates/partials/answers/textarea.html +++ b/templates/partials/answers/textarea.html @@ -1,27 +1,29 @@ {% from "components/textarea/_macro.njk" import onsTextarea %} {% set input = form.fields[answer['id']] %} - +{# djlint:off #} {% if answer.label %} - {% set label = { - "id": answer.id ~ "-label", - "text": answer.label, - "description": answer.description - } %} + {% set label = { + "id": answer.id ~ "-label", + "text": answer.label, + "description": answer.description + } %} {% endif %} - -{{ onsTextarea({ - "id": answer.id, - "name": input.name, - "label": label, - "value": input._value() | e, - "charCheckLimit": { - "limit": answer.max_length | default(input.maxlength, true), - "charCountSingular": _("You have {x} character remaining"), - "charCountPlural": _("You have {x} characters remaining") - }, - "rows": answer.rows, - "dontWrap": true if mutuallyExclusive else false, - "mutuallyExclusive": mutuallyExclusive, - "error": error -}) }} +{{ + onsTextarea({ + "id": answer.id, + "name": input.name, + "label": label, + "value": input._value() | e, + "charCheckLimit": { + "limit": answer.max_length | default(input.maxlength, true), + "charCountSingular": _("You have {x} character remaining"), + "charCountPlural": _("You have {x} characters remaining") + }, + "rows": answer.rows, + "dontWrap": true if mutuallyExclusive else false, + "mutuallyExclusive": mutuallyExclusive, + "error": error + }) +}} +{# djlint:on #} diff --git a/templates/partials/answers/textfield.html b/templates/partials/answers/textfield.html index 5d44ce1fda..d4ca8163ea 100644 --- a/templates/partials/answers/textfield.html +++ b/templates/partials/answers/textfield.html @@ -2,51 +2,49 @@ {% from "components/input/_macro.njk" import onsInput %} {% set input = form.fields[answer.id] if input is undefined else input %} - {% set config = { - "id": answer.id, - "name": input.name, - "value": input._value() | e, - "label": { - "id": answer.id ~ "-label", - "text": answer.label, - "description": answer.description - }, - "attributes": { - "data-qa": "input-text" - }, - "dontWrap": true if mutuallyExclusive else false, - "mutuallyExclusive": mutuallyExclusive, - "error": error + "id": answer.id, + "name": input.name, + "value": input._value() | e, + "label": { + "id": answer.id ~ "-label", + "text": answer.label, + "description": answer.description + }, + "attributes": { + "data-qa": "input-text" + }, + "dontWrap": true if mutuallyExclusive else false, + "mutuallyExclusive": mutuallyExclusive, + "error": error } %} - {% if answer.suggestions %} - {% set config = config | setAttributes({ - "instructions": _("Use up and down keys to navigate suggestions once you\'ve typed more than two characters. Use the enter key to select a suggestion. Touch device users, explore by touch or with swipe gestures."), - "moreResults": _("Continue entering to improve suggestions"), - "resultsTitle": _("Suggestions"), - "noResults": _("No results found"), - "typeMore": _("Continue entering to get suggestions"), - "ariaYouHaveSelected": "You have selected", - "ariaMinChars": "Enter 3 or more characters for suggestions.", - "ariaOneResult": "There is one suggestion available.", - "ariaNResults": "There are {n} suggestions available.", - "ariaLimitedResults": "Results have been limited to 10 suggestions. Type more characters to improve your search.", - "autosuggestData": answer.suggestions.url, - "allowMultiple": true if answer.suggestions.allow_multiple else false - }) %} - {{ onsAutosuggest(config) }} -{% else %} - {% if answer.max_length %} - {% set config = config | setAttribute("charCheckLimit", { - "limit": answer.max_length, - "charCountOverLimitSingular": _("{x} character too many"), - "charCountOverLimitPlural": _("{x} characters too many"), - "charCountSingular": _("You have {x} character remaining"), - "charCountPlural": _("You have {x} characters remaining") + {# djlint:off #} + {% set config = config | setAttributes({ + "instructions": _("Use up and down keys to navigate suggestions once you\'ve typed more than two characters. Use the enter key to select a suggestion. Touch device users, explore by touch or with swipe gestures."), + "moreResults": _("Continue entering to improve suggestions"), + "resultsTitle": _("Suggestions"), + "noResults": _("No results found"), + "typeMore": _("Continue entering to get suggestions"), + "ariaYouHaveSelected": "You have selected", + "ariaMinChars": "Enter 3 or more characters for suggestions.", + "ariaOneResult": "There is one suggestion available.", + "ariaNResults": "There are {n} suggestions available.", + "ariaLimitedResults": "Results have been limited to 10 suggestions. Type more characters to improve your search.", + "autosuggestData": answer.suggestions.url, + "allowMultiple": true if answer.suggestions.allow_multiple else false }) %} - {% endif %} - {{ onsInput(config) }} + {{ onsAutosuggest(config) }} +{% else %} + {% if answer.max_length %} + {% set config = config | setAttribute("charCheckLimit", { + "limit": answer.max_length, + "charCountOverLimitSingular": _("{x} character too many"), + "charCountOverLimitPlural": _("{x} characters too many"), + "charCountSingular": _("You have {x} character remaining"), + "charCountPlural": _("You have {x} characters remaining") + }) %} + {% endif %} + {{ onsInput(config) }} + {# djlint:on #} {% endif %} - - diff --git a/templates/partials/answers/unit.html b/templates/partials/answers/unit.html index 9d65d346d3..2c520adbee 100644 --- a/templates/partials/answers/unit.html +++ b/templates/partials/answers/unit.html @@ -1,28 +1,31 @@ {% from "components/input/_macro.njk" import onsInput %} {% set input = form.fields[answer['id']] %} - -{{ onsInput({ - "id": answer.id, - "type": "number", - "classes": "js-totaliser-input-calculated" if answer.calculated, - "name": input.name, - "value": input._value() | e, - "label": { - "id": answer.id ~ "-label", - "text": answer.label, - "description": answer.description - }, - "suffix": { - "id": answer.id + "-type", - "title": format_unit_input_label(answer.unit, unit_length="long"), - "text": format_unit_input_label(answer.unit, unit_length=answer.unit_length) - }, - "attributes": { - "data-qa": "input-text" - }, - "dontWrap": true if mutuallyExclusive else false, - "mutuallyExclusive": mutuallyExclusive, - "width": get_width_for_number(answer), - "error": error -}) }} +{# djlint:off #} +{{ + onsInput({ + "id": answer.id, + "type": "number", + "classes": "js-totaliser-input-calculated" if answer.calculated, + "name": input.name, + "value": input._value() | e, + "label": { + "id": answer.id ~ "-label", + "text": answer.label, + "description": answer.description + }, + "suffix": { + "id": answer.id ~ "-type", + "title": format_unit_input_label(answer.unit, unit_length="long"), + "text": format_unit_input_label(answer.unit, unit_length=answer.unit_length) + }, + "attributes": { + "data-qa": "input-text" + }, + "dontWrap": true if mutuallyExclusive else false, + "mutuallyExclusive": mutuallyExclusive, + "width": get_width_for_number(answer), + "error": error + }) +}} +{# djlint:on #} diff --git a/templates/partials/answers/yeardate.html b/templates/partials/answers/yeardate.html index 8cfd76628e..43599a6b96 100644 --- a/templates/partials/answers/yeardate.html +++ b/templates/partials/answers/yeardate.html @@ -1 +1 @@ -{% include 'partials/answers/date.html' %} +{% include "partials/answers/date.html" %} diff --git a/templates/partials/block.html b/templates/partials/block.html index 71b854a4bb..714e2df4c4 100644 --- a/templates/partials/block.html +++ b/templates/partials/block.html @@ -1,9 +1,7 @@ {% set block = content.block %} - -
- - {% if 'question' in block %} - {% set question = block['question'] %} - {% include 'partials/question.html' %} - {% endif %} +
+ {% if 'question' in block %} + {% set question = block['question'] %} + {% include "partials/question.html" %} + {% endif %}
diff --git a/templates/partials/confirmation-email-form.html b/templates/partials/confirmation-email-form.html new file mode 100644 index 0000000000..c63b642844 --- /dev/null +++ b/templates/partials/confirmation-email-form.html @@ -0,0 +1,52 @@ +{% from "components/button/_macro.njk" import onsButton %} +{% from "components/input/_macro.njk" import onsInput %} + +{% set errors = form.errors['email'] %} +{% set email_field = form.email_field %} +{% if errors %} + {# djlint:off #} + {% set error = { + "text": errors[0], + "id": email_field.id ~ '-error', + "attributes": { + "data-ga": "error", + "data-ga-category": "Error", + "data-ga-action": "Display", + "data-ga-label": "Invalid / Missing Email", + "data-ga-page": "Confirmation Email", + } + } %} + {# djlint:on #} +{% endif %} +{% set config = { + "id": email_field.id, + "name": email_field.name, + "value": email_field._value() | e, + "label": { + "id": email_field.id ~ "-label", + "text": _("Email address"), + "description": _("This will not be stored and only used once to send your confirmation") + }, + "attributes": { + "data-qa": "input-text", + }, + "error": error +} %} +{# djlint:off #} +{{ onsInput(config) }} +{{ + onsButton({ + "text": _("Send confirmation"), + "variants": 'timer', + "classes": "ons-u-mt-s", + "attributes": { + "data-qa": "btn-submit", + "data-ga-category": "Button", + "data-ga-action": "Submit", + "data-ga-label": "Send", + "data-ga-page": "Confirmation Email", + "data-ga": "click", + } + }) +}} +{# djlint:on #} diff --git a/templates/partials/content-block.html b/templates/partials/content-block.html index 54934eaee1..5e43c6581c 100644 --- a/templates/partials/content-block.html +++ b/templates/partials/content-block.html @@ -1,7 +1,5 @@ -{% import 'macros/helpers.html' as helpers %} +{% import "macros/helpers.html" as helpers %} -{%- if content_block.title -%} - {{content_block.title}} -{% endif %} +{%- if content_block.title -%}{{ content_block.title }}{% endif %} {% set contents = content_block.contents %} -{% include 'partials/contents.html' %} +{% include "partials/contents.html" %} diff --git a/templates/partials/contents.html b/templates/partials/contents.html index 1efa447efd..e24699425a 100644 --- a/templates/partials/contents.html +++ b/templates/partials/contents.html @@ -1,39 +1,28 @@ {% set definition_count = namespace(value=1) %} {% set guidance_count = namespace(value=1) %} - {% for item in contents %} - {%- if item.definition -%} - {% set definition = item.definition %} - {% set definition_id = 'definition-' ~ definition_count.value %} - {% set category = 'Definition' %} - {%- include 'partials/definition.html' -%} - {% set definition_count.value = definition_count.value + 1 %} - {%- endif -%} - - {%- if item.guidance -%} - {% set guidance = item.guidance %} - {% set guidance_id = 'guidance-' ~ guidance_count.value %} - {%- include 'partials/guidance.html' -%} - {% set guidance_count.value = guidance_count.value + 1 %} - {%- endif -%} - - {%- if item.title -%} - {%- if block and block.type == 'Interstitial' -%} -

{{item.title}}

- {%- else -%} -

{{item.title}}

+ {%- if item.definition -%} + {% set definition = item.definition %} + {% set definition_id = "definition" %} + {% set category = "Definition" %} + {%- include 'partials/definition.html' -%} + {% set definition_count.value = definition_count.value + 1 %} {%- endif -%} - {% endif %} - - {%- if item.description -%} -

{{item.description}}

- {% endif %} - - {%- if item.list -%} -
    - {%- for list_item in item.list -%} -
  • {{list_item}}
  • - {% endfor %} -
- {% endif %} + {%- if item.guidance -%} + {% set guidance = item.guidance %} + {% set guidance_id = 'guidance-' ~ guidance_count.value %} + {%- include 'partials/guidance.html' -%} + {% set guidance_count.value = guidance_count.value + 1 %} + {%- endif -%} + {%- if item.title -%}

{{ item.title }}

{% endif %} + {%- if item.description -%}

{{ item.description }}

{% endif %} + {%- if item.list -%} +
    + {%- for list_item in item.list -%} + {%- if list_item|length > 0 -%} +
  • {{ list_item }}
  • + {%- endif -%} + {% endfor %} +
+ {% endif %} {% endfor %} diff --git a/templates/partials/definition.html b/templates/partials/definition.html index b2155f90bc..51e3957155 100644 --- a/templates/partials/definition.html +++ b/templates/partials/definition.html @@ -1,31 +1,25 @@ -{% from "components/collapsible/_macro.njk" import onsCollapsible %} +{% from "components/details/_macro.njk" import onsDetails %} -{% call onsCollapsible({ - "id": definition_id, - "title": definition.title, - "classes": "ons-u-mb-s", - "headingAttributes": { - "data-ga": "click", - "data-ga-category": category, - "data-ga-action": "Open panel", - "data-ga-label": definition.title, - "data-qa": definition_id ~ "-title" - }, - "contentAttributes": { - "data-qa": definition_id ~ "-content" - }, - "button": { - "close": _("Hide this"), - "contextSuffix": "content", - "attributes": { - "data-ga": "click", - "data-ga-category": category, - "data-ga-action": "Close panel", - "data-ga-label": definition.title, - "data-qa": definition_id ~ "-button" - } - } -}) %} - {% set contents = definition.contents %} - {% include 'partials/contents.html' %} +{# djlint:off #} +{% call + onsDetails({ + "id": definition_id, + "title": definition.title, + "classes": "ons-u-mb-s", + "headingAttributes": { + "data-ga": "click", + "data-ga-category": "Accordion", + "data-ga-action": "View", + "data-ga-label": "Definition", + "data-ga-page": "Questionnaire", + "data-qa": definition_id ~ "-title", + }, + "contentAttributes": { + "data-qa": definition_id ~ "-content" + } + }) + %} + {% set contents = definition.contents %} + {% include "partials/contents.html" %} {% endcall %} +{# djlint:on #} diff --git a/templates/partials/email-form.html b/templates/partials/email-form.html deleted file mode 100644 index 64f802a76e..0000000000 --- a/templates/partials/email-form.html +++ /dev/null @@ -1,46 +0,0 @@ -{% from "components/button/_macro.njk" import onsButton %} -{% from "components/input/_macro.njk" import onsInput %} -{% set errors = form.errors['email'] %} -{% set email_field = form.email_field %} - -{% if errors %} - {% set error = { - "text": errors[0], - "id": email_field.id ~ '-error', - "attributes": { - "data-ga": "error", - "data-ga-category": "Error", - "data-ga-action": "Confirmation Email", - "data-ga-label": email_field.id - } - }%} -{% endif %} - -{% set config = { - "id": email_field.id, - "name": email_field.name, - "value": email_field._value() | e, - "label": { - "id": email_field.id ~ "-label", - "text": _("Email address"), - "description": _("This will not be stored and only used once to send your confirmation") - }, - "attributes": { - "data-qa": "input-text", - }, - "error": error - } -%} - -{{ onsInput(config) }} - -{{ - onsButton({ - "text": _("Send confirmation"), - "submitType": 'timer', - "classes": "ons-u-mt-s", - "attributes": { - "data-qa": "btn-submit" - } - }) -}} diff --git a/templates/partials/error-panel.html b/templates/partials/error-panel.html index 834a6c18ec..7d4c4dad9e 100644 --- a/templates/partials/error-panel.html +++ b/templates/partials/error-panel.html @@ -1,19 +1,36 @@ {% from "components/panel/_macro.njk" import onsPanel %} +{% from "components/list/_macro.njk" import onsList %} + +{% set error_list = [] %} +{# djlint:off #} +{% for error_id, error in form.mapped_errors %} + {{ error_list.append({ + "text": error, + "url": "#" + error_id, + "variants": "inPageLink", + "attributes": { + "data-qa": "error-link-" + loop.index|string + } + }) or "" }} +{% endfor %} {% call - onsPanel({ - "type": "error", - "classes": "ons-u-mb-s", - "title": error_title , - "attributes": { - "data-qa": "error-body" - } - }) + onsPanel({ + "variant": "error", + "classes": "ons-u-mb-s", + "title": error_title, + "attributes": { + "data-qa": "error-body" + } + }) %} -
    - {% for error_id, error in form.mapped_errors %} -
  1. - {{ error }} -
  2. - {% endfor %} -
+ {{ + onsList({ + "element": "ol", + "attributes": { + "data-qa": "error-list" + }, + "itemsList": error_list + }) + }} {% endcall %} +{# djlint:on #} diff --git a/templates/partials/feedback-call-to-action.html b/templates/partials/feedback-call-to-action.html index 8595648c41..4186065279 100644 --- a/templates/partials/feedback-call-to-action.html +++ b/templates/partials/feedback-call-to-action.html @@ -1,5 +1,6 @@ -{% from 'components/feedback/_macro.njk' import onsFeedback %} +{% from "components/feedback/_macro.njk" import onsFeedback %} +{# djlint:off #} {{ onsFeedback({ "id": "feedback", @@ -9,3 +10,4 @@ "linkText": _("Give feedback") }) }} +{# djlint:on #} diff --git a/templates/partials/guidance.html b/templates/partials/guidance.html index 28b5dce63d..e04f4682eb 100644 --- a/templates/partials/guidance.html +++ b/templates/partials/guidance.html @@ -1,13 +1,16 @@ {% from "components/panel/_macro.njk" import onsPanel %} -{% call onsPanel({ - "id": guidance_id, - "classes": "ons-u-mb-m", - "attributes": { - "data-qa": guidance_id - } - }) %} - +{# djlint:off #} +{% call + onsPanel({ + "id": guidance_id, + "classes": "ons-u-mb-m", + "attributes": { + "data-qa": guidance_id + } + }) +%} {% set contents = guidance.contents %} - {% include 'partials/contents.html' %} + {% include "partials/contents.html" %} {% endcall %} +{# djlint:on #} diff --git a/templates/partials/individual-response-guidance.html b/templates/partials/individual-response-guidance.html index d50a37de49..7d4c2051c8 100644 --- a/templates/partials/individual-response-guidance.html +++ b/templates/partials/individual-response-guidance.html @@ -1,27 +1,26 @@ -{% from "components/collapsible/_macro.njk" import onsCollapsible %} +{% from "components/details/_macro.njk" import onsDetails %} -{% call onsCollapsible({ - "classes": "ons-u-mt-s", - "title": title, - "headingAttributes": { - "data-ga": "click", - "data-ga-category": "definition", - "data-ga-action": "Open panel", - "data-ga-label": title - }, - "button": { - "close": _("Hide this"), - "contextSuffix": "content", - "attributes": { - "data-ga": "click", - "data-ga-category": "definition", - "data-ga-action": "Close panel", - "data-ga-label": title - } - } -}) %} -
-

{{ _("You can share your household access code with the people you live with so they can complete their own sections.") }}

-

{{ _("If this is not possible, there are other ways each person can complete their own census.").format(url=content.individual_response_url)}}

-
+{# djlint:off #} +{% call + onsDetails({ + "classes": "ons-u-mt-s", + "title": title, + "headingAttributes": { + "data-ga": "click", + "data-ga-category": "Accordion", + "data-ga-action": "View", + "data-ga-label": "Open Definition", + "data-ga-page": "Individual Response", + } + }) +%} +
+

+ {{ _("You can share your household access code with the people you live with so they can complete their own sections.") }} +

+

+ {{ _("If this is not possible, there are other ways each person can complete their own census.").format(url=content.individual_response_url) }} +

+
{% endcall %} +{# djlint:on #} diff --git a/templates/partials/introduction/basic.html b/templates/partials/introduction/basic.html index 1aa59a9cd4..6c0d99afa4 100644 --- a/templates/partials/introduction/basic.html +++ b/templates/partials/introduction/basic.html @@ -1,7 +1,5 @@ -
- {% if content_block.title %} -

{{ content_block.title }}

- {% endif %} - {% set contents = content_block.contents %} - {% include 'partials/contents.html' %} +
+ {% if content_block.title %}<{{ title_tag }}>{{ content_block.title }}{% endif %} + {% set contents = content_block.contents %} + {% include "partials/contents.html" %}
diff --git a/templates/partials/introduction/preview.html b/templates/partials/introduction/preview.html index e60cc6b035..5b5050ba8d 100644 --- a/templates/partials/introduction/preview.html +++ b/templates/partials/introduction/preview.html @@ -1,60 +1,48 @@

{{ intro.title }}

-
- {% set contents = intro.contents %} - {% include "partials/contents.html" %} + {% set contents = intro.contents %} + {% include "partials/contents.html" %}
- {% if intro.questions %} - {% from "components/accordion/_macro.njk" import onsAccordion %} - - {% set accordionItems = [] %} - - {% for question in intro.questions %} - {% set content %} - {% set contents = question.contents %} - {% include "partials/contents.html" %} - {% endset %} + {% from "components/accordion/_macro.njk" import onsAccordion %} - {% set item = { - "title": question.question, - "content": content, - "summaryAttributes": { - "data-ga": "click", - "data-ga-category": "Preview Survey", - "data-ga-action": "Open panel", - "data-ga-label": question.question - }, - "button": { - "open": _('Show'), - "close": _('Hide'), - "attributes": { - "data-ga": "click", - "data-ga-category": "Preview Survey", - "data-ga-action": "Open panel", - "data-ga-label": question.question - } - } - } %} - - {% do accordionItems.append(item) %} - {% endfor %} - - {{ - onsAccordion({ - "id": "intro-questions", - "classes": "ons-u-mb-s", - "allButton": { - "open": _('Show all'), + {% set accordionItems = [] %} + {# djlint:off #} + {% for question in intro.questions %} + {% set content %} + {% set contents = question.contents %} + {% include "partials/contents.html" %} + {% endset %} + {% set item = { + "title": question.question, + "titleTag": "h3", + "content": content, + "summaryAttributes": { + "data-ga": "click", + "data-ga-category": "Accordion", + "data-ga-action": "Preview", + "data-ga-label": "Preview Question", + "data-ga-page": "Introduction", + } + } %} + {% do accordionItems.append(item) %} + {% endfor %} + {{ + onsAccordion({ + "id": "intro-questions", + "allButton": { + "open": _('Show all') , "close": _('Hide all'), "attributes": { - "data-ga": "click", - "data-ga-category": "Preview Survey", - "data-ga-action": "Show all", - "data-ga-label": "Show all" + "data-ga": "click", + "data-ga-category": "Button", + "data-ga-action": "View", + "data-ga-label": "Show all", + "data-ga-page": "Introduction", } - }, - "itemsList": accordionItems - }) - }} + }, + "itemsList": accordionItems + }) + }} + {# djlint:on #} {% endif %} diff --git a/templates/partials/introduction/start-survey.html b/templates/partials/introduction/start-survey.html index 5ae95473e9..e90b4c6ed3 100644 --- a/templates/partials/introduction/start-survey.html +++ b/templates/partials/introduction/start-survey.html @@ -1,10 +1,20 @@ {% from "components/button/_macro.njk" import onsButton %} +{# djlint:off #} {{ - onsButton({ - "text": _("Start survey"), - "submitType": 'timer', - "classes": "qa-btn-get-started", - "name": "action[start_questionnaire]" - }) + onsButton({ + "text": _("Start survey"), + "variants": 'timer', + "classes": "qa-btn-get-started", + "name": "action[start_questionnaire]", + "attributes": { + "data-qa": "btn-submit", + "data-ga-category": "Button", + "data-ga-action": "Start", + "data-ga-label": "Start Survey", + "data-ga-page": "Introduction", + "data-ga": "click", + } + }) }} +{# djlint:on #} diff --git a/templates/partials/last_viewed_question_guidance.html b/templates/partials/last_viewed_question_guidance.html index c581c45991..3175d38560 100644 --- a/templates/partials/last_viewed_question_guidance.html +++ b/templates/partials/last_viewed_question_guidance.html @@ -1,11 +1,14 @@ {% from "components/panel/_macro.njk" import onsPanel %} -{% call onsPanel({ - "id": "last-viewed-question-guidance", - "classes": "ons-u-mb-m" - }) %} - - {{ _("This is the last viewed question in this section") }} - -
-{{ _("You can also go back to the start of the section").format(url = last_viewed_question_guidance.first_location_in_section_url)}} + +{# djlint:off #} +{% call + onsPanel({ + "id": "last-viewed-question-guidance", + "classes": "ons-u-mb-m" + }) +%} + {{ _("This is the last viewed question in this section") }} +
+ {{ _("You can also go back to the start of the section").format(url = last_viewed_question_guidance.first_location_in_section_url) }} {% endcall %} +{# djlint:on #} diff --git a/templates/partials/preview-question.html b/templates/partials/preview-question.html new file mode 100644 index 0000000000..5980e3dc34 --- /dev/null +++ b/templates/partials/preview-question.html @@ -0,0 +1,65 @@ +{% from "components/panel/_macro.njk" import onsPanel %} +{% from "macros/helpers.html" import format_paragraphs %} + +{% set answers = question.answers %} +
+

{{ question.title }}

+ {% set answers_length = answers | length %} + {% if question.descriptions %} + {% set descriptions = question.descriptions %} + {% set descriptions_length = descriptions | length %} + {% for description in descriptions %} +
{{- description | safe -}}
+ {% endfor %} + {% endif %} + {%- if question.guidance -%} + {% set contents = question.guidance.contents %} + {# djlint:off #} + {% call + onsPanel({ + "id": "question-guidance-" ~ question.id, + "classes": "ons-u-mb-m" + }) + %} + {% include "partials/contents.html" %} + {% endcall %} + {# djlint:on #} + {% endif %} + {% for answer in answers %} + {% if loop.last and question.type == "MutuallyExclusive" %} +

+ {{ _("Or") }} +

+ {% endif %} + {% if not loop.last or (answer.options and answers_length == 1) %}

{{ answer.options_text }}

{% endif %} + {% if answer.options %} + {% if answer.label %} +

+ {{- answer.label | safe -}} +

+ {% endif %} +
    + {% for option in answer.options %}
  • {{ option }}
  • {% endfor %} +
+ {% else %} + {% if answer.label %} +

+ {{- answer.label | safe -}} +

+ {% endif %} + {% if answer.max_length %} +

{{ _("{max_characters} characters can be added.").format(max_characters = answer.max_length) }}

+ {% endif %} + {% endif %} + {# answer guidance not implemented yet due to some work that needs to be done in the DS will be implemented in iteration 2 #} + {# {% if answer.guidance %} + {% with answer_guidance = { + 'id': answer.id, + 'label': answer.label, + 'schema_item': answer.guidance + } %} + {% include "partials/answer-guidance.html" %} + {% endwith %} +{% endif %} #} + {% endfor %} +
diff --git a/templates/partials/question-definition.html b/templates/partials/question-definition.html deleted file mode 100644 index 8435680445..0000000000 --- a/templates/partials/question-definition.html +++ /dev/null @@ -1,7 +0,0 @@ -{% from "components/collapsible/_macro.njk" import onsCollapsible %} - -{% for definition in question.definitions %} - {% set definition_id = "question-definition-" ~ loop.index %} - {% set category = 'Question definition' %} - {%- include 'partials/definition.html' -%} -{% endfor %} diff --git a/templates/partials/question.html b/templates/partials/question.html index 51b1f033ef..2aca679da8 100644 --- a/templates/partials/question.html +++ b/templates/partials/question.html @@ -1,123 +1,141 @@ {% from "components/question/_macro.njk" import onsQuestion %} -{% from "components/fieldset/_macro.njk" import onsFieldset %} {% from "components/panel/_macro.njk" import onsPanel %} {% from "components/error/_macro.njk" import onsError %} - -{% from 'macros/helpers.html' import format_paragraphs %} -{% from 'macros/helpers.html' import interviewer_note %} +{% from "macros/helpers.html" import format_paragraphs %} +{% from "macros/helpers.html" import interviewer_note %} {% set form = content.form %} - -{% set title= interviewer_note(question.title) if block.interviewer_only else question.title %} -{% set question_title= question.title %} +{% set display = namespace(guidance=False) %} +{% set title = interviewer_note(question.title) if block.interviewer_only else question.title %} +{% set question_title = question.title %} {% set question_description = format_paragraphs(question.description) %} {% set question_instruction = format_paragraphs(question.instruction) %} {% set question_error = form.question_errors[question.id] %} - -{%- set question_definition -%} - {%- if question.definitions -%} - {%- include 'partials/question-definition.html' -%} - {%- endif -%} -{%- endset -%} - +{%- if question.definition -%} + {% set definition_id = "question-definition" %} + {% set definition_content %} + {% set contents = question.definition.contents %} + {% include "partials/contents.html" %} + {% endset %} + {% set question_definition = { + "title": question.definition.title, + "id": definition_id, + "content": definition_content, + "headingAttributes": { + "data-ga": "click", + "data-ga-category": "Accordion", + "data-ga-action": "View", + "data-ga-label": "Question Definition", + "data-ga-page": "Questionnaire", + "data-qa": definition_id ~ "-title", + }, + "contentAttributes": { + "data-qa": definition_id ~ "-content" + } + } %} +{% elif question.definitions %} + {%- set question_definitions -%} + {% for definition in question.definitions %} + {% set definition_id = "question-definition" %} + {% set category = "Question definition" %} + {%- include 'partials/definition.html' -%} + {% endfor %} + {%- endset -%} +{%- endif -%} {% set individual_response_guidance %} - {%- if show_individual_response_guidance == True -%} - {% set title = _("If you can’t answer questions for this person") %} - {% include 'partials/individual-response-guidance.html' %} - {%- endif -%} -{% endset %} - -{% set question_warning %} - {%- if question.warning -%} - {% call onsPanel({ - "id": "question-warning-" ~ question.id, - "type": "warn" - }) %} -

{{question.warning}}

- {% endcall %} - {% endif %} + {%- if show_individual_response_guidance == True -%} + {% set title = _("If you can’t answer questions for this person") %} + {% include "partials/individual-response-guidance.html" %} + {%- endif -%} {% endset %} - +{%- if question.warning -%} + {% set question_warning = {"body": question.warning} %} +{% endif %} {% set question_guidance %} - {%- if question.guidance -%} - {% set contents = question.guidance.contents %} - {% call onsPanel({ - "id": "question-guidance-" ~ question.id, - "classes": "ons-u-mb-m" - }) %} - {% set contents = question.guidance.contents %} - {% include 'partials/contents.html' %} - {% endcall %} - {% endif %} + {%- if question.guidance -%} + {% set contents = question.guidance.contents %} + {% for item in contents %} + {%- if (item['title'], item['description'], item['list'], item['definition'], item['guidance'])|select|first -%} + {% set display.guidance = True %} + {%- endif -%} + {% endfor %} + {%- if display.guidance -%} + {# djlint:off #} + {% call onsPanel({ + "id": "question-guidance-" ~ question.id, + "classes": "ons-u-mb-m" + }) %} + {% include "partials/contents.html" %} + {% endcall %} + {# djlint:on #} + {%- endif -%} + {% endif %} {% endset %} - {%- set mutually_exclusive_question = question.type == 'MutuallyExclusive' -%} - {% set question_answers %} - {% if mutually_exclusive_question %} - {%- set answer = question.answers[0] -%} - - {%- set deselectionMessage = _("Selecting this will clear your answer") -%} - {%- set deselectGroupAdjective = _("cleared") -%} - - {%- if answer.type == 'checkbox' -%} - {%- set deselectionMessage = _("Selecting this will deselect any selected options") -%} - {%- set deselectGroupAdjective = _("deselected") -%} + {% if mutually_exclusive_question %} + {%- set answer = question.answers[0] -%} + {%- set deselectionMessage = _("Selecting this will clear your answer") -%} + {%- set deselectGroupAdjective = _("cleared") -%} + {%- if answer.type == 'checkbox' -%} + {%- set deselectionMessage = _("Selecting this will deselect any selected options") -%} + {%- set deselectGroupAdjective = _("deselected") -%} + {%- endif -%} + {# djlint:off #} + {%- set mutuallyExclusive = { + "or": _("Or"), + "exclusiveOptions": map_select_config(form, question.answers[-1]), + "deselectionMessage": deselectionMessage, + "deselectGroupAdjective": deselectGroupAdjective, + "deselectExclusiveOptionAdjective": _("deselected") + } -%} + {# djlint:on #} + {% include "partials/answer.html" %} + {%- else -%} + {%- set answers -%} + {%- for answer in question.answers -%} + {% include "partials/answer.html" %} + {%- endfor -%} + {%- endset -%} + {{ answers }} {%- endif -%} - - {%- set mutuallyExclusive = { - "or": _("Or"), - "checkbox": map_select_config(form, question.answers[-1])[0], - "deselectionMessage": deselectionMessage, - "deselectGroupAdjective": deselectGroupAdjective, - "deselectCheckboxAdjective": _("deselected") - } -%} - - {% include 'partials/answer.html' %} - {%- else -%} - {%- set answers -%} - {%- for answer in question.answers -%} - {% include 'partials/answer.html' %} - {%- endfor -%} - {%- endset -%} - {{ answers }} - {%- endif -%} {% endset %} - {% call onsQuestion({ - "id": question.id, - "title": title, - "description": question_description, - "instruction": question_instruction, - "legendIsQuestionTitle": should_wrap_with_fieldset(question) -}) %} - {%- if content.list and content.list.list_items -%} - {% set list = content.list %} -
- {% include 'partials/summary/list-summary.html' %} -
- {% endif %} - {{ question_warning }} - {{ individual_response_guidance }} - {{ question_definition }} - {{ question_guidance }} - - {% if question_error %} - {% set config = { - "text": question_error, - "id": question.id ~ '-error', - "attributes": { - "data-ga": "question-error", - "data-ga-category": "Question error", - "data-ga-action": question.type, - "data-ga-label": question.id - } - }%} - {% call onsError(config) %} - {{ question_answers }} - {% endcall %} - {% else %} - {{ question_answers }} - {% endif %} - + "id": question.id, + "title": title, + "description": question_description, + "instruction": question_instruction, + "warning": question_warning, + "definition": question_definition, + "legendIsQuestionTitle": should_wrap_with_fieldset(question) + }) %} + {%- if content.list and content.list.list_items -%} + {% set list = content.list %} +
{% include "partials/summary/list-summary.html" %}
+ {% endif %} + {{ individual_response_guidance }} + {{ question_definitions }} + {{ question_guidance }} + {# djlint:off #} + {% if question_error %} + {% set config = { + "text": question_error, + "id": question.id ~ '-error', + "attributes": { + "data-ga": "error", + "data-ga-category": "Error", + "data-ga-action": "Display", + "data-ga-label": "Question Error", + "data-ga-type": question.type, + "data-ga-identifier": question.id, + "data-ga-page": "Questionnaire", + } + } %} + {% call onsError(config) %} + {{ question_answers }} + {% endcall %} + {% else %} + {{ question_answers }} + {% endif %} + {# djlint:on #} {% endcall %} diff --git a/templates/partials/summary/collapsible-summary.html b/templates/partials/summary/collapsible-summary.html index 4b0ac9f72f..e443df3e5d 100644 --- a/templates/partials/summary/collapsible-summary.html +++ b/templates/partials/summary/collapsible-summary.html @@ -2,68 +2,63 @@ {% from "components/summary/_macro.njk" import onsSummary %} {%- set itemList = [] -%} - -{%- for group in content.summary.groups -%} - {%- if group["blocks"] | length -%} - {%- if group.title -%} - {%- set item = { - "title": group.title, - "id": group.id, - "content": "", - "button": { - "open": _('Show'), - "close": _('Hide'), - "attributes": { - "data-ga": "click", - "data-ga-category": "Preview Survey", - "data-ga-action": "Open panel", - "data-ga-label": group.title - } - } - } -%} - - {%- set summary %} - {{ onsSummary({ - "classes": "summary--no-bottom-border", - "summaries": [ - { - "groups": [ - { - "headers":["Question", "Answer given", "Change answer"], - "rows": map_summary_item_config( - group, - content.summary.summary_type, - content.summary.answers_are_editable, - _("No answer provided"), - _("Change"), - _("Change your answer for:"), - content.summary.calculated_question - ) - } - ] - } - ] - }) }} - {%- endset -%} - - {%- do item | setAttribute("content", item.content + summary) -%} - - {%- do itemList.append(item) -%} - {%- endif -%} - {%- endif -%} +{%- for section in content.summary.sections -%} + {%- for group in section.groups -%} + {%- if group["blocks"] | length -%} + {%- if group.title -%} + {# djlint:off #} + {%- set item = { + "title": group.title, + "id": group.id, + "content": "" + } -%} + {%- set summary %} + {{ + onsSummary({ + "classes": "summary--no-bottom-border", + "summaries": [ + { + "groups": [ + { + "rows": map_summary_item_config( + group=group, + summary_type=content.summary.summary_type, + answers_are_editable=content.summary.answers_are_editable, + no_answer_provided=_("No answer provided"), + edit_link_text=_("Change"), + edit_link_aria_label=_("Change details for {item_name}"), + calculated_question=content.summary.calculated_question + ) + } + ] + } + ] + }) + }} + {%- endset -%} + {# djlint:on #} + {%- do item | setAttribute("content", item.content + summary) -%} + {%- do itemList.append(item) -%} + {%- endif -%} + {%- endif -%} + {%- endfor -%} {%- endfor -%} - -{{ onsAccordion({ - "id": "summary-accordion", - "allButton": { - "open": _('Show all'), - "close": _('Hide all'), - "attributes": { - "data-ga": "click", - "data-ga-category": "Preview Survey", - "data-ga-action": "Show all", - "data-ga-label": "Show all" - } - }, - "itemsList": itemList -}) }} +{# djlint:off #} +{{ + onsAccordion({ + "id": "summary-accordion", + "allButton": { + "open": _('Show all') , + "close": _('Hide all'), + "attributes": { + "data-ga": "click", + "data-ga-category": "Button", + "data-ga-action": "View", + "data-ga-label": "Collapsible Answers", + "data-ga-page": "Summary", + } + }, + "itemsList": itemList + }) +}} +{# djlint:on #} diff --git a/templates/partials/summary/list-summary.html b/templates/partials/summary/list-summary.html index 246dd96d03..588f6cb923 100644 --- a/templates/partials/summary/list-summary.html +++ b/templates/partials/summary/list-summary.html @@ -1,49 +1,46 @@ {% from "components/summary/_macro.njk" import onsSummary %} {% if list.editable %} - {% set headers = ["Name of person", "Action"] %} - {% set rows = map_list_collector_config( - list.list_items, - "person", - _("Change"), - _("Change details for {item_name}"), - _("Remove"), - _("Remove {item_name}") - ) %} + {# djlint:off #} + {% set rows = map_list_collector_config( + list_items=list.list_items, + render_icon=True, + edit_link_text=_("Change"), + edit_link_aria_label=_("Change details for {item_name}"), + remove_link_text=_("Remove"), + remove_link_aria_label=_("Remove {item_name}") + ) %} + {# djlint:on #} {% else %} - {% set headers = ["Name of person"] %} - {% set rows = map_list_collector_config( - list.list_items, - "person" - ) %} + {% set rows = map_list_collector_config( + list_items=list.list_items, + render_icon=True + ) %} {% endif %} - {% set group_config = { - "groupTitle": list_title, - "headers": headers, - "rows": rows, - "placeholderText": empty_list_text, + "groupTitle": list_title, + "rows": rows, + "placeholderText": empty_list_text, } %} - {% if add_link %} - {% set group_config = group_config | setAttribute("summaryLink", { - "url": add_link, - "text": add_link_text, - "attributes": { - "data-qa": "add-item-link" - } - }) %} + {% set group_config = group_config | setAttribute("summaryLink", { + "url": add_link, + "text": add_link_text, + "attributes": { + "data-qa": "add-item-link" + } + }) %} {% endif %} - {% set config = { - "withinQuestion": true, - "summaries": [ - { - "groups": [ - group_config - ] - } - ] + "withinQuestion": true, + "summaries": [ + { + "groups": [ + group_config + ] + } + ] } %} - +{# djlint:off #} {{ onsSummary(config) }} +{# djlint:on #} diff --git a/templates/partials/summary/summary.html b/templates/partials/summary/summary.html index 4034a39946..c0fa459536 100644 --- a/templates/partials/summary/summary.html +++ b/templates/partials/summary/summary.html @@ -1,35 +1,55 @@ {%- if content.summary.collapsible -%} - {%- include 'partials/summary/collapsible-summary.html' -%} + {%- include 'partials/summary/collapsible-summary.html' -%} {%- else -%} - {% from "components/summary/_macro.njk" import onsSummary %} + {% from "components/summary/_macro.njk" import onsSummary %} - {% set summaryGroups = [] %} - {%- for group in content.summary.groups if group.blocks -%} - {% do summaryGroups.append ( - { - "groups": [ - { - "groupTitle": group.title if group.title else None, - "id": group.id if group.id else None, - "headers": ["Question", "Answer given", "Change answer"], - "rows": map_summary_item_config( - group, - content.summary.summary_type, - content.summary.answers_are_editable, - _("No answer provided"), - _("Change"), - _("Change your answer for:"), - content.summary.calculated_question - ), - "classes": "ons-u-mt-m" if loop.index > 1 else "" - } - ] - } - ) - %} - {%- endfor -%} - {{ onsSummary({ - "summaries": summaryGroups - }) }} + {% set summary_sections = [] %} + {% set summary_type = content.summary.summary_type %} + {% set view_submitted_response = content.summary.view_submitted_response %} + {# djlint:off #} + {%- for section in content.summary.sections -%} + {% set summary_groups = [] %} + {%- if section.groups -%} + {%- for group in section.groups -%} + {%- if group.blocks -%} + {% do summary_groups.append + ( + { + "groupTitle": group.title if group.title else None, + "id": group.id if group.id else None, + "rows": map_summary_item_config( + group=group, + summary_type=summary_type, + answers_are_editable=content.summary.answers_are_editable, + no_answer_provided=_("No answer provided"), + remove_link_text=_("Remove") if not view_submitted_response else "", + remove_link_aria_label=_("Remove details for {item_name}") if not view_submitted_response else "", + edit_link_text=_("Change") if not view_submitted_response else "", + edit_link_aria_label=_("Change details for {item_name}") if not view_submitted_response else "", + calculated_question=content.summary.calculated_question + ), + "classes": "ons-u-mt-m" if loop.index > 1 else "", + "placeholderText": group.placeholder_text, + "summaryLink": group.links.add_link, + } + ) + %} + {%- endif -%} + {%- endfor -%} + {%- endif -%} + {% do summary_sections.append + ( + { + "summaryTitle": section.title if summary_type == "Summary", + "groups": summary_groups + } + ) + %} + {%- endfor -%} + {{ + onsSummary({ + "summaries": summary_sections + }) + }} + {# djlint:on #} {%- endif -%} - diff --git a/templates/preview.html b/templates/preview.html new file mode 100644 index 0000000000..2dd4d03a09 --- /dev/null +++ b/templates/preview.html @@ -0,0 +1,121 @@ +{% extends "layouts/_base.html" %} + +{% from "components/panel/_macro.njk" import onsPanel %} +{% from "components/button/_macro.njk" import onsButton %} +{% from "components/accordion/_macro.njk" import onsAccordion %} + +{% set save_on_signout = true %} +{% set breadcrumbs = { + "ariaLabel": 'Back', + "itemsList": [ + { + "url": url_for("questionnaire.get_questionnaire"), + "id": "top-previous", + "text": _("Back"), + "attributes": { + "data-ga": "click", + "data-ga-category": "Link", + "data-ga-action": "Navigate", + "data-ga-label": "Previous", + "data-ga-page": "Preview", + } + } + ] +} %} +{% macro preview_blocks_for_sections(blocks) -%} + {% for block in blocks %} + {% if 'question' in block %} + {% set question = block['question'] %} + {% include "partials/preview-question.html" %} + {% endif %} + {% endfor %} +{%- endmacro %} + +{% block main %} +

{{ _("Preview of the questions in this survey") }}

+ {# djlint:off #} + {% call + onsPanel({ + "classes": 'ons-u-mb-m ons-u-ph' + }) + %} +

+ {{ _("To answer these questions you need to start survey").format(url=url_for('questionnaire.get_questionnaire')) }} +

+ {% endcall %} + {# djlint:on #} +

+ {{ _("You may not have to answer all of these questions. The questions you see will depend on the answers you provide.") }} +

+ {# djlint:off #} + {{ + onsButton({ + "type": 'button', + "text": _('Print questions'), + "variants": ['small', 'secondary', 'print'], + "attributes": { + "data-qa": "btn-print", + "data-ga-category": "Button", + "data-ga-action": "View", + "data-ga-label": "Print Dialogue", + "data-ga-page": "Preview", + "data-ga": "click", + } + }) + }} + {{ + onsButton({ + "text": _('Save questions as PDF') , + "variants": ['small', 'secondary', 'timer', 'download'], + "url": content.pdf_url, + "removeDownloadAttribute": true, + "attributes": { + "data-qa": "btn-pdf", + "data-ga-category": "Button", + "data-ga-action": "Download", + "data-ga-label": "Download PDF", + "data-ga-page": "Preview", + "data-ga": "click", + } + }) + }} +
+ {%- if content.preview.sections | length > 1 -%} + {%- set itemList = [] -%} + {%- for section in content.preview.sections if section.blocks -%} + {%- set item = { + "title": section.title, + "id": section.id + } -%} + {%- set block_previews = preview_blocks_for_sections(blocks=section["blocks"]) -%} + {%- do item | setAttribute("content", block_previews) -%} + {%- do itemList.append(item) -%} + {%- endfor -%} + {{ + onsAccordion({ + "id": "summary-accordion", + "allButton": { + "open": _('Show all') , + "close": _('Hide all'), + "attributes": { + "data-ga": "click", + "data-ga-category": "Button", + "data-ga-action": "View", + "data-ga-label": "Show all", + "data-ga-page": "Preview", + } + }, + "itemsList": itemList + }) + }} + {%- else %} + {%- for section in content.preview.sections if section.blocks -%} +
+

{{ section.title }}

+ {{ preview_blocks_for_sections(blocks=section["blocks"]) }} +
+ {%- endfor -%} + {%- endif -%} +
+ {# djlint:on #} +{% endblock main %} diff --git a/templates/primarypersonlistaddoreditquestion.html b/templates/primarypersonlistaddoreditquestion.html index 118134653c..2f61a13acd 100644 --- a/templates/primarypersonlistaddoreditquestion.html +++ b/templates/primarypersonlistaddoreditquestion.html @@ -1 +1 @@ -{% include 'question.html' %} +{% include "question.html" %} diff --git a/templates/primarypersonlistcollector.html b/templates/primarypersonlistcollector.html index 118134653c..2f61a13acd 100644 --- a/templates/primarypersonlistcollector.html +++ b/templates/primarypersonlistcollector.html @@ -1 +1 @@ -{% include 'question.html' %} +{% include "question.html" %} diff --git a/templates/question.html b/templates/question.html index aa21d8965d..9c7aa9b7f3 100644 --- a/templates/question.html +++ b/templates/question.html @@ -1,14 +1,13 @@ -{% extends 'layouts/_questionnaire.html' %} -{% import 'macros/helpers.html' as helpers %} +{% extends "layouts/_questionnaire.html" %} + +{% import "macros/helpers.html" as helpers %} {% set save_on_signout = true %} {% block form_errors %} - {% set form = content.form %} - - {% if form and (form.errors or form.question_errors) %} - {% set error_title = ngettext('There is a problem with your answer', 'There are %(num)s problems with your answer', form.mapped_errors | length) %} - {% include 'partials/error-panel.html' %} - {% endif %} - + {% set form = content.form %} + {% if form and (form.errors or form.question_errors) %} + {% set error_title = ngettext('There is a problem with your answer', 'There are %(num)s problems with your answer', form.mapped_errors | length) %} + {% include "partials/error-panel.html" %} + {% endif %} {% endblock form_errors %} diff --git a/templates/relationshipcollector.html b/templates/relationshipcollector.html index 9956ae57e6..2f61a13acd 100644 --- a/templates/relationshipcollector.html +++ b/templates/relationshipcollector.html @@ -1 +1 @@ -{% include 'question.html' %} \ No newline at end of file +{% include "question.html" %} diff --git a/templates/sectionsummary.html b/templates/sectionsummary.html index 01c4c845dd..2e4a0dec02 100644 --- a/templates/sectionsummary.html +++ b/templates/sectionsummary.html @@ -1,54 +1,66 @@ -{% extends 'layouts/_questionnaire.html' %} +{% extends "layouts/_questionnaire.html" %} + {% from "components/button/_macro.njk" import onsButton %} -{% import 'macros/helpers.html' as helpers %} +{% import "macros/helpers.html" as helpers %} {% set save_on_signout = true %} - +{# djlint:off #} {{ - onsButton({ - "text": _("Continue"), - "attributes": { - "data-qa": "btn-submit" - } - }) + onsButton({ + "text": _("Continue"), + "attributes": { + "data-qa": "btn-submit", + "data-ga-category": "Button", + "data-ga-action": "Submit", + "data-ga-label": "Continue", + "data-ga-page": "Section Summary", + "data-ga": "click", + } + }) }} -{% block form_content %} - {% if content.summary.groups %} - {% set group = content.summary.groups %} -

{{content.summary.title}}

-
- {% include 'partials/summary/summary.html' %} -
- {% elif content.summary.custom_summary %} -

{{content.summary.title}}

- {% for summary in content.summary.custom_summary %} - {% if summary.type == 'List' %} - {% set add_link = summary.add_link %} - {% set add_link_text = summary.add_link_text %} - {% set empty_list_text = summary.empty_list_text %} - {% set list_title = summary.title %} +{# djlint:on #} -
- {% if summary.list %} - {% set list = summary.list %} - {% include 'partials/summary/list-summary.html' %} - {% endif %} -
- {% endif %} - {% endfor %} - {% endif %} -{% endblock %} +{% block form_content %} + {% if content.summary.custom_summary %} +

{{ content.summary.title }}

+ {% for summary in content.summary.custom_summary %} + {% if summary.type == 'List' %} + {% set add_link = summary.add_link %} + {% set add_link_text = summary.add_link_text %} + {% set empty_list_text = summary.empty_list_text %} + {% set list_title = summary.title %} +
+ {% if summary.list %} + {% set list = summary.list %} + {% include "partials/summary/list-summary.html" %} + {% endif %} +
+ {% endif %} + {% endfor %} + {%- else -%} +

{{ content.summary.title }}

+
{% include "partials/summary/summary.html" %}
+ {% endif %} +{% endblock form_content %} {% block submit_button %} - {{ - onsButton({ - "text": continue_button_text | default(_("Continue")), - "submitType": 'timer', - "classes": "ons-u-mt-xl", - "attributes": { - "data-qa": "btn-submit" - } - }) - }} -{% endblock %} + {# djlint:off #} + {{ + onsButton({ + "text": continue_button_text | default(_("Continue")), + "variants": 'timer', + "classes": "ons-u-mt-xl", + "attributes": { + "data-qa": "btn-submit", + "data-ga-category": "Button", + "data-ga-action": "Submit", + "data-ga-label": "Continue", + "data-ga-page": "Section Summary", + "data-ga": "click", + } + }) + }} + {# djlint:on #} +{% endblock submit_button %} diff --git a/templates/signed-out.html b/templates/signed-out.html index 0597dabb1f..2b8542b5bb 100644 --- a/templates/signed-out.html +++ b/templates/signed-out.html @@ -1,11 +1,29 @@ -{% extends 'layouts/_base.html' %} +{% extends "errors/_base.html" %} + +{% from "components/panel/_macro.njk" import onsPanel %} {% set page_title = _("Signed out") %} {% block main %} -

{{ _("Your survey answers have been saved. You are now signed out") }}.

- - {% if account_service_log_out_url %} - {{ _("Return to your account") }} - {% endif %} -{% endblock %} + {# djlint:off #} + {{ + onsPanel({ + "id": '', + "variant": 'success', + "iconType": 'check', + "iconSize": 'xl', + "classes": "ons-u-mb-xl", + "body": _("

Your progress has been saved

") + }) + }} + {# djlint:on #} + {% if survey_type in SURVEY_TYPES_BUSINESS + SURVEY_TYPES_DEFAULT %} +

+ {{ _("To find further information or resume the survey, return to My Account.").format(url = redirect_url) }} +

+ {% elif survey_type in SURVEY_TYPES_SOCIAL + SURVEY_TYPES_HEALTH %} +

+ {{ _("To resume the survey, re-enter your access code.").format(url = redirect_url) }} +

+ {% endif %} +{% endblock main %} diff --git a/templates/submit-with-summary.html b/templates/submit-with-summary.html index 6a2fac86bb..4e8586d9da 100644 --- a/templates/submit-with-summary.html +++ b/templates/submit-with-summary.html @@ -1,7 +1,5 @@ -{% extends 'layouts/_submit.html' %} +{% extends "layouts/_submit.html" %} {% block summary %} -
- {% include 'partials/summary/summary.html' %} -
-{% endblock summary %} +
{% include "partials/summary/summary.html" %}
+{% endblock summary %} diff --git a/templates/submit.html b/templates/submit.html index f1465577a6..4ba917cf2f 100644 --- a/templates/submit.html +++ b/templates/submit.html @@ -1 +1 @@ -{% extends 'layouts/_submit.html' %} +{% extends "layouts/_submit.html" %} diff --git a/templates/thank-you.html b/templates/thank-you.html index f92fd97042..12328fe945 100644 --- a/templates/thank-you.html +++ b/templates/thank-you.html @@ -1,86 +1,110 @@ -{% extends 'layouts/_base.html' %} -{% from 'components/panel/_macro.njk' import onsPanel %} -{% from "components/metadata/_macro.njk" import onsMetadata %} +{% extends "layouts/_base.html" %} + +{% from "components/panel/_macro.njk" import onsPanel %} +{% from "components/description-list/_macro.njk" import onsDescriptionList %} {% from "components/timeout-panel/_macro.njk" import onsTimeoutPanel %} {% set page_title = _("We’ve received your answers") %} {% set hide_sign_out_button = content.hide_sign_out_button %} -{% set breadcrumbs = { - "ariaLabel": 'Back to surveys', - "itemsList": [ - { - "url": account_service_todo_url, - "id": "back-to-surveys", - "text": _("Back to surveys"), - "attributes": { - "data-ga": 'click', - "data-ga-category": 'Navigation', - "data-ga-action": 'Back to surveys link click' - } - } - ] -} %} -{% block main %} - - {% call onsPanel({ - "type": "success", - "iconType": "check", - "iconSize": "xl", - "classes": "ons-u-mb-m" - }) %} -

{{ _("Thank you for completing the {survey_title}").format( - survey_title = survey_title) }}

-

- {{ content.submission_text }} -

- {{ onsMetadata(content.metadata) }} - {% endcall %} - - {% if content.guidance %} - {% set contents = content.guidance.contents %} - {% include 'partials/contents.html' %} - {% else %} -

{{ _("Your answers will be processed in the next few weeks.") }} - {{ _("We may contact you to query your answers via phone or secure message.") }}

-

{{ _("For more information on how we use this data.") }}
- https://www.ons.gov.uk/surveys

- {% endif %} - - {% set countdown_expired_text = _("For security, you can no longer view or get a copy of your answers") %} - - {% if content.view_submitted_response.enabled %} - {% if content.view_submitted_response.expired %} - {% call onsPanel({ - "id": "view-submitted-response-guidance", - "classes": "ons-u-mb-m" - }) %} - {{ countdown_expired_text }} - {% endcall %} +{% if account_service_todo_url %} + {# djlint:off #} + {% set breadcrumbs = { + "ariaLabel": 'Back to surveys', + "itemsList": [ + { + "url": account_service_todo_url, + "id": "back-to-surveys", + "text": _("Back to surveys"), + "attributes": { + "data-ga": "click", + "data-ga-category": "Link", + "data-ga-action": "Navigate", + "data-ga-label": "Back to Surveys", + "data-ga-page": "Thank You", + } + } + ] + } %} + {# djlint:on #} +{% endif %} +{% block main %} + {# djlint:off #} + {% call + onsPanel({ + "variant": "success", + "iconType": "check", + "iconSize": "xl", + "classes": "ons-u-mb-m" + }) + %} +

+ {{ _("Thank you for completing the {survey_title}").format( + survey_title = survey_title) }} +

+

{{ content.submission_text }}

+ {{ onsDescriptionList(content.metadata) }} + {% endcall %} + {# djlint:on #} + {% if content.guidance %} + {% set contents = content.guidance.contents %} + {% include "partials/contents.html" %} {% else %} - {% set countdown_text = _("For security, your answers will only be available to view for another ") %} -

{{ _("Get a copy of your answers") }}

-

{{ _("You can save or print your answers for your records.").format(url = content.view_submitted_response.url) }}

- {{ onsTimeoutPanel ({ - "id": "view-submitted-response-countdown", - "minutesTextSingular": _("minute"), - "minutesTextPlural": _("minutes"), - "secondsTextSingular": _("second"), - "secondsTextPlural": _("seconds"), - "countdownText": countdown_text, - "nojsText": _("For security, your answers will only be available to view for 45 minutes"), - "redirectUrl": url_for("post_submission.get_thank_you"), - "countdownExpiredText": countdown_expired_text, - "sessionExpiresAt": content.view_submitted_response.expires_at - }) }} +

+ {{ _("Your response will help inform decision-makers how best to support the UK population and economy.") }} +

+

+ {{ _("Learn more about how we use this data") }} +

{% endif %} - {% endif %} - - - - {% if content.show_feedback_call_to_action %} - {% include 'partials/feedback-call-to-action.html' %} - {% endif %} - -{% endblock %} + {% set countdown_expired_text = _("For security, you can no longer view or get a copy of your answers") %} + {% if content.view_submitted_response.enabled %} + {% if content.view_submitted_response.expired %} + {# djlint:off #} + {% call + onsPanel({ + "id": "view-submitted-response-guidance", + "classes": "ons-u-mb-m" + }) + %} + {{ countdown_expired_text }} + {% endcall %} + {# djlint:on #} + {% else %} + {% set countdown_text = _("For security, your answers will only be available to view for another ") %} +

{{ _("Get a copy of your answers") }}

+

{{ _("We may contact you to query your answers.") }}

+

+ {{ _("If you need a copy for your records, save or print your answers.").format(url = content.view_submitted_response.url) }} +

+ {# djlint:off #} + {{ onsTimeoutPanel ({ + "id": "view-submitted-response-countdown", + "redirectUrl": url_for("post_submission.get_thank_you"), + "minutesTextSingular": _("minute"), + "minutesTextPlural": _("minutes"), + "secondsTextSingular": _("second"), + "secondsTextPlural": _("seconds"), + "countdownText": countdown_text, + "nojsText": _("For security, your answers will only be available to view for 45 minutes"), + "countdownExpiredText": countdown_expired_text, + "sessionExpiresAt": content.view_submitted_response.expires_at + }) + }} + {# djlint:on #} + {% endif %} + {% endif %} + {% if content.confirmation_email_form %} +
+

{{ _("Get confirmation email") }}

+

+ {{ _("If you would like to be sent confirmation that you have completed your survey, enter your email address") }} +

+ {% with form=content.confirmation_email_form %} + {% include "partials/confirmation-email-form.html" %} + {% endwith %} + {% endif %} + {% if content.show_feedback_call_to_action %} + {% include "partials/feedback-call-to-action.html" %} + {% endif %} +{% endblock main %} diff --git a/templates/unrelatedquestion.html b/templates/unrelatedquestion.html index 9956ae57e6..2f61a13acd 100644 --- a/templates/unrelatedquestion.html +++ b/templates/unrelatedquestion.html @@ -1 +1 @@ -{% include 'question.html' %} \ No newline at end of file +{% include "question.html" %} diff --git a/templates/view-submitted-response.html b/templates/view-submitted-response.html index 1a05d061a2..b86ed939a3 100644 --- a/templates/view-submitted-response.html +++ b/templates/view-submitted-response.html @@ -1,69 +1,80 @@ -{% extends 'layouts/_base.html' %} +{% extends "layouts/_base.html" %} {% from "components/panel/_macro.njk" import onsPanel %} {% from "components/button/_macro.njk" import onsButton %} -{% from "components/metadata/_macro.njk" import onsMetadata %} +{% from "components/description-list/_macro.njk" import onsDescriptionList %} {% set hide_sign_out_button = content.hide_sign_out_button %} {% set sign_out_url = content.sign_out_url %} - {% set breadcrumbs = { - "ariaLabel": 'Back', - "itemsList": [ - { - "url": url_for("post_submission.get_thank_you"), - "id": "top-previous", - "text": _("Back"), - "attributes": { - "data-ga": 'click', - "data-ga-category": 'Navigation', - "data-ga-action": 'Previous link click' - } - } - ] -}%} + "ariaLabel": 'Back', + "itemsList": [ + { + "url": url_for("post_submission.get_thank_you"), + "id": "top-previous", + "text": _("Back"), + "attributes": { + "data-ga": "click", + "data-ga-category": "Link", + "data-ga-action": "Navigate", + "data-ga-label": "Previous", + "data-ga-page": "View Submitted Response", + } + } + ] +} %} {% block main %} -

- {{ content.submitted_text}} -

- {{ onsMetadata(content.metadata)}} - {% if not content.view_submitted_response.expired %} - {{ - onsButton({ - "type": 'button', - "text": _('Print answers'), - "buttonStyle": "print", - "variants": ['small', 'secondary'], - "attributes": { - "data-qa": "btn-print" - } - }) - }} - {{ - onsButton({ - "buttonStyle": 'download', - "text": _('Save answers as PDF'), - "submitType": "timer", - "variants": ['small', 'secondary'], - "url": content.pdf_url, - "removeDownloadAttribute": true, - "attributes": { - "data-qa": "btn-pdf" - } - }) - }} - {% block summary %} -
- {% include 'partials/summary/summary.html' %} -
- {% endblock summary %} - {% else %} - {% call onsPanel({ - "id": "view-submitted-guidance", - "classes": "ons-u-mb-m" - }) %} - {{ _("For security, you can no longer view or get a copy of your answers") }} - {% endcall %} - {% endif %} -{% endblock %} +

{{ content.submitted_text }}

+ {{ onsDescriptionList(content.metadata) }} + {% if not content.view_submitted_response.expired %} + {# djlint:off #} + {{ + onsButton({ + "type": 'button', + "text": _('Print answers'), + "variants": ['small', 'secondary', 'print'], + "attributes": { + "data-qa": "btn-print", + "data-ga-category": "Button", + "data-ga-action": "View", + "data-ga-label": "Print Dialogue", + "data-ga-page": "View Submitted Response", + "data-ga": "click", + } + }) + }} + {{ + onsButton({ + "text": _('Save answers as PDF') , + "variants": ['small', 'secondary', 'timer', 'download'], + "url": content.pdf_url, + "removeDownloadAttribute": true, + "attributes": { + "data-qa": "btn-pdf", + "data-ga-category": "Button", + "data-ga-action": "Download", + "data-ga-label": "Download PDF", + "data-ga-page": "View Submitted Response", + "data-ga": "click", + } + }) + }} + {# djlint:on #} + + {% block summary %} +
{% include "partials/summary/summary.html" %}
+ {% endblock summary %} + {% else %} + {# djlint:off #} + {% call + onsPanel({ + "id": "view-submitted-guidance", + "classes": "ons-u-mb-m" + }) + %} + {{ _("For security, you can no longer view or get a copy of your answers") }} + {% endcall %} + {# djlint:on #} + {% endif %} +{% endblock main %} diff --git a/tests/app/authentication/test_roles.py b/tests/app/authentication/test_roles.py index 2d8174e9c5..a59431e4c4 100644 --- a/tests/app/authentication/test_roles.py +++ b/tests/app/authentication/test_roles.py @@ -2,6 +2,7 @@ from werkzeug.exceptions import Forbidden from app.authentication.roles import role_required +from tests.app.questionnaire.conftest import get_metadata def test_role_required_unauthenticated_no_metadata( @@ -59,7 +60,7 @@ def test_role_required_authenticated_with_metadata_none_roles( # Given I am authenticated but my metadata contains a # roles list set to None mock_current_user.is_authenticated = True - mock_get_metadata.return_value = {"roles": None} + mock_get_metadata.return_value = get_metadata(extra_metadata={"roles": None}) # And I have decorated a function with role_required wrapper = role_required("dumper") @@ -77,7 +78,7 @@ def test_role_required_authenticated_with_metadata_empty_roles( # Given I am authenticated and my metadata contains an empty # roles list mock_current_user.is_authenticated = True - mock_get_metadata.return_value = {"roles": []} + mock_get_metadata.return_value = get_metadata(extra_metadata={"roles": []}) # And I have decorated a function with role_required wrapper = role_required("dumper") @@ -94,7 +95,7 @@ def test_role_required_authenticated_with_metadata_wrong_role( ): # Given I am authenticated and my metadata contains a single role. mock_current_user.is_authenticated = True - mock_get_metadata.return_value = {"roles": ["flusher"]} + mock_get_metadata.return_value = get_metadata(extra_metadata={"roles": ["flusher"]}) # And I have decorated a function with role_required, specifying a # role that isn't in the metadata roles list. @@ -112,7 +113,7 @@ def test_role_required_authenticated_with_metadata_matching_role( ): # Given I am authenticated and my metadata contains a single role. mock_current_user.is_authenticated = True - mock_get_metadata.return_value = {"roles": ["dumper"]} + mock_get_metadata.return_value = get_metadata(extra_metadata={"roles": ["dumper"]}) # And I have decorated a function with role_required, specifying a # role that is listed in the metadata roles list. @@ -129,7 +130,9 @@ def test_role_required_authenticated_with_metadata_matching_multiple_role( ): # Given I am authenticated but my metadata contains multiples roles mock_current_user.is_authenticated = True - mock_get_metadata.return_value = {"roles": ["flusher", "other", "dumper"]} + mock_get_metadata.return_value = get_metadata( + extra_metadata={"roles": ["flusher", "other", "dumper"]} + ) # And I have decorated a function with role_required, specifying a # role that is listed in the metadata roles list. @@ -146,7 +149,9 @@ def test_role_required_wrapped_with_positional_arguments( ): # Given I am authenticated and my metadata contains roles mock_current_user.is_authenticated = True - mock_get_metadata.return_value = {"roles": ["flusher", "other", "dumper"]} + mock_get_metadata.return_value = get_metadata( + extra_metadata={"roles": ["flusher", "other", "dumper"]} + ) # And I have decorated a function that takes multiple positional arguments # with role_required, specifying a role that is listed in the metadata @@ -164,7 +169,9 @@ def test_role_required_wrapped_with_keyword_arguments( ): # Given I am authenticated and my metadata contains roles mock_current_user.is_authenticated = True - mock_get_metadata.return_value = {"roles": ["flusher", "other", "dumper"]} + mock_get_metadata.return_value = get_metadata( + extra_metadata={"roles": ["flusher", "other", "dumper"]} + ) # And I have decorated a function that takes multiple positional arguments # with role_required, specifying a role that is listed in the metadata @@ -182,7 +189,9 @@ def test_role_required_wrapped_with_positional_and_keyword_arguments( ): # Given I am authenticated and my metadata contains roles mock_current_user.is_authenticated = True - mock_get_metadata.return_value = {"roles": ["flusher", "other", "dumper"]} + mock_get_metadata.return_value = get_metadata( + extra_metadata={"roles": ["flusher", "other", "dumper"]} + ) # And I have decorated a function that takes multiple positional arguments # with role_required, specifying a role that is listed in the metadata @@ -217,7 +226,9 @@ def test_role_required_unauthenticated_wrapped_with_keyword_arguments( ): # Given I am not authenticated mock_current_user.is_authenticated = False - mock_get_metadata.return_value = {"roles": ["flusher", "other", "dumper"]} + mock_get_metadata.return_value = get_metadata( + extra_metadata={"roles": ["flusher", "other", "dumper"]} + ) # And I have decorated a function that takes multiple positional arguments # with role_required, specifying a role that is listed in the metadata @@ -236,7 +247,9 @@ def test_role_required_unauthenticated_wrapped_with_positional_and_keyword_argum ): # Given I am not authenticated mock_current_user.is_authenticated = False - mock_get_metadata.return_value = {"roles": ["flusher", "other", "dumper"]} + mock_get_metadata.return_value = get_metadata( + extra_metadata={"roles": ["flusher", "other", "dumper"]} + ) # And I have decorated a function that takes multiple positional arguments # with role_required, specifying a role that is listed in the metadata diff --git a/tests/app/conftest.py b/tests/app/conftest.py index 09c3a368f3..3a1a8d0622 100644 --- a/tests/app/conftest.py +++ b/tests/app/conftest.py @@ -1,15 +1,28 @@ # pylint: disable=redefined-outer-name - +from copy import deepcopy from datetime import datetime, timedelta, timezone +from http.client import HTTPMessage import fakeredis import pytest +from mock import MagicMock +from mock.mock import Mock +from requests.adapters import ConnectTimeoutError, ReadTimeoutError +from urllib3.connectionpool import HTTPConnectionPool +from urllib3.response import HTTPResponse +from app.data_models import QuestionnaireStore from app.data_models.answer_store import AnswerStore +from app.data_models.data_stores import DataStores from app.data_models.list_store import ListStore +from app.data_models.metadata_proxy import MetadataProxy from app.data_models.progress_store import ProgressStore from app.data_models.session_data import SessionData from app.data_models.session_store import SessionStore +from app.data_models.supplementary_data_store import ( + SupplementaryDataListMapping, + SupplementaryDataStore, +) from app.publisher import PubSubPublisher from app.questionnaire.location import Location from app.setup import create_app @@ -73,25 +86,56 @@ def expires_at(): @pytest.fixture(name="session_store") def fixture_session_store(session_data): - session_store = SessionStore("user_ik", "pepper", "eq_session_id") + session_store = SessionStore( + "user_ik", + "pepper", + "eq_session_id", + ) session_store.session_data = session_data + session_store.user_id = "user_id" return session_store +@pytest.fixture +def fake_questionnaire_store(): + storage = MagicMock() + storage.get_user_data = MagicMock(return_value=("{}", "ce_sid", 1, None)) + storage.add_or_update = MagicMock() + store = QuestionnaireStore(storage) + store.data_stores.metadata = MetadataProxy.from_dict( + { + "schema_name": "test_checkbox", + "display_address": "68 Abingdon Road, Goathill", + "tx_id": "tx_id", + "language_code": "en", + } + ) + + return store + + +@pytest.fixture +def fake_metadata(): + return MetadataProxy.from_dict( + { + "tx_id": "tx_id", + "language_code": "en", + "display_address": "68 Abingdon Road, Goathill", + } + ) + + @pytest.fixture def session_data(): return SessionData( - tx_id="tx_id", - schema_name="test_checkbox", - period_str="period_str", language_code=None, - launch_language_code=None, - survey_url=None, - ru_name="ru_name", - ru_ref="ru_ref", - response_id="response_id", - trad_as="trading_as", - case_id="case_id", + ) + + +@pytest.fixture +def session_data_with_language_code(): + return SessionData( + language_code="en", ) @@ -125,11 +169,21 @@ def progress_store(): return ProgressStore() +@pytest.fixture +def supplementary_data_store(): + return SupplementaryDataStore() + + +@pytest.fixture +def data_stores(): + return DataStores() + + @pytest.fixture def publisher(mocker): mocker.patch( "app.publisher.publisher.google.auth._default._get_explicit_environ_credentials", - return_value=(mocker.Mock(), "test-project-id"), + return_value=(mocker.Mock(universe_domain="test"), "test-project-id"), ) return PubSubPublisher() @@ -152,6 +206,248 @@ def current_location(): return Location(section_id="some-section", block_id="some-block") +@pytest.fixture +def location(): + return Location("test-section", "test-block", "test-list", "list_item_id") + + @pytest.fixture def mock_autoescape_context(mocker): return mocker.Mock(autoescape=True) + + +@pytest.fixture +def mocked_response_content(mocker): + decodable_content = Mock() + decodable_content.decode.return_value = b"{}" + mocker.patch("requests.models.Response.content", decodable_content) + + +@pytest.fixture +def mocked_make_request_with_timeout( + mocker, mocked_response_content # pylint: disable=unused-argument +): + connect_timeout_error = ConnectTimeoutError("connect timed out") + read_timeout_error = ReadTimeoutError( + pool=None, message="read timed out", url="test-url" + ) + + response_not_timed_out = HTTPResponse(status=200, headers={}, msg=HTTPMessage()) + response_not_timed_out.drain_conn = Mock(return_value=None) + + return mocker.patch.object( + HTTPConnectionPool, + "_make_request", + side_effect=[ + connect_timeout_error, + read_timeout_error, + response_not_timed_out, + ], + ) + + +@pytest.fixture +def supplementary_data(): + return { + "schema_version": "v1", + "identifier": "12345678901", + "note": { + "title": "Volume of total production", + "example": { + "title": "Including", + "description": "Sales across all UK stores", + }, + }, + "guidance": "Some supplementary guidance about the survey", + "items": { + "products": [ + { + "identifier": 89929001, + "name": "Articles and equipment for sports or outdoor games", + "cn_codes": "2504 + 250610 + 2512 + 2519 + 2524", + "guidance": {"title": "Include", "description": "sportswear"}, + "value_sales": { + "answer_code": "89929001", + "label": "Value of sales", + }, + }, + { + "identifier": "201630601", + "name": "Other Minerals", + "cn_codes": "5908 + 5910 + 591110 + 591120 + 591140", + "value_sales": { + "answer_code": "201630601", + "label": "Value of sales", + }, + }, + ] + }, + } + + +@pytest.fixture +def supplementary_data_with_employees(supplementary_data): + copy = deepcopy(supplementary_data) + copy["items"]["employees"] = [ + { + "identifier": "429001", + "personal_details": { + "forename": "Harry", + "surname": "Potter", + "address": { + "postcode": "BS1 1AJ", + "house_number": "12", + "city": "Bristol", + }, + }, + "employment_details": { + "job_title": "Customer assistant", + "start_date": "2020-01-01", + "salary": { + "payroll_number": "54345", + "value": "25000", + "currency": "GBP", + }, + }, + }, + { + "identifier": "529001", + "personal_details": { + "forename": "Bruce", + "surname": "Wayne", + "address": { + "postcode": "BS1 1HJ", + "house_number": "15", + "city": "Bristol", + }, + }, + "employment_details": { + "job_title": "Customer assistant", + "start_date": "2019-03-01", + "salary": { + "payroll_number": "4345", + "value": "27000", + "currency": "GBP", + }, + }, + }, + { + "identifier": "629011", + "personal_details": { + "forename": "Henry", + "surname": "Green", + "address": { + "postcode": "BS1 1HR", + "house_number": "11", + "city": "Bristol", + }, + }, + "employment_details": { + "job_title": "Warehouse operative", + "start_date": "2022-10-01", + "salary": { + "payroll_number": "28379", + "value": "29000", + "currency": "GBP", + }, + }, + }, + { + "identifier": "729011", + "personal_details": { + "forename": "Fourth", + "surname": "Person", + "address": { + "postcode": "BS1 1HO", + "house_number": "14", + "city": "Bristol", + }, + }, + "employment_details": { + "job_title": "Warehouse operative", + "start_date": "2022-11-01", + "salary": { + "payroll_number": "22238379", + "value": "29500", + "currency": "GBP", + }, + }, + }, + ] + return copy + + +@pytest.fixture +def supplementary_data_list_mappings(): + return { + "products": [ + SupplementaryDataListMapping(identifier=89929001, list_item_id="item-1"), + SupplementaryDataListMapping(identifier="201630601", list_item_id="item-2"), + ], + } + + +@pytest.fixture +def supplementary_data_list_mappings_extra_item(): + return { + "products": [ + SupplementaryDataListMapping(identifier=89929001, list_item_id="item-1"), + SupplementaryDataListMapping(identifier="201630601", list_item_id="item-2"), + SupplementaryDataListMapping(identifier="103219277", list_item_id="item-3"), + ], + } + + +@pytest.fixture +def supplementary_data_employee_list_mappings(): + return { + "employees": [ + SupplementaryDataListMapping( + identifier="429001", list_item_id="employee-1" + ), + SupplementaryDataListMapping( + identifier="529001", list_item_id="employee-2" + ), + SupplementaryDataListMapping( + identifier="629011", list_item_id="employee-3" + ), + SupplementaryDataListMapping( + identifier="729011", list_item_id="employee-4" + ), + ], + } + + +@pytest.fixture +def supplementary_data_store_with_data( + supplementary_data, supplementary_data_list_mappings +): + return SupplementaryDataStore( + supplementary_data=supplementary_data, + list_mappings=supplementary_data_list_mappings, + ) + + +@pytest.fixture +def supplementary_data_store_with_data_extra_item( + supplementary_data, supplementary_data_list_mappings_extra_item +): + return SupplementaryDataStore( + supplementary_data=supplementary_data, + list_mappings=supplementary_data_list_mappings_extra_item, + ) + + +@pytest.fixture +def supplementary_data_store_with_employees( + supplementary_data_with_employees, + supplementary_data_list_mappings, + supplementary_data_employee_list_mappings, +): + return SupplementaryDataStore( + supplementary_data=supplementary_data_with_employees, + list_mappings={ + **supplementary_data_list_mappings, + **supplementary_data_employee_list_mappings, + }, + ) diff --git a/tests/app/data_model/conftest.py b/tests/app/data_model/conftest.py index ecc3d9b4af..4e182b8abb 100644 --- a/tests/app/data_model/conftest.py +++ b/tests/app/data_model/conftest.py @@ -2,15 +2,16 @@ import pytest +from app.data_models import CompletionStatus from app.data_models.answer_store import Answer -from app.data_models.progress_store import CompletionStatus +from app.data_models.progress import ProgressDict from app.data_models.session_store import SessionStore from app.storage import storage_encryption +from tests.app.parser.conftest import get_response_expires_at @pytest.fixture def basic_answer_store(answer_store): - answer_store.add_or_update( Answer(answer_id="answer1", value=10, list_item_id="abc123") ) @@ -40,7 +41,6 @@ def basic_answer_store(answer_store): @pytest.fixture def relationship_answer_store(answer_store): - answer_store.add_or_update( Answer( answer_id="relationship-answer", @@ -69,7 +69,6 @@ def relationship_answer_store(answer_store): @pytest.fixture def store_to_serialize(answer_store): - answer_store.add_or_update( Answer(answer_id="answer1", value=10, list_item_id="abc123") ) @@ -84,17 +83,21 @@ def store_to_serialize(answer_store): @pytest.fixture def basic_input(): return { - "METADATA": {"test": True}, + "METADATA": { + "test": True, + "response_expires_at": get_response_expires_at(), + }, "ANSWERS": [{"answer_id": "test", "value": "test"}], "LISTS": [], "PROGRESS": [ - { - "section_id": "a-test-section", - "list_item_id": "abc123", - "status": CompletionStatus.COMPLETED, - "block_ids": ["a-test-block"], - } + ProgressDict( + section_id="a-test-section", + list_item_id="abc123", + status=CompletionStatus.COMPLETED, + block_ids=["a-test-block"], + ) ], + "SUPPLEMENTARY_DATA": {"data": {}, "list_mappings": {}}, "RESPONSE_METADATA": {"test-meta": "test"}, } diff --git a/tests/app/data_model/test_answer_store.py b/tests/app/data_model/test_answer_store.py index 4688736bf8..6ebc1d0536 100644 --- a/tests/app/data_model/test_answer_store.py +++ b/tests/app/data_model/test_answer_store.py @@ -1,4 +1,3 @@ -# pylint: disable=redefined-outer-name import pytest from app.data_models.answer_store import Answer, AnswerStore @@ -103,13 +102,13 @@ def test_remove_answer_without_list_item_id(basic_answer_store): def test_remove_all_answers_for_list_item_id(relationship_answer_store): - relationship_answer_store.remove_all_answers_for_list_item_id("abc123") + relationship_answer_store.remove_all_answers_for_list_item_ids("abc123") assert relationship_answer_store.get_answer("answer1") is None def test_remove_all_answers_for_list_item_id_doesnt_exist(relationship_answer_store): len_before = len(relationship_answer_store) - relationship_answer_store.remove_all_answers_for_list_item_id("not-an-id") + relationship_answer_store.remove_all_answers_for_list_item_ids("not-an-id") assert len(relationship_answer_store) == len_before diff --git a/tests/app/data_model/test_list_store.py b/tests/app/data_model/test_list_store.py index 0f99322cac..8765686c05 100644 --- a/tests/app/data_model/test_list_store.py +++ b/tests/app/data_model/test_list_store.py @@ -91,6 +91,14 @@ def test_delete_list_item_id(): assert not store._lists # pylint: disable=protected-access +def test_delete_list(): + store = ListStore() + store.add_list_item("people") + store.add_list_item("people") + store.delete_list("people") + assert not store._lists # pylint: disable=protected-access + + def test_delete_list_item_id_does_not_raise(): store = ListStore() store.add_list_item("people") @@ -151,3 +159,24 @@ def test_first_raises_index_error_when_list_is_empty(): assert "unable to access first item in list, list 'people' is empty" in str( error.value ) + + +def test_get_item_using_method(): + store = ListStore() + + first_id = store.add_list_item("people") + + item = store.get("people") + + assert item.items[0] == first_id + + +def test_lookup_list_items(): + store = ListStore() + + person_id = store.add_list_item("people") + item_id = store.add_list_item("items") + + assert store.get_list_name_for_list_item_id(person_id) == "people" + assert store.get_list_name_for_list_item_id(item_id) == "items" + assert store.get_list_name_for_list_item_id("not-a-list-item-id") is None diff --git a/tests/app/data_model/test_metadata_proxy.py b/tests/app/data_model/test_metadata_proxy.py new file mode 100644 index 0000000000..d5411f3232 --- /dev/null +++ b/tests/app/data_model/test_metadata_proxy.py @@ -0,0 +1,54 @@ +import pytest +from werkzeug.datastructures import ImmutableDict + +from app.authentication.auth_payload_versions import AuthPayloadVersion +from app.data_models.metadata_proxy import MetadataProxy, SurveyMetadata + +METADATA_V2 = { + "version": AuthPayloadVersion.V2.value, + "schema_name": "1_0000", + "response_id": "1", + "account_service_url": "account_service_url", + "tx_id": "tx_id", + "collection_exercise_sid": "collection_exercise_sid", + "case_id": "case_id", + "response_expires_at": "2023-04-24T10:46:32+00:00", + "survey_metadata": { + "data": { + "ru_ref": "12345678901A", + }, + }, +} + + +@pytest.mark.parametrize( + "resolved_metadata_proxy_value, metadata_var", + ( + ( + MetadataProxy.from_dict(METADATA_V2)["ru_ref"], + METADATA_V2["survey_metadata"]["data"]["ru_ref"], + ), + ( + MetadataProxy.from_dict(METADATA_V2)["schema_name"], + METADATA_V2["schema_name"], + ), + ( + MetadataProxy.from_dict(METADATA_V2)["response_expires_at"], + METADATA_V2["response_expires_at"], + ), + (MetadataProxy.from_dict(METADATA_V2)["non_existing"], None), + ), +) +def test_metadata_proxy_returns_value_for_valid_key( + resolved_metadata_proxy_value, metadata_var +): + assert resolved_metadata_proxy_value == metadata_var + + +def test_survey_metadata_returns_valid_key(): + expected_values = {"key": "value"} + data = ImmutableDict(expected_values) + + survey_metadata = SurveyMetadata(data=data) + + assert survey_metadata["key"] == expected_values["key"] diff --git a/tests/app/data_model/test_progress_store.py b/tests/app/data_model/test_progress_store.py index 020bd38ffa..1f2712e286 100644 --- a/tests/app/data_model/test_progress_store.py +++ b/tests/app/data_model/test_progress_store.py @@ -1,8 +1,9 @@ import pytest -from app.data_models.progress import Progress -from app.data_models.progress_store import CompletionStatus, ProgressStore -from app.questionnaire.location import Location +from app.data_models import CompletionStatus +from app.data_models.progress import Progress, ProgressDict +from app.data_models.progress_store import ProgressStore +from app.questionnaire.location import Location, SectionKey def test_serialisation(): @@ -11,7 +12,8 @@ def test_serialisation(): store.add_completed_location(Location(section_id="s1", block_id="one")) store.add_completed_location(Location(section_id="s1", block_id="two")) store.update_section_status( - section_status=CompletionStatus.COMPLETED, section_id="s1" + status=CompletionStatus.COMPLETED, + section_key=SectionKey("s1"), ) store.add_completed_location( @@ -23,58 +25,60 @@ def test_serialisation(): ) ) store.update_section_status( - section_status=CompletionStatus.IN_PROGRESS, - section_id="s2", - list_item_id="abc123", + status=CompletionStatus.IN_PROGRESS, + section_key=SectionKey("s2", "abc123"), ) serialized = store.serialize() assert serialized == [ Progress.from_dict( - { - "section_id": "s1", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["one", "two"], - } + ProgressDict( + section_id="s1", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["one", "two"], + ) ), Progress.from_dict( - { - "section_id": "s2", - "list_item_id": "abc123", - "status": CompletionStatus.IN_PROGRESS, - "block_ids": ["another-one"], - } + ProgressDict( + section_id="s2", + list_item_id="abc123", + status=CompletionStatus.IN_PROGRESS, + block_ids=["another-one"], + ) ), ] def test_deserialisation(): in_progress_sections = [ - { - "section_id": "s1", - "list_item_id": None, - "status": CompletionStatus.IN_PROGRESS, - "block_ids": ["one", "two"], - }, - { - "section_id": "s2", - "list_item_id": "abc123", - "status": CompletionStatus.COMPLETED, - "block_ids": ["three", "four"], - }, + ProgressDict( + section_id="s1", + list_item_id=None, + status=CompletionStatus.IN_PROGRESS, + block_ids=["one", "two"], + ), + ProgressDict( + section_id="s2", + list_item_id="abc123", + status=CompletionStatus.COMPLETED, + block_ids=["three", "four"], + ), ] store = ProgressStore(in_progress_sections) - assert store.get_section_status(section_id="s1") == CompletionStatus.IN_PROGRESS - assert store.get_completed_block_ids("s1") == ["one", "two"] + assert ( + store.get_section_status(section_key=SectionKey("s1")) + == CompletionStatus.IN_PROGRESS + ) + assert store.get_completed_block_ids(section_key=SectionKey("s1")) == ["one", "two"] assert ( - store.get_section_status(section_id="s2", list_item_id="abc123") + store.get_section_status(section_key=SectionKey("s2", "abc123")) == CompletionStatus.COMPLETED ) - assert store.get_completed_block_ids(section_id="s2", list_item_id="abc123") == [ + assert store.get_completed_block_ids(section_key=SectionKey("s2", "abc123")) == [ "three", "four", ] @@ -82,18 +86,18 @@ def test_deserialisation(): def test_clear(): in_progress_sections = [ - { - "section_id": "s1", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["one", "two"], - }, - { - "section_id": "s2", - "list_item_id": "abc123", - "status": CompletionStatus.COMPLETED, - "block_ids": ["three", "four"], - }, + ProgressDict( + section_id="s1", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["one", "two"], + ), + ProgressDict( + section_id="s2", + list_item_id="abc123", + status=CompletionStatus.COMPLETED, + block_ids=["three", "four"], + ), ] store = ProgressStore(in_progress_sections) @@ -117,10 +121,10 @@ def test_add_completed_location(): store.add_completed_location(non_repeating_location) store.add_completed_location(repeating_location) - assert store.get_completed_block_ids(section_id="s1") == [ + assert store.get_completed_block_ids(section_key=SectionKey("s1")) == [ non_repeating_location.block_id ] - assert store.get_completed_block_ids(section_id="s2", list_item_id="abc123") == [ + assert store.get_completed_block_ids(section_key=SectionKey("s2", "abc123")) == [ repeating_location.block_id ] @@ -129,18 +133,18 @@ def test_add_completed_location(): def test_add_completed_location_existing(): completed = [ - { - "section_id": "s1", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["one"], - }, - { - "section_id": "s2", - "list_item_id": "abc123", - "status": CompletionStatus.COMPLETED, - "block_ids": ["three", "four"], - }, + ProgressDict( + section_id="s1", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["one"], + ), + ProgressDict( + section_id="s2", + list_item_id="abc123", + status=CompletionStatus.COMPLETED, + block_ids=["three", "four"], + ), ] store = ProgressStore(completed) @@ -152,15 +156,18 @@ def test_add_completed_location_existing(): store.add_completed_location(non_repeating_location) store.add_completed_location(repeating_location) - assert store.get_section_status(section_id="s1") == CompletionStatus.COMPLETED assert ( - store.get_section_status(section_id="s2", list_item_id="abc123") + store.get_section_status(section_key=SectionKey("s1")) + == CompletionStatus.COMPLETED + ) + assert ( + store.get_section_status(section_key=SectionKey("s2", "abc123")) == CompletionStatus.COMPLETED ) - assert len(store.get_completed_block_ids(section_id="s1")) == 1 + assert len(store.get_completed_block_ids(section_key=SectionKey("s1"))) == 1 assert ( - len(store.get_completed_block_ids(section_id="s2", list_item_id="abc123")) == 2 + len(store.get_completed_block_ids(section_key=SectionKey("s2", "abc123"))) == 2 ) assert not store.is_dirty @@ -168,18 +175,18 @@ def test_add_completed_location_existing(): def test_add_completed_location_new(): completed = [ - { - "section_id": "s1", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["one"], - }, - { - "section_id": "s2", - "list_item_id": "abc123", - "status": CompletionStatus.COMPLETED, - "block_ids": ["three", "four"], - }, + ProgressDict( + section_id="s1", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["one"], + ), + ProgressDict( + section_id="s2", + list_item_id="abc123", + status=CompletionStatus.COMPLETED, + block_ids=["three", "four"], + ), ] store = ProgressStore(completed) @@ -191,15 +198,18 @@ def test_add_completed_location_new(): store.add_completed_location(non_repeating_location) store.add_completed_location(repeating_location) - assert store.get_section_status(section_id="s1") == CompletionStatus.COMPLETED assert ( - store.get_section_status(section_id="s2", list_item_id="abc123") + store.get_section_status(section_key=SectionKey("s1")) + == CompletionStatus.COMPLETED + ) + assert ( + store.get_section_status(section_key=SectionKey("s2", "abc123")) == CompletionStatus.COMPLETED ) - assert len(store.get_completed_block_ids(section_id="s1")) == 2 + assert len(store.get_completed_block_ids(section_key=SectionKey("s1"))) == 2 assert ( - len(store.get_completed_block_ids(section_id="s2", list_item_id="abc123")) == 3 + len(store.get_completed_block_ids(section_key=SectionKey("s2", "abc123"))) == 3 ) assert store.is_dirty @@ -207,24 +217,24 @@ def test_add_completed_location_new(): def test_remove_completed_location(): completed = [ - { - "section_id": "s1", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["one", "two"], - }, - { - "section_id": "s2", - "list_item_id": "abc123", - "status": CompletionStatus.COMPLETED, - "block_ids": ["three", "four"], - }, - { - "section_id": "s3", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["one"], - }, + ProgressDict( + section_id="s1", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["one", "two"], + ), + ProgressDict( + section_id="s2", + list_item_id="abc123", + status=CompletionStatus.COMPLETED, + block_ids=["three", "four"], + ), + ProgressDict( + section_id="s3", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["one"], + ), ] store = ProgressStore(completed) @@ -238,27 +248,36 @@ def test_remove_completed_location(): store.remove_completed_location(repeating_location) store.remove_completed_location(last_and_final_location) - assert store.get_completed_block_ids(section_id="s1") == ["two"] - assert store.get_completed_block_ids(section_id="s2", list_item_id="abc123") == [ + assert store.get_completed_block_ids(section_key=SectionKey("s1")) == ["two"] + assert store.get_completed_block_ids(section_key=SectionKey("s2", "abc123")) == [ "four" ] - assert store.get_completed_block_ids(section_id="s3") == [] + assert store.get_completed_block_ids(section_key=SectionKey("s3")) == [] - assert store.get_section_status("s1") == CompletionStatus.COMPLETED - assert store.get_section_status("s2", "abc123") == CompletionStatus.COMPLETED - assert store.get_section_status("s3") == CompletionStatus.IN_PROGRESS + assert ( + store.get_section_status(section_key=SectionKey("s1")) + == CompletionStatus.COMPLETED + ) + assert ( + store.get_section_status(section_key=SectionKey("s2", "abc123")) + == CompletionStatus.COMPLETED + ) + assert ( + store.get_section_status(section_key=SectionKey("s3")) + == CompletionStatus.IN_PROGRESS + ) assert store.is_dirty def test_remove_non_existent_completed_location(): completed = [ - { - "section_id": "s1", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["one"], - } + ProgressDict( + section_id="s1", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["one"], + ) ] store = ProgressStore(completed) @@ -270,39 +289,42 @@ def test_remove_non_existent_completed_location(): store.remove_completed_location(non_repeating_location) store.remove_completed_location(repeating_location) - assert len(store.get_completed_block_ids(section_id="s1")) == 1 + assert len(store.get_completed_block_ids(section_key=SectionKey("s1"))) == 1 assert not store.is_dirty def test_update_section_status(): completed = [ - { - "section_id": "s1", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["one"], - }, - { - "section_id": "s2", - "list_item_id": "abc123", - "status": CompletionStatus.COMPLETED, - "block_ids": ["three"], - }, + ProgressDict( + section_id="s1", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["one"], + ), + ProgressDict( + section_id="s2", + list_item_id="abc123", + status=CompletionStatus.COMPLETED, + block_ids=["three"], + ), ] store = ProgressStore(completed) store.update_section_status( - section_status=CompletionStatus.IN_PROGRESS, section_id="s1" + status=CompletionStatus.IN_PROGRESS, + section_key=SectionKey("s1"), ) store.update_section_status( - section_status=CompletionStatus.IN_PROGRESS, - section_id="s2", - list_item_id="abc123", + status=CompletionStatus.IN_PROGRESS, + section_key=SectionKey("s2", "abc123"), ) - assert store.get_section_status(section_id="s1") == CompletionStatus.IN_PROGRESS assert ( - store.get_section_status(section_id="s2", list_item_id="abc123") + store.get_section_status(section_key=SectionKey("s1")) + == CompletionStatus.IN_PROGRESS + ) + assert ( + store.get_section_status(section_key=SectionKey("s2", "abc123")) == CompletionStatus.IN_PROGRESS ) assert store.is_dirty @@ -310,155 +332,164 @@ def test_update_section_status(): def test_update_non_existing_section_status(): completed = [ - { - "section_id": "s1", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["one"], - } + ProgressDict( + section_id="s1", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["one"], + ) ] store = ProgressStore(completed) - store.update_section_status("s2", CompletionStatus.IN_PROGRESS) + store.update_section_status( + status=CompletionStatus.IN_PROGRESS, + section_key=SectionKey("s2"), + ) - assert store.get_section_status("s1") == CompletionStatus.COMPLETED + assert ( + store.get_section_status(section_key=SectionKey("s1")) + == CompletionStatus.COMPLETED + ) assert "s2" not in store - assert store.get_completed_block_ids(section_id="s2") == [] + assert store.get_completed_block_ids(section_key=SectionKey("s2")) == [] assert not store.is_dirty def test_get_section_status(): existing_progress = [ - { - "section_id": "s1", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["one"], - }, - { - "section_id": "s2", - "list_item_id": "abc123", - "status": CompletionStatus.IN_PROGRESS, - "block_ids": ["three"], - }, + ProgressDict( + section_id="s1", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["one"], + ), + ProgressDict( + section_id="s2", + list_item_id="abc123", + status=CompletionStatus.IN_PROGRESS, + block_ids=["three"], + ), ] store = ProgressStore(existing_progress) - assert store.get_section_status(section_id="s1") == CompletionStatus.COMPLETED assert ( - store.get_section_status(section_id="s2", list_item_id="abc123") + store.get_section_status(section_key=SectionKey("s1")) + == CompletionStatus.COMPLETED + ) + assert ( + store.get_section_status(section_key=SectionKey("s2", "abc123")) == CompletionStatus.IN_PROGRESS ) def test_get_section_locations(): completed = [ - { - "section_id": "s1", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["one"], - }, - { - "section_id": "s2", - "list_item_id": "abc123", - "status": CompletionStatus.IN_PROGRESS, - "block_ids": ["three"], - }, + ProgressDict( + section_id="s1", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["one"], + ), + ProgressDict( + section_id="s2", + list_item_id="abc123", + status=CompletionStatus.IN_PROGRESS, + block_ids=["three"], + ), ] store = ProgressStore(completed) - assert store.get_completed_block_ids(section_id="s1") == ["one"] + assert store.get_completed_block_ids(section_key=SectionKey("s1")) == ["one"] - assert store.get_completed_block_ids(section_id="s2", list_item_id="abc123") == [ + assert store.get_completed_block_ids(section_key=SectionKey("s2", "abc123")) == [ "three" ] def test_is_section_complete(): completed = [ - { - "section_id": "s1", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["one", "two"], - }, - { - "section_id": "s2", - "list_item_id": None, - "status": CompletionStatus.IN_PROGRESS, - "block_ids": ["three"], - }, - { - "section_id": "s3", - "list_item_id": "abc123", - "status": CompletionStatus.IN_PROGRESS, - "block_ids": ["three"], - }, - { - "section_id": "s4", - "list_item_id": "123abc", - "status": CompletionStatus.COMPLETED, - "block_ids": ["not-three"], - }, - { - "section_id": "s5", - "list_item_id": "456def", - "status": CompletionStatus.INDIVIDUAL_RESPONSE_REQUESTED, - "block_ids": ["not-three"], - }, + ProgressDict( + section_id="s1", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["one", "two"], + ), + ProgressDict( + section_id="s2", + list_item_id=None, + status=CompletionStatus.IN_PROGRESS, + block_ids=["three"], + ), + ProgressDict( + section_id="s3", + list_item_id="abc123", + status=CompletionStatus.IN_PROGRESS, + block_ids=["three"], + ), + ProgressDict( + section_id="s4", + list_item_id="123abc", + status=CompletionStatus.COMPLETED, + block_ids=["not-three"], + ), + ProgressDict( + section_id="s5", + list_item_id="456def", + status=CompletionStatus.INDIVIDUAL_RESPONSE_REQUESTED, + block_ids=["not-three"], + ), ] store = ProgressStore(completed) - assert store.is_section_complete(section_id="s1", list_item_id=None) is True - assert store.is_section_complete(section_id="s4", list_item_id="123abc") is True - assert store.is_section_complete(section_id="s5", list_item_id="456def") is True + assert store.is_section_complete(section_key=SectionKey("s1")) + assert store.is_section_complete(SectionKey("s4", "123abc")) + assert store.is_section_complete(SectionKey("s5", "456def")) def test_remove_progress_for_list_item_id(): completed = [ - { - "section_id": "s1", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["one", "two"], - }, - { - "section_id": "s2", - "list_item_id": None, - "status": CompletionStatus.IN_PROGRESS, - "block_ids": ["three"], - }, - { - "section_id": "s3", - "list_item_id": "abc123", - "status": CompletionStatus.IN_PROGRESS, - "block_ids": ["three"], - }, - { - "section_id": "s4", - "list_item_id": "123abc", - "status": CompletionStatus.COMPLETED, - "block_ids": ["not-three"], - }, + ProgressDict( + section_id="s1", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["one", "two"], + ), + ProgressDict( + section_id="s2", + list_item_id=None, + status=CompletionStatus.IN_PROGRESS, + block_ids=["three"], + ), + ProgressDict( + section_id="s3", + list_item_id="abc123", + status=CompletionStatus.IN_PROGRESS, + block_ids=["three"], + ), + ProgressDict( + section_id="s4", + list_item_id="123abc", + status=CompletionStatus.COMPLETED, + block_ids=["not-three"], + ), ] store = ProgressStore(completed) store.remove_progress_for_list_item_id(list_item_id="abc123") - assert ("s3", "abc123") not in store - assert store.get_completed_block_ids(section_id="s3", list_item_id="abc123") == [] + assert (SectionKey("s3", "abc123")) not in store + assert store.get_completed_block_ids(SectionKey("s3", "abc123")) == [] - assert ("s4", "123abc") in store + assert SectionKey("s4", "123abc") in store store.remove_progress_for_list_item_id(list_item_id="123abc") - assert ("s4", "123abc") not in store - assert store.get_completed_block_ids(section_id="s4", list_item_id="123abc") == [] + assert (SectionKey("s4", "123abc")) not in store + assert store.get_completed_block_ids(SectionKey("s4", "123abc")) == [] @pytest.mark.parametrize( @@ -476,36 +507,36 @@ def test_remove_progress_for_list_item_id(): ) def test_in_progress_and_completed_section_ids(section_ids, expected_section_keys): completed = [ - { - "section_id": "s1", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["one", "two"], - }, - { - "section_id": "s2", - "list_item_id": None, - "status": CompletionStatus.IN_PROGRESS, - "block_ids": ["three"], - }, - { - "section_id": "s3", - "list_item_id": "abc123", - "status": CompletionStatus.NOT_STARTED, - "block_ids": ["three"], - }, - { - "section_id": "s4", - "list_item_id": "123abc", - "status": CompletionStatus.COMPLETED, - "block_ids": ["not-three"], - }, - { - "section_id": "s5", - "list_item_id": "456def", - "status": CompletionStatus.IN_PROGRESS, - "block_ids": ["not-three"], - }, + ProgressDict( + section_id="s1", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["one", "two"], + ), + ProgressDict( + section_id="s2", + list_item_id=None, + status=CompletionStatus.IN_PROGRESS, + block_ids=["three"], + ), + ProgressDict( + section_id="s3", + list_item_id="abc123", + status=CompletionStatus.NOT_STARTED, + block_ids=["three"], + ), + ProgressDict( + section_id="s4", + list_item_id="123abc", + status=CompletionStatus.COMPLETED, + block_ids=["not-three"], + ), + ProgressDict( + section_id="s5", + list_item_id="456def", + status=CompletionStatus.IN_PROGRESS, + block_ids=["not-three"], + ), ] store = ProgressStore(completed) @@ -518,36 +549,36 @@ def test_in_progress_and_completed_section_ids(section_ids, expected_section_key def test_section_keys(): completed = [ - { - "section_id": "s1", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["one", "two"], - }, - { - "section_id": "s2", - "list_item_id": None, - "status": CompletionStatus.IN_PROGRESS, - "block_ids": ["three"], - }, - { - "section_id": "s3", - "list_item_id": "abc123", - "status": CompletionStatus.NOT_STARTED, - "block_ids": ["three"], - }, - { - "section_id": "s4", - "list_item_id": "123abc", - "status": CompletionStatus.COMPLETED, - "block_ids": ["not-three"], - }, - { - "section_id": "s5", - "list_item_id": "456def", - "status": CompletionStatus.IN_PROGRESS, - "block_ids": ["not-three"], - }, + ProgressDict( + section_id="s1", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["one", "two"], + ), + ProgressDict( + section_id="s2", + list_item_id=None, + status=CompletionStatus.IN_PROGRESS, + block_ids=["three"], + ), + ProgressDict( + section_id="s3", + list_item_id="abc123", + status=CompletionStatus.NOT_STARTED, + block_ids=["three"], + ), + ProgressDict( + section_id="s4", + list_item_id="123abc", + status=CompletionStatus.COMPLETED, + block_ids=["not-three"], + ), + ProgressDict( + section_id="s5", + list_item_id="456def", + status=CompletionStatus.IN_PROGRESS, + block_ids=["not-three"], + ), ] store = ProgressStore(completed) diff --git a/tests/app/data_model/test_questionnaire_store.py b/tests/app/data_model/test_questionnaire_store.py index c86f161fe9..a7a17b5240 100644 --- a/tests/app/data_model/test_questionnaire_store.py +++ b/tests/app/data_model/test_questionnaire_store.py @@ -1,8 +1,11 @@ import pytest -from app.data_models import QuestionnaireStore from app.data_models.answer_store import AnswerStore +from app.data_models.metadata_proxy import MetadataProxy from app.data_models.progress_store import ProgressStore +from app.data_models.questionnaire_store import QuestionnaireStore +from app.data_models.supplementary_data_store import SupplementaryDataStore +from app.questionnaire.location import SectionKey from app.utilities.json import json_dumps, json_loads @@ -18,21 +21,28 @@ def test_questionnaire_store_json_loads( questionnaire_store.input_data = json_dumps(basic_input) # When store = QuestionnaireStore(questionnaire_store.storage) + data_stores = store.data_stores # Then - assert store.metadata.copy() == basic_input["METADATA"] - assert store.response_metadata == basic_input["RESPONSE_METADATA"] - assert store.answer_store == AnswerStore(basic_input["ANSWERS"]) + assert data_stores.metadata == MetadataProxy.from_dict(basic_input["METADATA"]) + assert data_stores.response_metadata == basic_input["RESPONSE_METADATA"] + assert data_stores.answer_store == AnswerStore(basic_input["ANSWERS"]) assert not hasattr(store, "NOT_A_LEGAL_TOP_LEVEL_KEY") assert not hasattr(store, "not_a_legal_top_level_key") expected_completed_block_ids = basic_input["PROGRESS"][0]["block_ids"][0] assert ( - len(store.progress_store.get_completed_block_ids("a-test-section", "abc123")) + len( + data_stores.progress_store.get_completed_block_ids( + SectionKey("a-test-section", "abc123") + ) + ) == 1 ) assert ( - store.progress_store.get_completed_block_ids("a-test-section", "abc123")[0] + data_stores.progress_store.get_completed_block_ids( + SectionKey("a-test-section", "abc123") + )[0] == expected_completed_block_ids ) @@ -43,20 +53,25 @@ def test_questionnaire_store_missing_keys(questionnaire_store, basic_input): questionnaire_store.input_data = json_dumps(basic_input) # When store = QuestionnaireStore(questionnaire_store.storage) + data_stores = store.data_stores # Then - assert store.metadata.copy() == basic_input["METADATA"] - assert store.response_metadata == basic_input["RESPONSE_METADATA"] - assert store.answer_store == AnswerStore(basic_input["ANSWERS"]) - assert not store.progress_store.serialize() + assert data_stores.metadata == MetadataProxy.from_dict(basic_input["METADATA"]) + assert data_stores.response_metadata == basic_input["RESPONSE_METADATA"] + assert data_stores.answer_store == AnswerStore(basic_input["ANSWERS"]) + assert not data_stores.progress_store.serialize() def test_questionnaire_store_updates_storage(questionnaire_store, basic_input): # Given store = QuestionnaireStore(questionnaire_store.storage) + data_stores = store.data_stores store.set_metadata(basic_input["METADATA"]) - store.answer_store = AnswerStore(basic_input["ANSWERS"]) - store.response_metadata = basic_input["RESPONSE_METADATA"] - store.progress_store = ProgressStore(basic_input["PROGRESS"]) + data_stores.answer_store = AnswerStore(basic_input["ANSWERS"]) + data_stores.response_metadata = basic_input["RESPONSE_METADATA"] + data_stores.progress_store = ProgressStore(basic_input["PROGRESS"]) + store.supplementary_data_store = SupplementaryDataStore.deserialize( + basic_input["SUPPLEMENTARY_DATA"] + ) # When store.save() @@ -74,9 +89,10 @@ class NotSerializable: store = QuestionnaireStore(questionnaire_store.storage) store.set_metadata(non_serializable_metadata) - store.response_metadata = basic_input["RESPONSE_METADATA"] - store.answer_store = AnswerStore(basic_input["ANSWERS"]) - store.progress_store = ProgressStore(basic_input["PROGRESS"]) + data_stores = store.data_stores + data_stores.response_metadata = basic_input["RESPONSE_METADATA"] + data_stores.answer_store = AnswerStore(basic_input["ANSWERS"]) + data_stores.progress_store = ProgressStore(basic_input["PROGRESS"]) # When / Then with pytest.raises(TypeError): @@ -87,22 +103,22 @@ def test_questionnaire_store_deletes(questionnaire_store, basic_input): # Given store = QuestionnaireStore(questionnaire_store.storage) store.set_metadata(basic_input["METADATA"]) - store.response_metadata = basic_input["RESPONSE_METADATA"] - store.answer_store = AnswerStore(basic_input["ANSWERS"]) - store.progress_store = ProgressStore(basic_input["PROGRESS"]) + data_stores = store.data_stores + data_stores.response_metadata = basic_input["RESPONSE_METADATA"] + data_stores.answer_store = AnswerStore(basic_input["ANSWERS"]) + data_stores.progress_store = ProgressStore(basic_input["PROGRESS"]) # When store.delete() # Then - assert "a-test-section" not in store.progress_store - assert store.metadata.copy() == {} - assert len(store.answer_store) == 0 - assert store.response_metadata == {} + assert "a-test-section" not in data_stores.progress_store + assert len(data_stores.answer_store) == 0 + assert data_stores.response_metadata == {} def test_questionnaire_store_raises_when_writing_to_metadata(questionnaire_store): store = QuestionnaireStore(questionnaire_store.storage) with pytest.raises(TypeError): - store.metadata["no"] = "writing" + store.data_stores.metadata["no"] = "writing" diff --git a/tests/app/data_model/test_relationship_store.py b/tests/app/data_model/test_relationship_store.py index 1343a7f08d..8bb5f61c1e 100644 --- a/tests/app/data_model/test_relationship_store.py +++ b/tests/app/data_model/test_relationship_store.py @@ -27,7 +27,7 @@ def test_deserialisation(): assert len(relationship_store) == 2 -def test_clear(): # pylint: disable=redefined-outer-name +def test_clear(): relationship_store = RelationshipStore(relationships) relationship_store.clear() diff --git a/tests/app/data_model/test_session_data.py b/tests/app/data_model/test_session_data.py new file mode 100644 index 0000000000..9ea26f2c96 --- /dev/null +++ b/tests/app/data_model/test_session_data.py @@ -0,0 +1,15 @@ +import pytest + +from app.data_models import SessionData + + +def test_session_data_default_properties(): + try: + session_data = SessionData( + language_code="cy", + ) + except TypeError: + return pytest.fail("An error occurred when creating session data") + + assert session_data.confirmation_email_count == 0 + assert session_data.feedback_count == 0 diff --git a/tests/app/data_model/test_session_store.py b/tests/app/data_model/test_session_store.py index b553dd9790..60cc3bb05d 100644 --- a/tests/app/data_model/test_session_store.py +++ b/tests/app/data_model/test_session_store.py @@ -39,7 +39,8 @@ def test_save(app, app_session_store): expires_at=app_session_store.expires_at, ).save() session_store = SessionStore("user_ik", "pepper", "eq_session_id") - assert session_store.session_data.tx_id == "tx_id" + + assert session_store.session_data.confirmation_email_count == 0 def test_delete(app, app_session_store): @@ -63,12 +64,12 @@ def test_add_data_to_session(app, app_session_store): session_data=app_session_store.session_data, expires_at=app_session_store.expires_at, ).save() - display_address = "68 Abingdon Road, Goathill" - app_session_store.session_store.session_data.display_address = display_address + feedback_count = 9 + app_session_store.session_store.session_data.feedback_count = feedback_count app_session_store.session_store.save() session_store = SessionStore("user_ik", "pepper", "eq_session_id") - assert session_store.session_data.display_address == display_address + assert session_store.session_data.feedback_count == feedback_count def test_should_not_delete_when_no_session(app, app_session_store): @@ -113,31 +114,30 @@ def test_session_store_ignores_multiple_new_values_in_session_data( ).save() session_store = SessionStore("user_ik", "pepper", "eq_session_id") - assert hasattr(session_store.session_data, "additional_value") is False assert hasattr(session_store.session_data, "second_additional_value") is False -def test_session_store_stores_trading_as_value_if_present( - app, app_session_store, session_data +def test_session_store_stores_language_code_value( + app, app_session_store, session_data_with_language_code ): with app.test_request_context(): app_session_store.session_store.create( eq_session_id="eq_session_id", user_id="test", - session_data=session_data, + session_data=session_data_with_language_code, expires_at=app_session_store.expires_at, ).save() session_store = SessionStore("user_ik", "pepper", "eq_session_id") - assert hasattr(session_store.session_data, "trad_as") is True + assert session_store.session_data.language_code == "en" -def test_session_store_stores_none_for_trading_as_if_not_present( +def test_session_store_stores_none_for_language_code_value( app, app_session_store, session_data ): - session_data.trad_as = None + session_data.language_code = None with app.test_request_context(): app_session_store.session_store.create( eq_session_id="eq_session_id", @@ -148,7 +148,7 @@ def test_session_store_stores_none_for_trading_as_if_not_present( session_store = SessionStore("user_ik", "pepper", "eq_session_id") - assert session_store.session_data.trad_as is None + assert session_store.session_data.language_code is None @pytest.mark.usefixtures("app") @@ -166,8 +166,8 @@ def test_legacy_load(app_session_store_encoded): app_session_store_encoded.session_id, ) - assert ( - session_store.session_data.tx_id == app_session_store_encoded.session_data.tx_id + assert vars(session_store.session_data) == vars( + app_session_store_encoded.session_data ) @@ -184,8 +184,9 @@ def test_load(app_session_store_encoded): app_session_store_encoded.pepper, app_session_store_encoded.session_id, ) - assert ( - session_store.session_data.tx_id == app_session_store_encoded.session_data.tx_id + + assert vars(session_store.session_data) == vars( + app_session_store_encoded.session_data ) diff --git a/tests/app/data_model/test_supplementary_data_store.py b/tests/app/data_model/test_supplementary_data_store.py new file mode 100644 index 0000000000..5491e9795a --- /dev/null +++ b/tests/app/data_model/test_supplementary_data_store.py @@ -0,0 +1,93 @@ +import pytest + +from app.data_models.supplementary_data_store import ( + InvalidSupplementaryDataSelector, + SupplementaryDataStore, +) +from app.utilities.make_immutable import make_immutable + + +def test_supplementary_data_serialisation( + supplementary_data_store_with_data, + supplementary_data, + supplementary_data_list_mappings, +): + serialized = supplementary_data_store_with_data.serialize() + + assert serialized == { + "data": supplementary_data, + "list_mappings": supplementary_data_list_mappings, + } + + +def test_supplementary_data_deserialisation(): + raw_data = { + "identifier": "12345678901", + "items": { + "products": [ + {"identifier": 89929001}, + {"identifier": "201630601"}, + ] + }, + } + list_mappings = { + "products": [ + {"identifier": 89929001, "list_item_id": "item-1"}, + {"identifier": "201630601", "list_item_id": "item-2"}, + ] + } + + serialized = { + "data": raw_data, + "list_mappings": list_mappings, + } + + deserialized = SupplementaryDataStore.deserialize(serialized) + + assert deserialized.raw_data == make_immutable(raw_data) + assert deserialized.list_mappings == make_immutable(list_mappings) + assert deserialized._data_map == { # pylint: disable=protected-access + ("identifier", None): "12345678901", + ("products", "item-1"): {"identifier": 89929001}, + ("products", "item-2"): {"identifier": "201630601"}, + } + + +def test_empty_supplementary_data_deserialisation(): + empty_store = SupplementaryDataStore.deserialize({}) + assert not empty_store.raw_data + assert not empty_store.list_mappings + assert not empty_store._data_map # pylint: disable=protected-access + + +@pytest.mark.parametrize( + "identifier,list_item_id,selectors,expected", + [ + ("identifier", None, None, "12345678901"), + ("note", None, ["title"], "Volume of total production"), + ("products", "item-2", ["name"], "Other Minerals"), + ("products", "item-1", ["value_sales", "answer_code"], "89929001"), + ("products", "item-1", ["guidance", "title"], "Include"), + ("products", "item-2", ["guidance", "title"], None), + ("INVALID", None, None, None), + ], +) +def test_get_supplementary_data( + supplementary_data_store_with_data, identifier, list_item_id, selectors, expected +): + assert ( + supplementary_data_store_with_data.get_data( + identifier=identifier, + list_item_id=list_item_id, + selectors=selectors, + ) + == expected + ) + + +def test_get_supplementary_data_invalid_selectors(supplementary_data_store_with_data): + with pytest.raises(InvalidSupplementaryDataSelector) as exception: + supplementary_data_store_with_data.get_data( + identifier="identifier", selectors=["INVALID"], list_item_id=None + ) + assert "Cannot use the selector `INVALID` on non-nested data" == str(exception) diff --git a/tests/app/forms/conftest.py b/tests/app/forms/conftest.py index 1e88dd02fe..d5891ae308 100644 --- a/tests/app/forms/conftest.py +++ b/tests/app/forms/conftest.py @@ -3,7 +3,7 @@ @pytest.fixture def mock_form(mocker): - return mocker.Mock() + return mocker.MagicMock() @pytest.fixture diff --git a/tests/app/forms/field_handlers/conftest.py b/tests/app/forms/field_handlers/conftest.py index 63ef58a25b..ff1a04e61d 100644 --- a/tests/app/forms/field_handlers/conftest.py +++ b/tests/app/forms/field_handlers/conftest.py @@ -1,11 +1,10 @@ import pytest -from app.data_models.answer_store import AnswerStore -from app.data_models.list_store import ListStore -from app.forms.field_handlers.select_handlers import Choice, ChoiceWithDetailAnswer +from app.data_models.data_stores import DataStores from app.questionnaire import QuestionnaireSchema from app.questionnaire.rules.rule_evaluator import RuleEvaluator from app.questionnaire.value_source_resolver import ValueSourceResolver +from app.utilities.types import Choice, ChoiceWithDetailAnswer def get_mock_schema(): @@ -26,10 +25,7 @@ def get_mock_response_metadata(): @pytest.fixture def rule_evaluator(): evaluator = RuleEvaluator( - answer_store=AnswerStore(), - list_store=ListStore(), - metadata={}, - response_metadata=get_mock_response_metadata(), + data_stores=DataStores(response_metadata=get_mock_response_metadata()), schema=get_mock_schema(), location=None, ) @@ -40,10 +36,7 @@ def rule_evaluator(): @pytest.fixture def value_source_resolver(): resolver = ValueSourceResolver( - answer_store=AnswerStore(), - list_store=ListStore(), - metadata={}, - response_metadata=get_mock_response_metadata(), + data_stores=DataStores(response_metadata=get_mock_response_metadata()), schema=get_mock_schema(), location=None, list_item_id=None, diff --git a/tests/app/forms/field_handlers/test_address_handler.py b/tests/app/forms/field_handlers/test_address_handler.py index 5eb2a5a723..8a32d131cf 100644 --- a/tests/app/forms/field_handlers/test_address_handler.py +++ b/tests/app/forms/field_handlers/test_address_handler.py @@ -7,8 +7,13 @@ def get_test_form_class( - answer_schema, value_source_resolver, rule_evaluator, messages=error_messages.copy() + answer_schema, + value_source_resolver, + rule_evaluator, + messages=None, ): + if not messages: + messages = error_messages.copy() address_handler = AddressHandler( answer_schema, value_source_resolver, rule_evaluator, error_messages=messages ) @@ -64,8 +69,6 @@ def test_no_validation_when_address_not_mandatory( value_source_resolver, ) form.validate() - # pylint: disable=no-member - # wtforms Form parents are not discoverable in the 2.3.3 implementation assert not form.errors @@ -79,8 +82,6 @@ def test_mandatory_validation_when_address_line_1_missing( ) form = test_form_class(MultiDict({"test_field": "1"}), value_source_resolver) form.validate() - # pylint: disable=no-member - # wtforms Form parents are not discoverable in the 2.3.3 implementation assert form.errors["test_field"]["line1"][0] == "Enter an address" diff --git a/tests/app/forms/field_handlers/test_date_handler.py b/tests/app/forms/field_handlers/test_date_handler.py index 994d2917cf..08c941cc2a 100644 --- a/tests/app/forms/field_handlers/test_date_handler.py +++ b/tests/app/forms/field_handlers/test_date_handler.py @@ -51,7 +51,7 @@ def test_generate_date_form_validates_single_date_period( ): schema = load_schema_from_name("test_date_validation_single") value_source_resolver.schema = schema - value_source_resolver.metadata = {"ref_p_start_date": "2017-02-20"} + value_source_resolver.data_stores.metadata = {"ref_p_start_date": "2017-02-20"} handler = DateHandler( schema.get_answers_by_answer_id("date-range-from")[0], @@ -115,7 +115,7 @@ def test_get_referenced_offset_value_for_now_value( def test_get_referenced_offset_value_for_meta( app, value_source_resolver, rule_evaluator ): - value_source_resolver.metadata = {"date": "2018-02-20"} + value_source_resolver.data_stores.metadata = {"date": "2018-02-20"} answer = {"minimum": {"value": {"identifier": "date", "source": "metadata"}}} handler = DateHandler(answer, value_source_resolver, rule_evaluator, error_messages) @@ -135,7 +135,7 @@ def test_get_referenced_offset_value_for_answer_id( test_answer_id = Answer(answer_id="date", value="2018-03-20") answer_store.add_or_update(test_answer_id) - value_source_resolver.answer_store = answer_store + value_source_resolver.data_stores.answer_store = answer_store answer = {"maximum": {"value": {"identifier": "date", "source": "answers"}}} handler = DateHandler(answer, value_source_resolver, rule_evaluator, error_messages) @@ -146,8 +146,8 @@ def test_get_referenced_offset_value_for_answer_id( @patch( - "app.questionnaire.questionnaire_schema.QuestionnaireSchema.is_repeating_answer", - return_value=True, + "app.questionnaire.questionnaire_schema.QuestionnaireSchema.get_list_name_for_answer_id", + return_value="list", ) def test_get_referenced_offset_value_with_list_item_id( app, value_source_resolver, rule_evaluator @@ -163,7 +163,7 @@ def test_get_referenced_offset_value_with_list_item_id( answer_store = AnswerStore() answer_store.add_or_update(test_answer_id) - value_source_resolver.answer_store = answer_store + value_source_resolver.data_stores.answer_store = answer_store value_source_resolver.location = location value_source_resolver.list_item_id = list_item_id answer = { @@ -195,12 +195,12 @@ def test_get_referenced_offset_value_with_no_offset( "app.utilities.schema.load_schema_from_name", return_value=QuestionnaireSchema({}) ) def test_minimum_and_maximum_offset_dates(app, value_source_resolver, rule_evaluator): - value_source_resolver.metadata = {"date": "2018-02-20"} + value_source_resolver.data_stores.metadata = {"date": "2018-02-20"} answer_store = AnswerStore() test_answer_id = Answer(answer_id="date", value="2018-03-20") answer_store.add_or_update(test_answer_id) - value_source_resolver.answer_store = answer_store + value_source_resolver.data_stores.answer_store = answer_store answer = { "id": "date_answer", "type": "Date", diff --git a/tests/app/forms/field_handlers/test_number_handler.py b/tests/app/forms/field_handlers/test_number_handler.py index 56e034683b..d99574e71a 100644 --- a/tests/app/forms/field_handlers/test_number_handler.py +++ b/tests/app/forms/field_handlers/test_number_handler.py @@ -9,13 +9,12 @@ from app.forms.fields import DecimalFieldWithSeparator, IntegerFieldWithSeparator from app.settings import MAX_NUMBER -# pylint: disable=no-member -# wtforms Form parents are not discoverable in the 2.3.3 implementation - def get_test_form_class( - answer_schema, value_source_resolver, rule_evaluator, messages=error_messages.copy() + answer_schema, value_source_resolver, rule_evaluator, messages=None ): + if not messages: + messages = error_messages.copy() handler = NumberHandler( answer_schema, value_source_resolver, rule_evaluator, error_messages=messages ) @@ -383,7 +382,7 @@ def test_get_schema_value_answer_store(value_source_resolver, rule_evaluator): answer_store.add_or_update(Answer(answer_id="set-maximum", value=10)) answer_store.add_or_update(Answer(answer_id="set-minimum", value=1)) - value_source_resolver.answer_store = answer_store + value_source_resolver.data_stores.answer_store = answer_store number_handler = NumberHandler( answer_schema, value_source_resolver, rule_evaluator, error_messages ) diff --git a/tests/app/forms/fields/test_date_field.py b/tests/app/forms/fields/test_date_field.py index f1a65d9fe2..7581fb1a23 100644 --- a/tests/app/forms/fields/test_date_field.py +++ b/tests/app/forms/fields/test_date_field.py @@ -1,10 +1,11 @@ -from wtforms import Form, validators +from wtforms import Form from app.forms.fields import date_field +from app.forms.validators import OptionalForm def test_generate_date_form_creates_empty_form(): - form_class = date_field.get_form_class([validators.Optional()]) + form_class = date_field.get_form_class([OptionalForm()]) assert hasattr(form_class, "day") assert hasattr(form_class, "month") @@ -12,7 +13,7 @@ def test_generate_date_form_creates_empty_form(): def test_date_form_empty_data(): - form = date_field.get_form_class([validators.Optional()]) + form = date_field.get_form_class([OptionalForm()]) assert form().data is None @@ -21,7 +22,7 @@ def test_date_form_format_data(): data = {"field": "2000-01-01"} class TestForm(Form): - field = date_field.DateField([validators.Optional()]) + field = date_field.DateField(validators=[OptionalForm()]) test_form = TestForm(data=data) diff --git a/tests/app/forms/fields/test_month_year_date_field.py b/tests/app/forms/fields/test_month_year_date_field.py index b618428b00..1cf89f1951 100644 --- a/tests/app/forms/fields/test_month_year_date_field.py +++ b/tests/app/forms/fields/test_month_year_date_field.py @@ -1,10 +1,11 @@ -from wtforms import Form, validators +from wtforms import Form from app.forms.fields import month_year_date_field +from app.forms.validators import OptionalForm def test_generate_month_year_date_form_creates_empty_form(): - form_class = month_year_date_field.get_form_class([validators.Optional()]) + form_class = month_year_date_field.get_form_class([OptionalForm()]) assert not hasattr(form_class, "day") assert hasattr(form_class, "month") @@ -12,7 +13,7 @@ def test_generate_month_year_date_form_creates_empty_form(): def test_month_year_date_form_empty_data(): - form = month_year_date_field.get_form_class([validators.Optional()]) + form = month_year_date_field.get_form_class([OptionalForm()]) assert form().data is None @@ -21,7 +22,7 @@ def test_month_year_date_form_format_data(): data = {"field": "2000-01"} class TestForm(Form): - field = month_year_date_field.MonthYearDateField([validators.Optional()]) + field = month_year_date_field.MonthYearDateField(validators=[OptionalForm()]) test_form = TestForm(data=data) diff --git a/tests/app/forms/fields/test_year_date_field.py b/tests/app/forms/fields/test_year_date_field.py index d44207b912..813e1127e3 100644 --- a/tests/app/forms/fields/test_year_date_field.py +++ b/tests/app/forms/fields/test_year_date_field.py @@ -1,10 +1,11 @@ -from wtforms import Form, validators +from wtforms import Form from app.forms.fields import year_date_field +from app.forms.validators import OptionalForm def test_generate_year_date_form_creates_empty_form(): - form_class = year_date_field.get_form_class([validators.Optional()]) + form_class = year_date_field.get_form_class([OptionalForm()]) assert not hasattr(form_class, "day") assert not hasattr(form_class, "month") @@ -12,7 +13,7 @@ def test_generate_year_date_form_creates_empty_form(): def test_year_date_form_empty_data(): - form = year_date_field.get_form_class([validators.Optional()]) + form = year_date_field.get_form_class([OptionalForm()]) assert form().data is None @@ -21,7 +22,7 @@ def test_year_date_form_format_data(): data = {"field": "2000"} class TestForm(Form): - field = year_date_field.YearDateField([validators.Optional()]) + field = year_date_field.YearDateField(validators=[OptionalForm()]) test_form = TestForm(data=data) diff --git a/tests/app/forms/test_custom_fields.py b/tests/app/forms/test_custom_fields.py index a1d5454371..ad6f48542c 100644 --- a/tests/app/forms/test_custom_fields.py +++ b/tests/app/forms/test_custom_fields.py @@ -1,3 +1,5 @@ +from decimal import Decimal + import pytest from wtforms.fields import Field @@ -9,18 +11,29 @@ def test_text_area_a_wtforms_field(mock_form): - text_area = MaxTextAreaField("LabelText", _form=mock_form, name="aName") + text_area = MaxTextAreaField( + label="LabelText", + _form=mock_form, + name="aName", + rows=0, + maxlength=0, + ) assert isinstance(text_area, Field) def test_text_area_supports_maxlength_property(mock_form): text_area = MaxTextAreaField( - "TestLabel", maxlength=20, _form=mock_form, name="aName" + label="TestLabel", + maxlength=20, + _form=mock_form, + name="aName", + rows=0, ) assert isinstance(text_area, Field) assert text_area.maxlength == 20 +@pytest.mark.usefixtures("gb_locale") def test_integer_field(mock_form): integer_field = IntegerFieldWithSeparator(_form=mock_form, name="aName") assert isinstance(integer_field, Field) @@ -31,6 +44,44 @@ def test_integer_field(mock_form): pytest.fail("Exceptions should not thrown by CustomIntegerField") +@pytest.mark.usefixtures("gb_locale") +@pytest.mark.parametrize( + "number_input, result", + [ + ("_110", 110), + ("1_10", 110), + ("1__10", 110), + ("1_1,0", 110), + ("_1_1,0,0", 1100), + ("1.10", None), + ], +) +def test_integer_field_inputs(mock_form, number_input, result): + integer_field = IntegerFieldWithSeparator(_form=mock_form, name="aName") + integer_field.process_formdata([number_input]) + + assert integer_field.data == result + + +@pytest.mark.usefixtures("gb_locale") +@pytest.mark.parametrize( + "number_input, result", + [ + ("1_1,0", Decimal("110")), + ("1.10", Decimal("1.1")), + ("_1.1_0", Decimal("1.1")), + ("_1.1_0,0", Decimal("1.1")), + ("_1._1,0,0", Decimal("1.1")), + ], +) +def test_decimal_field_inputs(mock_form, number_input, result): + decimal_field = DecimalFieldWithSeparator(_form=mock_form, name="aName") + decimal_field.process_formdata([number_input]) + + assert decimal_field.data == result + + +@pytest.mark.usefixtures("gb_locale") def test_decimal_field(mock_form): decimal_field = DecimalFieldWithSeparator(_form=mock_form, name="aName") assert isinstance(decimal_field, Field) diff --git a/tests/app/forms/test_field_factory.py b/tests/app/forms/test_field_factory.py index bab122042a..e8c7ca5906 100644 --- a/tests/app/forms/test_field_factory.py +++ b/tests/app/forms/test_field_factory.py @@ -1,5 +1,6 @@ import pytest +from app.data_models.data_stores import DataStores from app.forms import error_messages from app.forms.field_handlers import get_field_handler from app.questionnaire import QuestionnaireSchema @@ -7,7 +8,7 @@ from app.questionnaire.value_source_resolver import ValueSourceResolver -def test_invalid_field_type_raises_on_invalid(answer_store, list_store): +def test_invalid_field_type_raises_on_invalid(): schema = QuestionnaireSchema( { "questionnaire_flow": { @@ -25,20 +26,15 @@ def test_invalid_field_type_raises_on_invalid(answer_store, list_store): "period_str": "2016-01-01", "ref_p_start_date": "2016-02-02", "ref_p_end_date": "2016-03-03", - "ru_ref": "432423423423", + "ru_ref": "12345678901A", "ru_name": "Apple", "return_by": "2016-07-07", "case_id": "1234567890", "case_ref": "1000000000000001", } - response_metadata = {} - value_source_resolver = ValueSourceResolver( - answer_store=answer_store, - list_store=list_store, - metadata=metadata, - response_metadata=response_metadata, + data_stores=DataStores(metadata=metadata, response_metadata={}), schema=schema, location=None, list_item_id=None, @@ -46,10 +42,7 @@ def test_invalid_field_type_raises_on_invalid(answer_store, list_store): ) rule_evaluator = RuleEvaluator( - answer_store=answer_store, - list_store=list_store, - metadata=metadata, - response_metadata=response_metadata, + data_stores=DataStores(), schema=schema, location=None, ) diff --git a/tests/app/forms/test_questionnaire_form.py b/tests/app/forms/test_questionnaire_form.py index 1c4b9b22c2..8708f3fdbb 100644 --- a/tests/app/forms/test_questionnaire_form.py +++ b/tests/app/forms/test_questionnaire_form.py @@ -2,10 +2,11 @@ from decimal import Decimal import pytest -from mock import patch from werkzeug.datastructures import MultiDict +from app.data_models import ListStore from app.data_models.answer_store import Answer, AnswerStore +from app.data_models.data_stores import DataStores from app.forms import error_messages from app.forms.questionnaire_form import generate_form from app.forms.validators import ( @@ -13,9 +14,10 @@ ResponseRequired, format_message_with_title, ) -from app.questionnaire import QuestionnaireSchema +from app.questionnaire import Location, QuestionnaireSchema from app.questionnaire.placeholder_renderer import PlaceholderRenderer from app.utilities.schema import load_schema_from_name +from tests.app.questionnaire.conftest import get_metadata def error_exists(answer_id, msg, mapped_errors): @@ -26,26 +28,23 @@ def error_exists(answer_id, msg, mapped_errors): ) -def test_form_ids_match_block_answer_ids(app, answer_store, list_store): +def test_form_ids_match_block_answer_ids(app, data_stores): with app.test_request_context(): schema = load_schema_from_name("test_textfield") question_schema = schema.get_block("name-block").get("question") form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata={}, - response_metadata={}, + schema=schema, + question_schema=question_schema, + data_stores=data_stores, ) for answer_id in schema.get_answer_ids_for_block("name-block"): assert hasattr(form, answer_id) -def test_form_date_range_populates_data(app, answer_store, list_store): +def test_form_date_range_populates_data(app, data_stores): with app.test_request_context(): schema = load_schema_from_name("test_date_range") @@ -67,20 +66,18 @@ def test_form_date_range_populates_data(app, answer_store, list_store): "date-range-from-answer": "2016-03-01", "date-range-to-answer": "2016-03-31", } + form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata={}, - response_metadata={}, + schema=schema, + question_schema=question_schema, + data_stores=data_stores, form_data=form_data, ) assert form.data == expected_form_data -def test_date_range_matching_dates_raises_question_error(app, answer_store, list_store): +def test_date_range_matching_dates_raises_question_error(app, data_stores): with app.test_request_context(): schema = load_schema_from_name("test_date_range") @@ -102,14 +99,12 @@ def test_date_range_matching_dates_raises_question_error(app, answer_store, list "date-range-from-answer": "2016-12-25", "date-range-to-answer": "2016-12-25", } + form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata={}, - response_metadata={}, + schema=schema, + question_schema=question_schema, form_data=form_data, + data_stores=data_stores, ) form.validate() @@ -120,9 +115,7 @@ def test_date_range_matching_dates_raises_question_error(app, answer_store, list ) -def test_date_range_to_precedes_from_raises_question_error( - app, answer_store, list_store -): +def test_date_range_to_precedes_from_raises_question_error(app, data_stores): with app.test_request_context(): schema = load_schema_from_name("test_date_range") @@ -146,13 +139,10 @@ def test_date_range_to_precedes_from_raises_question_error( } form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata={}, - response_metadata={}, + schema=schema, + question_schema=question_schema, form_data=form_data, + data_stores=data_stores, ) form.validate() @@ -163,9 +153,7 @@ def test_date_range_to_precedes_from_raises_question_error( ) -def test_date_range_too_large_period_raises_question_error( - app, answer_store, list_store -): +def test_date_range_too_large_period_raises_question_error(app, data_stores): with app.test_request_context(): schema = load_schema_from_name("test_date_validation_range") @@ -187,13 +175,11 @@ def test_date_range_too_large_period_raises_question_error( "date-range-from": "2016-12-25", "date-range-to": "2017-12-24", } + form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata={}, - response_metadata={}, + schema=schema, + question_schema=question_schema, + data_stores=data_stores, form_data=form_data, ) @@ -204,9 +190,7 @@ def test_date_range_too_large_period_raises_question_error( ] % {"max": "1 month, 20 days"} -def test_date_range_too_small_period_raises_question_error( - app, answer_store, list_store -): +def test_date_range_too_small_period_raises_question_error(app, data_stores): with app.test_request_context(): schema = load_schema_from_name("test_date_validation_range") @@ -228,13 +212,11 @@ def test_date_range_too_small_period_raises_question_error( "date-range-from": "2016-12-25", "date-range-to": "2016-12-26", } + form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata={}, - response_metadata={}, + schema=schema, + question_schema=question_schema, + data_stores=data_stores, form_data=form_data, ) @@ -245,7 +227,7 @@ def test_date_range_too_small_period_raises_question_error( ] % {"min": "23 days"} -def test_date_range_valid_period(app, answer_store, list_store): +def test_date_range_valid_period(app, data_stores): with app.test_request_context(): schema = load_schema_from_name("test_date_validation_range") @@ -267,21 +249,19 @@ def test_date_range_valid_period(app, answer_store, list_store): "date-range-from": "2016-12-25", "date-range-to": "2017-01-26", } + form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata={}, - response_metadata={}, + schema=schema, + question_schema=question_schema, form_data=form_data, + data_stores=data_stores, ) form.validate() assert form.data == expected_form_data -def test_date_combined_single_validation(app, answer_store, list_store): +def test_date_combined_single_validation(app): with app.test_request_context(): schema = load_schema_from_name("test_date_validation_combined") @@ -298,26 +278,24 @@ def test_date_combined_single_validation(app, answer_store, list_store): } ) - metadata = { + test_metadata = { "ref_p_start_date": "2017-01-21", "ref_p_end_date": "2017-02-21", } - response_metadata = {} + metadata = get_metadata(extra_metadata=test_metadata) expected_form_data = { "csrf_token": None, "date-range-from": "2017-01-01", "date-range-to": "2017-03-14", } + form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata, - response_metadata, + schema=schema, + question_schema=question_schema, form_data=form_data, + data_stores=DataStores(metadata=metadata), ) form.validate() @@ -331,7 +309,7 @@ def test_date_combined_single_validation(app, answer_store, list_store): ] % {"max": "14 March 2017"} -def test_date_combined_range_too_small_validation(app, answer_store, list_store): +def test_date_combined_range_too_small_validation(app): with app.test_request_context(): schema = load_schema_from_name("test_date_validation_combined") @@ -348,23 +326,23 @@ def test_date_combined_range_too_small_validation(app, answer_store, list_store) } ) - metadata = { + test_metadata = { "ref_p_start_date": "2017-01-20", "ref_p_end_date": "2017-02-20", } + metadata = get_metadata(extra_metadata=test_metadata) + expected_form_data = { "csrf_token": None, "date-range-from": "2017-01-01", "date-range-to": "2017-01-10", } + form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata, - response_metadata={}, + schema=schema, + question_schema=question_schema, + data_stores=DataStores(metadata=metadata), form_data=form_data, ) @@ -375,7 +353,7 @@ def test_date_combined_range_too_small_validation(app, answer_store, list_store) ] % {"min": "10 days"} -def test_date_combined_range_too_large_validation(app, answer_store, list_store): +def test_date_combined_range_too_large_validation(app): with app.test_request_context(): schema = load_schema_from_name("test_date_validation_combined") @@ -392,25 +370,23 @@ def test_date_combined_range_too_large_validation(app, answer_store, list_store) } ) - metadata = { + test_metadata = { "ref_p_start_date": "2017-01-20", "ref_p_end_date": "2017-02-20", } - response_metadata = {} + metadata = get_metadata(extra_metadata=test_metadata) expected_form_data = { "csrf_token": None, "date-range-from": "2017-01-01", "date-range-to": "2017-02-21", } + form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata, - response_metadata, + schema=schema, + question_schema=question_schema, + data_stores=DataStores(metadata=metadata), form_data=form_data, ) @@ -421,7 +397,7 @@ def test_date_combined_range_too_large_validation(app, answer_store, list_store) ] % {"max": "50 days"} -def test_date_mm_yyyy_combined_single_validation(app, answer_store, list_store): +def test_date_mm_yyyy_combined_single_validation(app): with app.test_request_context(): schema = load_schema_from_name("test_date_validation_mm_yyyy_combined") @@ -436,25 +412,23 @@ def test_date_mm_yyyy_combined_single_validation(app, answer_store, list_store): } ) - metadata = { + test_metadata = { "ref_p_start_date": "2017-01-01", "ref_p_end_date": "2017-02-12", } - response_metadata = {} + metadata = get_metadata(extra_metadata=test_metadata) expected_form_data = { "csrf_token": None, "date-range-from": "2016-11", "date-range-to": "2017-06", } + form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata, - response_metadata, + schema=schema, + question_schema=question_schema, + data_stores=DataStores(metadata=metadata), form_data=form_data, ) @@ -469,9 +443,7 @@ def test_date_mm_yyyy_combined_single_validation(app, answer_store, list_store): ] % {"max": "June 2017"} -def test_date_mm_yyyy_combined_range_too_small_validation( - app, answer_store, list_store -): +def test_date_mm_yyyy_combined_range_too_small_validation(app): with app.test_request_context(): schema = load_schema_from_name("test_date_validation_mm_yyyy_combined") @@ -486,23 +458,23 @@ def test_date_mm_yyyy_combined_range_too_small_validation( } ) - metadata = { + test_metadata = { "ref_p_start_date": "2017-01-01", "ref_p_end_date": "2017-02-12", } + metadata = get_metadata(extra_metadata=test_metadata) + expected_form_data = { "csrf_token": None, "date-range-from": "2017-01", "date-range-to": "2017-02", } + form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata, - response_metadata={}, + schema=schema, + question_schema=question_schema, + data_stores=DataStores(metadata=metadata), form_data=form_data, ) @@ -513,9 +485,7 @@ def test_date_mm_yyyy_combined_range_too_small_validation( ] % {"min": "2 months"} -def test_date_mm_yyyy_combined_range_too_large_validation( - app, answer_store, list_store -): +def test_date_mm_yyyy_combined_range_too_large_validation(app): with app.test_request_context(): schema = load_schema_from_name("test_date_validation_mm_yyyy_combined") @@ -530,25 +500,23 @@ def test_date_mm_yyyy_combined_range_too_large_validation( } ) - metadata = { + test_metadata = { "ref_p_start_date": "2017-01-01", "ref_p_end_date": "2017-02-12", } - response_metadata = {} + metadata = get_metadata(extra_metadata=test_metadata) expected_form_data = { "csrf_token": None, "date-range-from": "2017-01", "date-range-to": "2017-05", } + form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata, - response_metadata, + schema=schema, + question_schema=question_schema, + data_stores=DataStores(metadata=metadata), form_data=form_data, ) @@ -559,7 +527,7 @@ def test_date_mm_yyyy_combined_range_too_large_validation( ] % {"max": "3 months"} -def test_date_yyyy_combined_single_validation(app, answer_store, list_store): +def test_date_yyyy_combined_single_validation(app): with app.test_request_context(): schema = load_schema_from_name("test_date_validation_yyyy_combined") @@ -569,23 +537,23 @@ def test_date_yyyy_combined_single_validation(app, answer_store, list_store): {"date-range-from-year": "2015", "date-range-to-year": "2021"} ) - metadata = { + test_metadata = { "ref_p_start_date": "2017-01-01", "ref_p_end_date": "2017-02-12", } + metadata = get_metadata(extra_metadata=test_metadata) + expected_form_data = { "csrf_token": None, "date-range-from": "2015", "date-range-to": "2021", } + form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata, - response_metadata={}, + schema=schema, + question_schema=question_schema, + data_stores=DataStores(metadata=metadata), form_data=form_data, ) @@ -600,7 +568,7 @@ def test_date_yyyy_combined_single_validation(app, answer_store, list_store): ] % {"max": "2021"} -def test_date_yyyy_combined_range_too_small_validation(app, answer_store, list_store): +def test_date_yyyy_combined_range_too_small_validation(app): with app.test_request_context(): schema = load_schema_from_name("test_date_validation_yyyy_combined") @@ -610,23 +578,23 @@ def test_date_yyyy_combined_range_too_small_validation(app, answer_store, list_s {"date-range-from-year": "2016", "date-range-to-year": "2017"} ) - metadata = { + test_metadata = { "ref_p_start_date": "2017-01-01", "ref_p_end_date": "2017-02-12", } + metadata = get_metadata(extra_metadata=test_metadata) + expected_form_data = { "csrf_token": None, "date-range-from": "2016", "date-range-to": "2017", } + form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata, - response_metadata={}, + schema=schema, + question_schema=question_schema, + data_stores=DataStores(metadata=metadata), form_data=form_data, ) @@ -637,7 +605,7 @@ def test_date_yyyy_combined_range_too_small_validation(app, answer_store, list_s ] % {"min": "2 years"} -def test_date_yyyy_combined_range_too_large_validation(app, answer_store, list_store): +def test_date_yyyy_combined_range_too_large_validation(app): with app.test_request_context(): schema = load_schema_from_name("test_date_validation_yyyy_combined") @@ -647,23 +615,23 @@ def test_date_yyyy_combined_range_too_large_validation(app, answer_store, list_s {"date-range-from-year": "2016", "date-range-to-year": "2020"} ) - metadata = { + test_metadata = { "ref_p_start_date": "2017-01-01", "ref_p_end_date": "2017-02-12", } + metadata = get_metadata(extra_metadata=test_metadata) + expected_form_data = { "csrf_token": None, "date-range-from": "2016", "date-range-to": "2020", } + form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata, - response_metadata={}, + schema=schema, + question_schema=question_schema, + data_stores=DataStores(metadata=metadata), form_data=form_data, ) @@ -674,9 +642,7 @@ def test_date_yyyy_combined_range_too_large_validation(app, answer_store, list_s ] % {"max": "3 years"} -def test_date_raises_ValueError_when_any_date_range_parts_are_falsy( - app, answer_store, list_store -): +def test_date_raises_ValueError_when_any_date_range_parts_are_falsy(app): with app.test_request_context(): schema = load_schema_from_name("test_date_validation_combined") @@ -693,17 +659,12 @@ def test_date_raises_ValueError_when_any_date_range_parts_are_falsy( } ) - metadata = {"ref_p_start_date": "2017-01-21"} - - response_metadata = {} + metadata = get_metadata() form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata, - response_metadata, + schema=schema, + question_schema=question_schema, + data_stores=DataStores(metadata=metadata), form_data=form_data, ) @@ -711,9 +672,7 @@ def test_date_raises_ValueError_when_any_date_range_parts_are_falsy( form.validate() -def test_bespoke_message_for_date_validation_range( - app, answer_store, list_store, mocker -): +def test_bespoke_message_for_date_validation_range(app, data_stores, mocker): with app.test_request_context(): schema = load_schema_from_name("test_date_validation_range") @@ -750,12 +709,9 @@ def test_bespoke_message_for_date_validation_range( ) form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata={}, - response_metadata={}, + schema=schema, + question_schema=question_schema, + data_stores=data_stores, form_data=form_data, ) @@ -767,9 +723,7 @@ def test_bespoke_message_for_date_validation_range( assert form.question_errors["date-range-question"] == "Test Message" -def test_invalid_minimum_period_limit_and_single_date_periods( - app, answer_store, list_store, mocker -): +def test_invalid_minimum_period_limit_and_single_date_periods(app, data_stores, mocker): with app.test_request_context(): schema = load_schema_from_name("test_date_validation_range") @@ -807,12 +761,9 @@ def test_invalid_minimum_period_limit_and_single_date_periods( ) form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata={}, - response_metadata={}, + schema=schema, + question_schema=question_schema, + data_stores=data_stores, form_data=form_data, ) @@ -827,9 +778,7 @@ def test_invalid_minimum_period_limit_and_single_date_periods( ) -def test_invalid_maximum_period_limit_and_single_date_periods( - app, answer_store, list_store, mocker -): +def test_invalid_maximum_period_limit_and_single_date_periods(app, data_stores, mocker): with app.test_request_context(): schema = load_schema_from_name("test_date_validation_range") @@ -867,12 +816,9 @@ def test_invalid_maximum_period_limit_and_single_date_periods( ) form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata={}, - response_metadata={}, + schema=schema, + question_schema=question_schema, + data_stores=data_stores, form_data=form_data, ) @@ -889,7 +835,7 @@ def test_invalid_maximum_period_limit_and_single_date_periods( def test_period_limits_minimum_not_set_and_single_date_periods( - app, answer_store, list_store, mocker + app, data_stores, mocker ): with app.test_request_context(): schema = load_schema_from_name("test_date_validation_range") @@ -928,12 +874,9 @@ def test_period_limits_minimum_not_set_and_single_date_periods( ) form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata={}, - response_metadata={}, + schema=schema, + question_schema=question_schema, + data_stores=data_stores, form_data=form_data, ) @@ -946,9 +889,7 @@ def test_period_limits_minimum_not_set_and_single_date_periods( assert len(form.question_errors) == 0 -def test_invalid_date_range_and_single_date_periods( - app, answer_store, list_store, mocker -): +def test_invalid_date_range_and_single_date_periods(app, answer_store, mocker): with app.test_request_context(): test_answer_id = Answer(answer_id="date", value="2017-03-20") answer_store.add_or_update(test_answer_id) @@ -993,12 +934,9 @@ def test_invalid_date_range_and_single_date_periods( metadata = {"schema_name": "test_date_validation_range"} form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata=metadata, - response_metadata={}, + schema=schema, + question_schema=question_schema, + data_stores=DataStores(answer_store=answer_store, metadata=metadata), form_data=form_data, ) @@ -1014,7 +952,7 @@ def test_invalid_date_range_and_single_date_periods( ) -def test_invalid_calculation_type(app, answer_store, list_store, mocker): +def test_invalid_calculation_type(app, answer_store, mocker): answer_total = Answer(answer_id="total-answer", value=10) answer_store.add_or_update(answer_total) @@ -1038,12 +976,9 @@ def test_invalid_calculation_type(app, answer_store, list_store, mocker): form_data = MultiDict({"breakdown-1": "3", "breakdown-2": "5"}) form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata={}, - response_metadata={}, + schema=schema, + question_schema=question_schema, + data_stores=DataStores(answer_store=answer_store), form_data=form_data, ) @@ -1056,8 +991,7 @@ def test_invalid_calculation_type(app, answer_store, list_store, mocker): assert "Invalid calculation_type: subtraction" == str(exc.value) -def test_bespoke_message_for_sum_validation(app, answer_store, list_store, mocker): - +def test_bespoke_message_for_sum_validation(app, answer_store, mocker): answer_total = Answer(answer_id="total-answer", value=10) answer_store.add_or_update(answer_total) @@ -1076,12 +1010,9 @@ def test_bespoke_message_for_sum_validation(app, answer_store, list_store, mocke form_data = MultiDict({"breakdown-1": "3", "breakdown-2": "5"}) form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata={}, - response_metadata={}, + schema=schema, + question_schema=question_schema, + data_stores=DataStores(answer_store=answer_store), form_data=form_data, ) @@ -1094,135 +1025,259 @@ def test_bespoke_message_for_sum_validation(app, answer_store, list_store, mocke assert form.question_errors["breakdown-question"] == "Test Message" -def test_empty_calculated_field(app, answer_store, list_store): - - answer_total = Answer(answer_id="total-answer", value=10) - - answer_store.add_or_update(answer_total) - - with app.test_request_context(): - schema = load_schema_from_name("test_validation_sum_against_total_equal") - - question_schema = schema.get_block("breakdown-block").get("question") - - form_data = MultiDict( +@pytest.mark.parametrize( + "schema_name, block, answers, breakdowns, expected_form_data, question, errors_text, value_dict", + [ + [ + "test_validation_sum_against_total_equal", + "breakdown-block", + [Answer(answer_id="total-answer", value=Decimal("10.00"))], { "breakdown-1": "", "breakdown-2": "5", "breakdown-3": "4", "breakdown-4": "", - } - ) - - expected_form_data = { - "csrf_token": None, - "breakdown-1": None, - "breakdown-2": Decimal("5"), - "breakdown-3": Decimal("4"), - "breakdown-4": None, - } - form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata={}, - response_metadata={}, - form_data=form_data, - ) - - form.validate() - assert form.data == expected_form_data - assert form.question_errors["breakdown-question"] == schema.error_messages[ - "TOTAL_SUM_NOT_EQUALS" - ] % {"total": "10"} - - -def test_sum_calculated_field(app, answer_store, list_store): - answer_total = Answer(answer_id="total-answer", value=10) - - answer_store.add_or_update(answer_total) - - with app.test_request_context(): - schema = load_schema_from_name("test_validation_sum_against_total_equal") - - question_schema = schema.get_block("breakdown-block").get("question") - - form_data = MultiDict( + }, + { + "csrf_token": None, + "breakdown-1": None, + "breakdown-2": Decimal("5"), + "breakdown-3": Decimal("4"), + "breakdown-4": None, + }, + "breakdown-question", + ["TOTAL_SUM_NOT_EQUALS"], + {"total": "10.00"}, + ], + [ + "test_validation_sum_against_total_equal", + "breakdown-block", + [Answer(answer_id="total-answer", value=10)], { "breakdown-1": "", "breakdown-2": "5", "breakdown-3": "4", "breakdown-4": "1", - } + }, + { + "csrf_token": None, + "breakdown-1": None, + "breakdown-2": Decimal("5"), + "breakdown-3": Decimal("4"), + "breakdown-4": Decimal("1"), + }, + "breakdown-question", + None, + None, + ], + [ + "test_validation_sum_against_total_equal", + "breakdown-block", + [Answer(answer_id="total-answer", value=10)], + { + "breakdown-1": "", + "breakdown-2": "", + "breakdown-3": "", + "breakdown-4": "", + }, + { + "csrf_token": None, + "breakdown-1": None, + "breakdown-2": None, + "breakdown-3": None, + "breakdown-4": None, + }, + "breakdown-question", + ["TOTAL_SUM_NOT_EQUALS"], + {"total": "10"}, + ], + [ + "test_validation_sum_against_value_source", + "breakdown-block", + [Answer(answer_id="total-answer", value=10)], + { + "breakdown-1": "", + "breakdown-2": "5", + "breakdown-3": "4", + "breakdown-4": "1", + }, + { + "csrf_token": None, + "breakdown-1": None, + "breakdown-2": Decimal("5"), + "breakdown-3": Decimal("4"), + "breakdown-4": Decimal("1"), + }, + "breakdown-question", + None, + None, + ], + [ + "test_validation_sum_against_value_source", + "second-breakdown-block", + [ + Answer(answer_id="breakdown-1", value=5), + Answer(answer_id="breakdown-2", value=5), + ], + { + "second-breakdown-1": "", + "second-breakdown-2": "5", + "second-breakdown-3": "4", + "second-breakdown-4": "1", + }, + { + "csrf_token": None, + "second-breakdown-1": None, + "second-breakdown-2": Decimal("5"), + "second-breakdown-3": Decimal("4"), + "second-breakdown-4": Decimal("1"), + }, + "second-breakdown-question", + None, + None, + ], + [ + "test_validation_sum_against_value_source", + "breakdown-block", + [Answer(answer_id="total-answer", value=10)], + { + "breakdown-1": "", + "breakdown-2": "", + "breakdown-3": "4", + "breakdown-4": "1", + }, + { + "csrf_token": None, + "breakdown-1": None, + "breakdown-2": None, + "breakdown-3": Decimal("4"), + "breakdown-4": Decimal("1"), + }, + "breakdown-question", + ["TOTAL_SUM_NOT_EQUALS"], + {"total": "10"}, + ], + [ + "test_validation_sum_against_value_source", + "second-breakdown-block", + [ + Answer(answer_id="breakdown-1", value=5), + Answer(answer_id="breakdown-2", value=5), + ], + { + "second-breakdown-1": "", + "second-breakdown-2": "", + "second-breakdown-3": "4", + "second-breakdown-4": "1", + }, + { + "csrf_token": None, + "second-breakdown-1": None, + "second-breakdown-2": None, + "second-breakdown-3": Decimal("4"), + "second-breakdown-4": Decimal("1"), + }, + "second-breakdown-question", + ["TOTAL_SUM_NOT_EQUALS"], + {"total": "10"}, + ], + ], +) +def test_calculated_field( + app, + answer_store, + schema_name, + block, + answers, + breakdowns, + expected_form_data, + question, + errors_text, + value_dict, +): # pylint: disable=too-many-locals + for answer in answers: + answer_store.add_or_update(answer) + + with app.test_request_context(): + schema = load_schema_from_name(schema_name) + + question_schema = schema.get_block(block).get("question") + + location = Location( + section_id="default-section", + block_id=block, + list_item_id=None, ) - expected_form_data = { - "csrf_token": None, - "breakdown-1": None, - "breakdown-2": Decimal("5"), - "breakdown-3": Decimal("4"), - "breakdown-4": Decimal("1"), - } + form_data = MultiDict(breakdowns) + form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata={}, - response_metadata={}, + schema=schema, + question_schema=question_schema, + data_stores=DataStores(answer_store=answer_store), + location=location, form_data=form_data, ) form.validate() assert form.data == expected_form_data - - -def test_get_calculation_total_with_no_input(app, answer_store, list_store): - answer_total = Answer(answer_id="total-answer", value=10) - - answer_store.add_or_update(answer_total) + if errors_text: + for error_text in errors_text: + assert ( + form.question_errors[question] + == schema.error_messages[error_text] % value_dict + ) + else: + assert len(form.question_errors) == 0 + + +def test_sum_calculated_field_value_source_calculated_summary_repeat_not_equal_validation_error( + app, answer_store, mocker +): + list_store = ListStore([{"name": "people", "items": ["lCIZsS"]}]) + answer_store.add_or_update( + Answer( + answer_id="entertainment-spending-answer", value=10, list_item_id="lCIZsS" + ) + ) + mocker.patch( + "app.questionnaire.value_source_resolver.ValueSourceResolver._resolve_list_item_id_for_value_source", + return_value="lCIZsS", + ) with app.test_request_context(): - schema = load_schema_from_name("test_validation_sum_against_total_equal") + schema = load_schema_from_name( + "test_validation_sum_against_total_repeating_with_dependent_section" + ) - question_schema = schema.get_block("breakdown-block").get("question") + question_schema = schema.get_block("second-spending-breakdown-block").get( + "question" + ) form_data = MultiDict( { - "breakdown-1": "", - "breakdown-2": "", - "breakdown-3": "", - "breakdown-4": "", + "second-spending-breakdown-1": "", + "second-spending-breakdown-2": "", + "second-spending-breakdown-3": "4", + "second-spending-breakdown-4": "1", } ) - expected_form_data = { - "csrf_token": None, - "breakdown-1": None, - "breakdown-2": None, - "breakdown-3": None, - "breakdown-4": None, - } form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata={}, - response_metadata={}, + schema=schema, + question_schema=question_schema, + data_stores=DataStores(answer_store=answer_store, list_store=list_store), form_data=form_data, ) form.validate() - assert form.data == expected_form_data - assert form.question_errors["breakdown-question"] == schema.error_messages[ - "TOTAL_SUM_NOT_EQUALS" - ] % {"total": "10"} + assert form.question_errors[ + "second-spending-breakdown-question" + ] == schema.error_messages["TOTAL_SUM_NOT_EQUALS"] % {"total": "10"} -def test_multi_calculation(app, answer_store, list_store): - answer_total = Answer(answer_id="total-answer", value=10) +def test_multi_calculation(app, answer_store): + answer_total = Answer(answer_id="total-answer", value=Decimal("10.00")) answer_store.add_or_update(answer_total) @@ -1240,14 +1295,13 @@ def test_multi_calculation(app, answer_store, list_store): } ) + data_stores = DataStores(answer_store=answer_store) + # With no answers question validation should pass form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata={}, - response_metadata={}, + schema=schema, + question_schema=question_schema, + data_stores=data_stores, form_data=form_data, ) form.validate() @@ -1258,12 +1312,9 @@ def test_multi_calculation(app, answer_store, list_store): form_data["breakdown-1"] = "10" form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata={}, - response_metadata={}, + schema=schema, + question_schema=question_schema, + data_stores=data_stores, form_data=form_data, ) form.validate() @@ -1274,22 +1325,19 @@ def test_multi_calculation(app, answer_store, list_store): form_data["breakdown-1"] = "1" form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata={}, - response_metadata={}, + schema=schema, + question_schema=question_schema, + data_stores=data_stores, form_data=form_data, ) form.validate() assert form.question_errors["breakdown-question"] == schema.error_messages[ "TOTAL_SUM_NOT_EQUALS" - ] % {"total": "10"} + ] % {"total": "10.00"} -def test_generate_form_with_title_and_no_answer_label(app, answer_store, list_store): +def test_generate_form_with_title_and_no_answer_label(app, answer_store): """ Checks that the form is still generated when there is no answer label but there is a question title """ @@ -1306,34 +1354,27 @@ def test_generate_form_with_title_and_no_answer_label(app, answer_store, list_st expected_form_data = {"csrf_token": None, "feeling-answer": "Good"} - with patch("app.questionnaire.path_finder.evaluate_goto", return_value=False): - form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata={}, - response_metadata={}, - form_data=form_data, - ) + form = generate_form( + schema=schema, + question_schema=question_schema, + data_stores=DataStores(answer_store=answer_store), + form_data=form_data, + ) form.validate() assert form.data == expected_form_data -def test_form_errors_are_correctly_mapped(app, answer_store, list_store): +def test_form_errors_are_correctly_mapped(app, data_stores): with app.test_request_context(): schema = load_schema_from_name("test_numbers") question_schema = schema.get_block("set-min-max-block").get("question") form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata={}, - response_metadata={}, + schema=schema, + question_schema=question_schema, + data_stores=data_stores, ) form.validate() @@ -1344,19 +1385,16 @@ def test_form_errors_are_correctly_mapped(app, answer_store, list_store): ) -def test_form_subfield_errors_are_correctly_mapped(app, answer_store, list_store): +def test_form_subfield_errors_are_correctly_mapped(app, data_stores): with app.test_request_context(): schema = load_schema_from_name("test_date_range") question_schema = schema.get_block("date-block").get("question") form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata={}, - response_metadata={}, + schema=schema, + question_schema=question_schema, + data_stores=data_stores, ) form.validate() @@ -1375,9 +1413,7 @@ def test_form_subfield_errors_are_correctly_mapped(app, answer_store, list_store ) -def test_detail_answer_mandatory_only_checked_if_option_selected( - app, answer_store, list_store -): +def test_detail_answer_mandatory_only_checked_if_option_selected(app, data_stores): # The detail_answer can only be mandatory if the option it is associated with is answered with app.test_request_context(): schema = load_schema_from_name("test_checkbox_detail_answer_multiple") @@ -1386,12 +1422,9 @@ def test_detail_answer_mandatory_only_checked_if_option_selected( # Option is selected therefore the detail answer should be mandatory (schema defined) form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata={}, - response_metadata={}, + schema=schema, + question_schema=question_schema, + data_stores=data_stores, form_data=MultiDict({"mandatory-checkbox-answer": "Your choice"}), ) @@ -1400,12 +1433,9 @@ def test_detail_answer_mandatory_only_checked_if_option_selected( # Option not selected therefore the detail answer should not be mandatory form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata={}, - response_metadata={}, + schema=schema, + question_schema=question_schema, + data_stores=data_stores, data={"mandatory-checkbox-answer": "Ham"}, ) @@ -1413,9 +1443,7 @@ def test_detail_answer_mandatory_only_checked_if_option_selected( assert detail_answer_field.validators == () -def test_answer_with_detail_answer_errors_are_correctly_mapped( - app, answer_store, list_store -): +def test_answer_with_detail_answer_errors_are_correctly_mapped(app, data_stores): with app.test_request_context(): schema = load_schema_from_name( "test_radio_mandatory_with_detail_answer_mandatory" @@ -1424,12 +1452,9 @@ def test_answer_with_detail_answer_errors_are_correctly_mapped( question_schema = schema.get_block("radio-mandatory").get("question") form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata={}, - response_metadata={}, + schema=schema, + question_schema=question_schema, + data_stores=data_stores, form_data=MultiDict({"radio-mandatory-answer": "Other"}), ) @@ -1448,29 +1473,29 @@ def test_answer_with_detail_answer_errors_are_correctly_mapped( ) -def test_answer_errors_are_interpolated(app, answer_store, list_store): +def test_answer_errors_are_interpolated(app, data_stores): with app.test_request_context(): schema = load_schema_from_name("test_numbers") question_schema = schema.get_block("set-min-max-block").get("question") form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata={}, - response_metadata={}, - form_data=MultiDict({"set-minimum": "-1"}), + schema=schema, + question_schema=question_schema, + data_stores=data_stores, + form_data=MultiDict({"set-minimum": "-10001"}), ) form.validate() answer_errors = form.answer_errors("set-minimum") - assert schema.error_messages["NUMBER_TOO_SMALL"] % {"min": "0"} in answer_errors + assert ( + schema.error_messages["NUMBER_TOO_SMALL"] % {"min": "-1,000.98"} + in answer_errors + ) def test_mandatory_mutually_exclusive_question_raises_error_when_not_answered( - app, answer_store, list_store + app, data_stores ): with app.test_request_context(): schema = load_schema_from_name("test_mutually_exclusive") @@ -1480,12 +1505,9 @@ def test_mandatory_mutually_exclusive_question_raises_error_when_not_answered( ) form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata={}, - response_metadata={}, + schema=schema, + question_schema=question_schema, + data_stores=data_stores, form_data=MultiDict(), ) form.validate_mutually_exclusive_question(question_schema) @@ -1497,9 +1519,7 @@ def test_mandatory_mutually_exclusive_question_raises_error_when_not_answered( ) -def test_mandatory_mutually_exclusive_question_raises_error_with_question_text( - app, list_store -): +def test_mandatory_mutually_exclusive_question_raises_error_with_question_text(app): with app.test_request_context(): schema = load_schema_from_name("test_question_title_in_error") @@ -1512,21 +1532,18 @@ def test_mandatory_mutually_exclusive_question_raises_error_with_question_text( renderer = PlaceholderRenderer( language="en", - answer_store=answer_store, - list_store=list_store, - metadata={}, - response_metadata={}, + data_stores=DataStores(answer_store=answer_store), schema=schema, + location=Location(section_id="mutually-exclusive-checkbox-section"), + ) + rendered_schema = renderer.render( + data_to_render=question_schema, list_item_id=None ) - rendered_schema = renderer.render(question_schema, None) form = generate_form( - schema, - rendered_schema, - answer_store, - list_store, - metadata={}, - response_metadata={}, + schema=schema, + question_schema=rendered_schema, + data_stores=DataStores(answer_store=answer_store), form_data=MultiDict(), ) form.validate_mutually_exclusive_question(question_schema) @@ -1538,9 +1555,7 @@ def test_mandatory_mutually_exclusive_question_raises_error_with_question_text( ) -def test_mutually_exclusive_question_raises_error_when_both_answered( - app, answer_store, list_store -): +def test_mutually_exclusive_question_raises_error_when_both_answered(app, data_stores): with app.test_request_context(): schema = load_schema_from_name("test_mutually_exclusive") @@ -1556,12 +1571,9 @@ def test_mutually_exclusive_question_raises_error_when_both_answered( ) form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata={}, - response_metadata={}, + schema=schema, + question_schema=question_schema, + data_stores=data_stores, form_data=form_data, ) form.validate_mutually_exclusive_question(question_schema) @@ -1572,18 +1584,15 @@ def test_mutually_exclusive_question_raises_error_when_both_answered( ) -def test_date_range_form(app, answer_store, list_store): +def test_date_range_form(app, data_stores): with app.test_request_context(): schema = load_schema_from_name("test_date_range") question_schema = schema.get_block("date-block").get("question") form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata={}, - response_metadata={}, + schema=schema, + question_schema=question_schema, + data_stores=data_stores, ) assert hasattr(form, "date-range-from-answer") @@ -1596,7 +1605,7 @@ def test_date_range_form(app, answer_store, list_store): assert isinstance(period_to_field.year.validators[0], DateRequired) -def test_date_range_form_with_data(app, answer_store, list_store): +def test_date_range_form_with_data(app, data_stores): with app.test_request_context(): schema = load_schema_from_name("test_date_range") question_schema = schema.get_block("date-block").get("question") @@ -1613,12 +1622,9 @@ def test_date_range_form_with_data(app, answer_store, list_store): ) form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata={}, - response_metadata={}, + schema=schema, + question_schema=question_schema, + data_stores=data_stores, form_data=form_data, ) @@ -1635,7 +1641,7 @@ def test_date_range_form_with_data(app, answer_store, list_store): assert period_to_field.data == "2017-09-01" -def test_form_for_radio_other_not_selected(app, answer_store, list_store): +def test_form_for_radio_other_not_selected(app, data_stores): with app.test_request_context(): schema = load_schema_from_name( "test_radio_mandatory_with_detail_answer_mandatory" @@ -1651,12 +1657,9 @@ def test_form_for_radio_other_not_selected(app, answer_store, list_store): ) form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata={}, - response_metadata={}, + schema=schema, + question_schema=question_schema, + data_stores=data_stores, form_data=form_data, ) @@ -1665,7 +1668,7 @@ def test_form_for_radio_other_not_selected(app, answer_store, list_store): assert other_text_field.data == "" -def test_form_for_radio_other_selected(app, answer_store, list_store): +def test_form_for_radio_other_selected(app, data_stores): with app.test_request_context(): schema = load_schema_from_name( "test_radio_mandatory_with_detail_answer_mandatory" @@ -1679,15 +1682,117 @@ def test_form_for_radio_other_selected(app, answer_store, list_store): "other-answer-mandatory": "Other text field value", } ) + form = generate_form( - schema, - question_schema, - answer_store, - list_store, - metadata={}, - response_metadata={}, + schema=schema, + question_schema=question_schema, + data_stores=data_stores, form_data=form_data, ) other_text_field = getattr(form, "other-answer-mandatory") assert other_text_field.data == "Other text field value" + + +def test_dynamic_answers_question_validates(app): + with app.test_request_context(): + schema = load_schema_from_name( + "test_validation_sum_against_total_dynamic_answers" + ) + + question_schema = schema.get_block("dynamic-answer").get("question") + question_schema = QuestionnaireSchema.get_mutable_deepcopy(question_schema) + question_schema["answers"].append( + { + "label": "Percentage of shopping at Tesco", + "id": "percentage-of-shopping-lCIZsS", + "mandatory": False, + "type": "Percentage", + "maximum": {"value": 100}, + "decimal_places": 0, + "original_answer_id": "percentage-of-shopping", + "list_item_id": "lCIZsS", + } + ) + + form_data = MultiDict( + { + "percentage-of-shopping-lCIZsS": "25", + "percentage-of-shopping-elsewhere": "75", + } + ) + + expected_form_data = { + "csrf_token": None, + "percentage-of-shopping-lCIZsS": "25", + "percentage-of-shopping-elsewhere": "75", + } + + form = generate_form( + schema=schema, + question_schema=question_schema, + data_stores=DataStores( + answer_store=AnswerStore([{"answer_id": "total-answer", "value": 100}]), + list_store=ListStore([{"name": "supermarkets", "items": ["lCIZsS"]}]), + ), + location=Location( + section_id="section", + block_id="dynamic-answer", + list_name=None, + list_item_id=None, + ), + form_data=form_data, + ) + + form.validate() + assert form.data, expected_form_data + + +def test_dynamic_answers_question_raises_validation_error(app): + with app.test_request_context(): + schema = load_schema_from_name( + "test_validation_sum_against_total_dynamic_answers" + ) + + question_schema = schema.get_block("dynamic-answer").get("question") + question_schema = QuestionnaireSchema.get_mutable_deepcopy(question_schema) + question_schema["answers"].append( + { + "label": "Percentage of shopping at Tesco", + "id": "percentage-of-shopping-lCIZsS", + "mandatory": False, + "type": "Percentage", + "maximum": {"value": 100}, + "decimal_places": 0, + "original_answer_id": "percentage-of-shopping", + "list_item_id": "lCIZsS", + } + ) + + form_data = MultiDict( + { + "percentage-of-shopping-lCIZsS": "25", + "percentage-of-shopping-elsewhere": "70", + } + ) + + form = generate_form( + schema=schema, + question_schema=question_schema, + data_stores=DataStores( + answer_store=AnswerStore([{"answer_id": "total-answer", "value": 100}]), + list_store=ListStore([{"name": "supermarkets", "items": ["lCIZsS"]}]), + ), + form_data=form_data, + location=Location( + section_id="section", + block_id="dynamic-answer", + list_name=None, + list_item_id=None, + ), + ) + + form.validate() + assert form.question_errors["dynamic-answer-question"] == schema.error_messages[ + "TOTAL_SUM_NOT_EQUALS" + ] % {"total": "100"} diff --git a/tests/app/forms/validation/test_date_check_validator.py b/tests/app/forms/validation/test_date_check_validator.py index 55e6100558..3d7e82d2e0 100644 --- a/tests/app/forms/validation/test_date_check_validator.py +++ b/tests/app/forms/validation/test_date_check_validator.py @@ -7,18 +7,16 @@ @pytest.mark.parametrize( "date", [ - "", "2016-12-", - "2016--03", - "-12-03", "2016-12-40", "2016-13-20", - "20000-12-20", "2015-02-29", # 2015 was not a leap year + "20000-12-20", ], ) def test_invalid_day_month_year(date_check, mock_form, mock_field, date): mock_form.data = date + mock_form["year"].data = date.split("-")[0] assert_invalid_date_error(date_check, mock_form, mock_field) @@ -27,22 +25,34 @@ def test_invalid_day_month_year(date_check, mock_form, mock_field, date): "date", [ "2016-", - "-12", "2016-13", + "2016--03", "20000-12", ], ) def test_invalid_month_year(date_check, mock_form, mock_field, date): mock_form.data = date + mock_form["year"].data = date.split("-")[0] del mock_form.day assert_invalid_date_error(date_check, mock_form, mock_field) -def test_invalid_year(date_check, mock_form, mock_field): +@pytest.mark.parametrize( + "date", + ["", "-12-03", "-12", "200", "20", "2"], +) +def test_invalid_year(date_check, mock_form, mock_field, date): + mock_form["year"].data = date.split("-")[0] + + assert_invalid_year_format_error(date_check, mock_form, mock_field) + + +def test_invalid_data(date_check, mock_form, mock_field): del mock_form.day del mock_form.month - mock_form.data = "20000" + del mock_form.year + mock_form.data = "abc" assert_invalid_date_error(date_check, mock_form, mock_field) @@ -56,7 +66,7 @@ def test_invalid_year(date_check, mock_form, mock_field): ) def test_valid_day_month_year(date_check, mock_form, mock_field, date): mock_form.data = date - + mock_form["year"].data = date.split("-")[0] try: date_check(mock_form, mock_field) except StopValidation: @@ -66,6 +76,7 @@ def test_valid_day_month_year(date_check, mock_form, mock_field, date): def test_valid_month_year(date_check, mock_form, mock_field): del mock_form.day mock_form.data = "2016-12" + mock_form["year"].data = "2016" try: date_check(mock_form, mock_field) @@ -77,6 +88,7 @@ def test_valid_year(date_check, mock_form, mock_field): del mock_form.day del mock_form.month mock_form.data = "2016" + mock_form["year"].data = "2016" try: date_check(mock_form, mock_field) @@ -88,3 +100,9 @@ def assert_invalid_date_error(date_check, mock_form, mock_field): with pytest.raises(StopValidation) as ite: date_check(mock_form, mock_field) assert error_messages["INVALID_DATE"] == str(ite.value) + + +def assert_invalid_year_format_error(date_check, mock_form, mock_field): + with pytest.raises(StopValidation) as ite: + date_check(mock_form, mock_field) + assert error_messages["INVALID_YEAR_FORMAT"] == str(ite.value) diff --git a/tests/app/forms/validation/test_mutually_exclusive_validator.py b/tests/app/forms/validation/test_mutually_exclusive_validator.py index f0dba36152..322fec5e16 100644 --- a/tests/app/forms/validation/test_mutually_exclusive_validator.py +++ b/tests/app/forms/validation/test_mutually_exclusive_validator.py @@ -1,3 +1,5 @@ +from decimal import Decimal + import pytest from wtforms.validators import ValidationError @@ -6,7 +8,7 @@ @pytest.mark.parametrize( - "answer_permutations, is_mandatory, is_only_checkboxes, error_type", + "answer_permutations, is_mandatory, is_only_checkboxes_or_radios, error_type", ( ([[], []], True, True, "MANDATORY_CHECKBOX"), ([None, []], True, True, "MANDATORY_CHECKBOX"), @@ -22,17 +24,32 @@ ), (["123", ["I prefer not to say"]], True, True, "MUTUALLY_EXCLUSIVE"), (["2018-09-01", ["I prefer not to say"]], True, True, "MUTUALLY_EXCLUSIVE"), + (["", None], True, True, "MANDATORY_CHECKBOX"), + ( + [["British, Irish"], "I prefer not to say"], + True, + True, + "MUTUALLY_EXCLUSIVE", + ), + ( + ["British, Irish", "I prefer not to say"], + True, + True, + "MUTUALLY_EXCLUSIVE", + ), + (["123", "I prefer not to say"], True, True, "MUTUALLY_EXCLUSIVE"), + (["2018-09-01", "I prefer not to say"], True, True, "MUTUALLY_EXCLUSIVE"), ), ) -def test_mutually_exclusive_mandatory_checkbox_raises_ValidationError( - answer_permutations, is_mandatory, is_only_checkboxes, error_type +def test_mutually_exclusive_mandatory_answers_raise_validation_error( + answer_permutations, is_mandatory, is_only_checkboxes_or_radios, error_type ): validator = MutuallyExclusiveCheck(question_title="") with pytest.raises(ValidationError) as exc: validator( answer_values=iter(answer_permutations), is_mandatory=is_mandatory, - is_only_checkboxes=is_only_checkboxes, + is_only_checkboxes_or_radios=is_only_checkboxes_or_radios, ) assert format_message_with_title(error_messages[error_type], "") == str(exc.value) @@ -50,12 +67,21 @@ def test_mutually_exclusive_mandatory_checkbox_raises_ValidationError( ([[], []], False), ([None, []], False), (["", []], False), + (["", None], False), + ([None, None], False), + ([None, ""], False), + ([["British, Irish"], None], True), + (["British, Irish", None], True), + ([None, "I prefer not to say"], True), + (["", "I prefer not to say"], True), + ([0, []], True), + ([Decimal(0), []], True), ), ) -def test_mutually_exclusive_mandatory_checkbox(answer_permutations, is_mandatory): +def test_mutually_exclusive_mandatory_answers(answer_permutations, is_mandatory): validator = MutuallyExclusiveCheck(question_title="") validator( answer_values=iter(answer_permutations), is_mandatory=is_mandatory, - is_only_checkboxes=True, + is_only_checkboxes_or_radios=True, ) diff --git a/tests/app/forms/validation/test_number_validator.py b/tests/app/forms/validation/test_number_validator.py index 81d45fddde..ddc61ecf14 100644 --- a/tests/app/forms/validation/test_number_validator.py +++ b/tests/app/forms/validation/test_number_validator.py @@ -1,3 +1,5 @@ +from decimal import Decimal + import pytest from wtforms.validators import StopValidation, ValidationError @@ -7,12 +9,7 @@ @pytest.mark.parametrize( "value", - ( - [None], - [""], - ["a"], - ["2E2"], - ), + ([None], [""], ["a"], ["2E2"], ["NaN"], [",NaN_"]), ) @pytest.mark.usefixtures("gb_locale") def test_number_validator_raises_StopValidation( @@ -49,12 +46,13 @@ def test_decimal_validator_raises_StopValidation( @pytest.mark.parametrize( "value", ( - ["0"], - ["10"], - ["-10"], + "0", + "10", + "-10", ), ) @pytest.mark.usefixtures("gb_locale") def test_number_validator(number_check, value, mock_form, mock_field): - mock_field.raw_data = value + mock_field.raw_data = [value] + mock_field.data = Decimal(value) number_check(mock_form, mock_field) diff --git a/tests/app/forms/validation/test_single_date_range_validator.py b/tests/app/forms/validation/test_single_date_range_validator.py index 530a53db92..a6a56f173a 100644 --- a/tests/app/forms/validation/test_single_date_range_validator.py +++ b/tests/app/forms/validation/test_single_date_range_validator.py @@ -27,7 +27,6 @@ def test_single_date_period_invalid_raises_ValidationError( validator, data, error_type, error_message, mock_form, mock_field ): - mock_form.data = data with pytest.raises(ValidationError) as exc: diff --git a/tests/app/forms/validation/test_sum_check_validator.py b/tests/app/forms/validation/test_sum_check_validator.py index 7ea885ff40..d1bbc185ba 100644 --- a/tests/app/forms/validation/test_sum_check_validator.py +++ b/tests/app/forms/validation/test_sum_check_validator.py @@ -45,13 +45,11 @@ def test_currency_playback(mock_form): with pytest.raises(ValidationError) as exc: validator(mock_form, conditions, calculation_total, target_total) - assert ( - error_messages["TOTAL_SUM_NOT_EQUALS"] - % { - "total": format_playback_value(target_total, currency="EUR"), - } - == str(exc.value) - ) + assert error_messages["TOTAL_SUM_NOT_EQUALS"] % { + "total": format_playback_value( + value=target_total, currency="EUR", decimal_limit=1 + ), + } == str(exc.value) @pytest.mark.usefixtures("gb_locale") @@ -84,3 +82,11 @@ def test_invalid_multiple_conditions(mock_form): "There are multiple conditions, but equals is not one of them. We only support <= and >=" == str(exc.value) ) + + +# pylint: disable=protected-access +def test_is_valid_raises_NotImplementedError(): + condition = "invalid_condition" + total, target_total = 10.5, 10.5 + with pytest.raises(NotImplementedError): + SumCheck._is_valid(condition, total, target_total) diff --git a/tests/app/helpers/conftest.py b/tests/app/helpers/conftest.py index 3c0c9ab8fb..e07f4d62c8 100644 --- a/tests/app/helpers/conftest.py +++ b/tests/app/helpers/conftest.py @@ -1,6 +1,12 @@ from pytest import fixture from app.helpers.template_helpers import ContextHelper +from app.settings import ( + ACCOUNT_SERVICE_BASE_URL, + ACCOUNT_SERVICE_BASE_URL_SOCIAL, + ONS_URL, + ONS_URL_CY, +) @fixture @@ -19,8 +25,7 @@ def _context_helper( return _context_helper -@fixture(name="footer_context") -def footer(): +def footer_context(): return { "lang": "en", "crest": True, @@ -32,30 +37,24 @@ def footer(): } -@fixture -def expected_footer_census_theme(footer_context): - census = { +def expected_footer_business_theme(): + business = { "rows": [ { "itemsList": [ { - "text": "Help", - "url": "https://census.gov.uk/help/how-to-answer-questions/online-questions-help/", + "text": "What we do", + "url": "https://www.ons.gov.uk/aboutus/whatwedo/", "target": "_blank", }, { "text": "Contact us", - "url": "https://census.gov.uk/contact-us/", + "url": f"{ACCOUNT_SERVICE_BASE_URL}/contact-us/", "target": "_blank", }, { - "text": "Languages", - "url": "https://census.gov.uk/help/languages-and-accessibility/languages/", - "target": "_blank", - }, - { - "text": "BSL and audio videos", - "url": "https://census.gov.uk/help/languages-and-accessibility/accessibility/accessible-videos-with-bsl/", + "text": "Accessibility", + "url": "https://www.ons.gov.uk/help/accessibility/", "target": "_blank", }, ] @@ -66,33 +65,22 @@ def expected_footer_census_theme(footer_context): "itemsList": [ { "text": "Cookies", - "url": "https://census.gov.uk/cookies/", - "target": "_blank", - }, - { - "text": "Accessibility statement", - "url": "https://census.gov.uk/accessibility-statement/", + "url": f"{ACCOUNT_SERVICE_BASE_URL}/cookies/", "target": "_blank", }, { "text": "Privacy and data protection", - "url": "https://census.gov.uk/privacy-and-data-protection/", - "target": "_blank", - }, - { - "text": "Terms and conditions", - "url": "https://census.gov.uk/terms-and-conditions/", + "url": f"{ACCOUNT_SERVICE_BASE_URL}/privacy-and-data-protection/", "target": "_blank", }, ] } ], } - return {**footer_context, **census} + return {**footer_context(), **business} -@fixture -def expected_footer_business_theme(footer_context): +def expected_footer_business_theme_no_cookie(): business = { "rows": [ { @@ -102,11 +90,6 @@ def expected_footer_business_theme(footer_context): "url": "https://www.ons.gov.uk/aboutus/whatwedo/", "target": "_blank", }, - { - "text": "Contact us", - "url": "https://surveys.ons.gov.uk/contact-us/", - "target": "_blank", - }, { "text": "Accessibility", "url": "https://www.ons.gov.uk/help/accessibility/", @@ -115,47 +98,39 @@ def expected_footer_business_theme(footer_context): ] } ], - "legal": [ - { - "itemsList": [ - { - "text": "Cookies", - "url": "https://surveys.ons.gov.uk/cookies/", - "target": "_blank", - }, - { - "text": "Privacy and data protection", - "url": "https://surveys.ons.gov.uk/privacy-and-data-protection/", - "target": "_blank", - }, - ] - } - ], } - return {**footer_context, **business} + return {**footer_context(), **business} -@fixture -def expected_footer_nisra_theme(): - return { - "lang": "en", +def expected_footer_social_theme(language_code: str): + ons_url = ONS_URL_CY if language_code == "cy" else ONS_URL + upstream_url = f"{ACCOUNT_SERVICE_BASE_URL_SOCIAL}/{language_code}" + social_footer_context = { + "lang": language_code, "crest": True, "newTabWarning": "The following links open in a new tab", "copyrightDeclaration": { - "copyright": "Crown copyright and database rights 2021 NIMA MOU577.501.", + "copyright": "Crown copyright and database rights 2020 OS 100019153.", "text": "Use of address data is subject to the terms and conditions.", }, + } + social = { "rows": [ { "itemsList": [ { - "text": "Help", - "url": "https://census.gov.uk/ni/help/help-with-the-questions/online-questions-help/", + "text": "What we do", + "url": f"{ons_url}/aboutus/whatwedo/", "target": "_blank", }, { "text": "Contact us", - "url": "https://census.gov.uk/ni/contact-us/", + "url": f"{ons_url}/aboutus/contactus/surveyenquiries/", + "target": "_blank", + }, + { + "text": "Accessibility", + "url": f"{ons_url}/help/accessibility/", "target": "_blank", }, ] @@ -166,29 +141,38 @@ def expected_footer_nisra_theme(): "itemsList": [ { "text": "Cookies", - "url": "https://census.gov.uk/ni/cookies/", + "url": f"{upstream_url}/cookies/", "target": "_blank", }, { - "text": "Accessibility statement", - "url": "https://census.gov.uk/ni/accessibility-statement/", + "text": "Privacy and data protection", + "url": f"{upstream_url}/privacy-and-data-protection/", "target": "_blank", }, + ] + } + ], + } + return social_footer_context | social + + +def expected_footer_social_theme_no_cookie(): + social = { + "rows": [ + { + "itemsList": [ { - "text": "Privacy and data protection", - "url": "https://census.gov.uk/ni/privacy-and-data-protection/", + "text": "What we do", + "url": "https://www.ons.gov.uk/aboutus/whatwedo/", "target": "_blank", }, { - "text": "Terms and conditions", - "url": "https://census.gov.uk/ni/terms-and-conditions/", + "text": "Accessibility", + "url": "https://www.ons.gov.uk/help/accessibility/", "target": "_blank", }, ] } ], - "poweredBy": { - "logo": "nisra-logo-black-en", - "alt": "NISRA - Northern Ireland Statistics and Research Agency", - }, } + return {**footer_context(), **social} diff --git a/tests/app/helpers/test_schema_helper.py b/tests/app/helpers/test_schema_helper.py index bc29b87111..d7fb2a7234 100644 --- a/tests/app/helpers/test_schema_helper.py +++ b/tests/app/helpers/test_schema_helper.py @@ -6,7 +6,14 @@ @pytest.mark.usefixtures("app") -def test_questionnaire_schema_passed_into_function(mocker, session_store): +def test_questionnaire_schema_passed_into_function( + mocker, session_store, fake_questionnaire_store +): + mocker.patch( + "app.helpers.schema_helpers.get_metadata", + return_value=fake_questionnaire_store.data_stores.metadata, + ) + mocker.patch( "app.helpers.schema_helpers.get_session_store", return_value=session_store, diff --git a/tests/app/helpers/test_template_helpers.py b/tests/app/helpers/test_template_helpers.py index 56bf9cfeaf..b7bce8136d 100644 --- a/tests/app/helpers/test_template_helpers.py +++ b/tests/app/helpers/test_template_helpers.py @@ -1,169 +1,385 @@ -from typing import Type - import pytest from flask import Flask, current_app +from flask import session as cookie_session from app.helpers.template_helpers import ContextHelper, get_survey_config -from app.settings import ACCOUNT_SERVICE_BASE_URL +from app.questionnaire import QuestionnaireSchema +from app.routes.session import set_schema_context_in_cookie +from app.settings import ( + ACCOUNT_SERVICE_BASE_URL, + ACCOUNT_SERVICE_BASE_URL_SOCIAL, + ONS_URL, + ONS_URL_CY, + read_file, +) from app.survey_config import ( BusinessSurveyConfig, - CensusNISRASurveyConfig, - CensusSurveyConfig, - NorthernIrelandBusinessSurveyConfig, + DBTBusinessSurveyConfig, + DBTDSITBusinessSurveyConfig, + DBTDSITNIBusinessSurveyConfig, + DBTNIBusinessSurveyConfig, + DESNZBusinessSurveyConfig, + DESNZNIBusinessSurveyConfig, + NIBusinessSurveyConfig, + ONSNHSSocialSurveyConfig, + ORRBusinessSurveyConfig, + SocialSurveyConfig, SurveyConfig, - WelshCensusSurveyConfig, + UKHSAONSSocialSurveyConfig, ) +from app.survey_config.survey_type import SurveyType +from tests.app.helpers.conftest import ( + expected_footer_business_theme, + expected_footer_business_theme_no_cookie, + expected_footer_social_theme, + expected_footer_social_theme_no_cookie, +) +from tests.app.questionnaire.conftest import get_metadata - -def test_footer_context_census_theme(app: Flask, expected_footer_census_theme): - with app.app_context(): - survey_config = CensusSurveyConfig() - result = ContextHelper( - language="en", - is_post_submission=False, - include_csrf_token=True, - survey_config=survey_config, - ).context["footer"] - - assert result == expected_footer_census_theme - - -def test_footer_context_business_theme(app: Flask, expected_footer_business_theme): - with app.test_client(): - survey_config = BusinessSurveyConfig() - - result = ContextHelper( - language="en", - is_post_submission=False, - include_csrf_token=True, - survey_config=survey_config, - ).context["footer"] - - assert result == expected_footer_business_theme - - -def test_footer_warning_in_context_census_theme(app: Flask): - with app.app_context(): - expected = "Make sure you leave this page or close your browser if using a shared device" - - survey_config = CensusSurveyConfig() - - result = ContextHelper( - language="en", - is_post_submission=True, - include_csrf_token=True, - survey_config=survey_config, - ).context["footer"]["footerWarning"] - - assert result == expected - - -def test_footer_warning_not_in_context_census_theme(app: Flask): - with app.app_context(): - with pytest.raises(KeyError): - _ = ContextHelper( - language="en", - is_post_submission=False, - include_csrf_token=True, - survey_config=CensusSurveyConfig(), - ).context["footer"]["footerWarning"] +DEFAULT_URL = "http://localhost" -def test_footer_context_census_nisra_theme(app: Flask, expected_footer_nisra_theme): +@pytest.mark.parametrize( + "theme, survey_config, language, expected_footer", + [ + ( + SurveyType.BUSINESS, + BusinessSurveyConfig(), + "en", + expected_footer_business_theme(), + ), + ( + None, + BusinessSurveyConfig(), + "en", + expected_footer_business_theme_no_cookie(), + ), + ( + SurveyType.SOCIAL, + SocialSurveyConfig(), + "en", + expected_footer_social_theme("en"), + ), + (None, SocialSurveyConfig(), "en", expected_footer_social_theme_no_cookie()), + ( + SurveyType.SOCIAL, + SocialSurveyConfig(language_code="cy"), + "cy", + expected_footer_social_theme("cy"), + ), + ], +) +def test_footer_context(app: Flask, theme, survey_config, language, expected_footer): with app.app_context(): - survey_config = CensusNISRASurveyConfig() + if theme: + cookie_session["theme"] = theme + config = survey_config result = ContextHelper( - language="en", + language=language, is_post_submission=False, include_csrf_token=True, - survey_config=survey_config, + survey_config=config, ).context["footer"] - assert result == expected_footer_nisra_theme - - -def test_get_page_header_context_business(app: Flask): - expected = { - "logo": "ons-logo-en", - "logoAlt": "Office for National Statistics logo", - } - - with app.app_context(): - survey_config = SurveyConfig() - - result = ContextHelper( - language="en", - is_post_submission=False, - include_csrf_token=True, - survey_config=survey_config, - ).context["page_header"] - - assert result == expected - + assert result == expected_footer -def test_get_page_header_context_census(app: Flask): - expected = { - "title": "Census 2021", - "logo": "ons-logo-en", - "logoAlt": "Office for National Statistics logo", - "titleLogo": "census-logo-en", - "titleLogoAlt": "Census 2021", - } +@pytest.mark.parametrize( + "theme, survey_title, survey_config, expected", + ( + ( + SurveyType.BUSINESS, + None, + BusinessSurveyConfig(), + ["ONS Surveys", None, None], + ), + ( + SurveyType.BUSINESS, + "Test", + BusinessSurveyConfig(), + ["Test", None, None], + ), + ( + None, + None, + BusinessSurveyConfig(), + ["ONS Surveys", None, None], + ), + ( + SurveyType.SOCIAL, + None, + SocialSurveyConfig(), + ["ONS Surveys", None, None], + ), + ( + SurveyType.SOCIAL, + "Test", + SocialSurveyConfig(), + ["Test", None, None], + ), + ( + SurveyType.SOCIAL, + "Test", + SocialSurveyConfig(language_code="cy"), + ["Test", None, None], + ), + ( + None, + None, + SocialSurveyConfig(), + ["ONS Surveys", None, None], + ), + ( + None, + None, + SurveyConfig(), + ["ONS Surveys", None, None], + ), + ( + None, + None, + NIBusinessSurveyConfig(), + [ + "ONS Surveys", + read_file("./templates/assets/images/finance-ni-logo.svg"), + read_file("./templates/assets/images/finance-ni-mobile-logo.svg"), + ], + ), + ( + SurveyType.NORTHERN_IRELAND, + "Test", + NIBusinessSurveyConfig(), + [ + "Test", + read_file("./templates/assets/images/finance-ni-logo.svg"), + read_file("./templates/assets/images/finance-ni-mobile-logo.svg"), + ], + ), + ( + None, + None, + DBTDSITBusinessSurveyConfig(), + [ + "ONS Surveys", + read_file("./templates/assets/images/dbt-logo-stacked.svg") + + read_file("./templates/assets/images/dsit-logo-stacked.svg"), + None, + ], + ), + ( + SurveyType.DBT_DSIT, + "Test", + DBTDSITBusinessSurveyConfig(), + [ + "Test", + read_file("./templates/assets/images/dbt-logo-stacked.svg") + + read_file("./templates/assets/images/dsit-logo-stacked.svg"), + None, + ], + ), + ( + None, + None, + DBTDSITNIBusinessSurveyConfig(), + [ + "ONS Surveys", + read_file("./templates/assets/images/dbt-logo-stacked.svg") + + read_file("./templates/assets/images/dsit-logo-stacked.svg") + + read_file("./templates/assets/images/finance-ni-logo-stacked.svg"), + None, + ], + ), + ( + SurveyType.DBT_DSIT_NI, + "Test", + DBTDSITNIBusinessSurveyConfig(), + [ + "Test", + read_file("./templates/assets/images/dbt-logo-stacked.svg") + + read_file("./templates/assets/images/dsit-logo-stacked.svg") + + read_file("./templates/assets/images/finance-ni-logo-stacked.svg"), + None, + ], + ), + ( + None, + None, + DBTBusinessSurveyConfig(), + [ + "ONS Surveys", + read_file("./templates/assets/images/dbt-logo-stacked.svg"), + None, + ], + ), + ( + SurveyType.DBT, + "Test", + DBTBusinessSurveyConfig(), + [ + "Test", + read_file("./templates/assets/images/dbt-logo-stacked.svg"), + None, + ], + ), + ( + None, + None, + DBTNIBusinessSurveyConfig(), + [ + "ONS Surveys", + read_file("./templates/assets/images/dbt-logo-stacked.svg") + + read_file("./templates/assets/images/finance-ni-logo-stacked.svg"), + None, + ], + ), + ( + SurveyType.DBT_NI, + "Test", + DBTNIBusinessSurveyConfig(), + [ + "Test", + read_file("./templates/assets/images/dbt-logo-stacked.svg") + + read_file("./templates/assets/images/finance-ni-logo-stacked.svg"), + None, + ], + ), + ( + None, + None, + ORRBusinessSurveyConfig(), + [ + "ONS Surveys", + read_file("./templates/assets/images/orr-logo.svg"), + read_file("./templates/assets/images/orr-mobile-logo.svg"), + ], + ), + ( + SurveyType.ORR, + "Test", + ORRBusinessSurveyConfig(), + [ + "Test", + read_file("./templates/assets/images/orr-logo.svg"), + read_file("./templates/assets/images/orr-mobile-logo.svg"), + ], + ), + ( + SurveyType.DESNZ, + "Test", + DESNZBusinessSurveyConfig(), + [ + "Test", + read_file("./templates/assets/images/desnz-logo-stacked.svg"), + None, + ], + ), + ( + SurveyType.DESNZ, + "Test", + DESNZNIBusinessSurveyConfig(), + [ + "Test", + read_file("./templates/assets/images/desnz-logo-stacked.svg") + + read_file("./templates/assets/images/finance-ni-logo-stacked.svg"), + None, + ], + ), + ( + SurveyType.UKHSA_ONS, + "Test", + UKHSAONSSocialSurveyConfig(), + [ + "Test", + read_file("./templates/assets/images/ukhsa-logo-stacked.svg") + + read_file("./templates/assets/images/ons-logo-stacked.svg"), + read_file("./templates/assets/images/ukhsa-logo-stacked.svg") + + read_file("./templates/assets/images/ons-logo-stacked.svg"), + ], + ), + ( + SurveyType.ONS_NHS, + "Test", + ONSNHSSocialSurveyConfig(), + [ + "Test", + read_file("./templates/assets/images/ons-logo-stacked.svg") + + read_file("./templates/assets/images/nhs-logo.svg"), + None, + ], + ), + ), +) +def test_header_context(app: Flask, theme, survey_title, survey_config, expected): with app.app_context(): - survey_config = CensusSurveyConfig() - - result = ContextHelper( + for cookie_name, cookie_value in { + "theme": theme, + "title": survey_title, + }.items(): + if cookie_value: + cookie_session[cookie_name] = cookie_value + + context_helper = ContextHelper( language="en", is_post_submission=False, include_csrf_token=True, survey_config=survey_config, - ).context["page_header"] - - assert result == expected - - -def test_get_page_header_context_census_nisra(app: Flask): - expected = { - "title": "Census 2021", - "logo": "nisra-logo-en", - "logoAlt": "Northern Ireland Statistics and Research Agency logo", - "titleLogo": "census-logo-en", - "titleLogoAlt": "Census 2021", - "customHeaderLogo": True, - "mobileLogo": "nisra-logo-en-mobile", - } + ) - with app.app_context(): - survey_config = CensusNISRASurveyConfig() - - result = ContextHelper( - language="en", - is_post_submission=False, - include_csrf_token=True, - survey_config=survey_config, - ).context["page_header"] + result = [ + context_helper.context["survey_title"], + context_helper.context["masthead_logo"], + context_helper.context["masthead_logo_mobile"], + ] assert result == expected @pytest.mark.parametrize( - "survey_config, is_authenticated, expected", + "survey_config, is_authenticated, theme, expected", [ ( SurveyConfig(), True, None, + None, ), ( BusinessSurveyConfig(), + False, + "business", + { + "toggleServicesButton": { + "text": "Menu", + "ariaLabel": "Toggle services menu", + }, + "itemsList": [ + { + "title": "Help", + "url": f"{ACCOUNT_SERVICE_BASE_URL}/help", + "id": "header-link-help", + } + ], + }, + ), + ( + BusinessSurveyConfig(schema=QuestionnaireSchema({"survey_id": "001"})), True, + "business", { + "toggleServicesButton": { + "text": "Menu", + "ariaLabel": "Toggle services menu", + }, "itemsList": [ + { + "title": "Help", + "url": f"{ACCOUNT_SERVICE_BASE_URL}/surveys/surveys-help?survey_ref=001&ru_ref=12345678901", + "id": "header-link-help", + }, { "title": "My account", - "url": "https://surveys.ons.gov.uk/my-account", + "url": f"{ACCOUNT_SERVICE_BASE_URL}/my-account", "id": "header-link-my-account", }, { @@ -171,19 +387,34 @@ def test_get_page_header_context_census_nisra(app: Flask): "url": "/sign-out", "id": "header-link-sign-out", }, - ] + ], }, ), + (SocialSurveyConfig(), False, None, None), + ( + SocialSurveyConfig(schema=QuestionnaireSchema({"survey_id": "001"})), + True, + "social", + None, + ), ], ) def test_service_links_context( - app: Flask, mocker, survey_config, is_authenticated, expected + app: Flask, mocker, survey_config, is_authenticated, theme, expected ): with app.app_context(): - current_user = mocker.patch( - "flask_login.utils._get_user", return_value=mocker.MagicMock() - ) - current_user.is_authenticated = is_authenticated + mocked_current_user = mocker.Mock() + mocked_current_user.is_authenticated = is_authenticated + mocker.patch("flask_login.utils._get_user", return_value=mocked_current_user) + cookie_session["theme"] = theme + + if is_authenticated: + mocker.patch( + "app.helpers.template_helpers.get_metadata", + return_value=get_metadata( + extra_metadata={"ru_ref": "12345678901A", "tx_id": "tx_id"}, + ), + ) result = ContextHelper( language="en", @@ -196,28 +427,89 @@ def test_service_links_context( @pytest.mark.parametrize( - "survey_config, expected", + "survey_config, language, expected", [ ( SurveyConfig(), - "https://surveys.ons.gov.uk/contact-us/", + "en", + f"{ACCOUNT_SERVICE_BASE_URL}/contact-us/", ), ( BusinessSurveyConfig(), - "https://surveys.ons.gov.uk/contact-us/", + "en", + f"{ACCOUNT_SERVICE_BASE_URL}/contact-us/", + ), + ( + NIBusinessSurveyConfig(), + "en", + f"{ACCOUNT_SERVICE_BASE_URL}/contact-us/", + ), + ( + DBTDSITBusinessSurveyConfig(), + "en", + f"{ACCOUNT_SERVICE_BASE_URL}/contact-us/", + ), + ( + DBTDSITNIBusinessSurveyConfig(), + "en", + f"{ACCOUNT_SERVICE_BASE_URL}/contact-us/", + ), + ( + DBTBusinessSurveyConfig(), + "en", + f"{ACCOUNT_SERVICE_BASE_URL}/contact-us/", + ), + ( + DBTNIBusinessSurveyConfig(), + "en", + f"{ACCOUNT_SERVICE_BASE_URL}/contact-us/", + ), + ( + ORRBusinessSurveyConfig(), + "en", + f"{ACCOUNT_SERVICE_BASE_URL}/contact-us/", + ), + ( + DESNZBusinessSurveyConfig(), + "en", + f"{ACCOUNT_SERVICE_BASE_URL}/contact-us/", + ), + ( + DESNZNIBusinessSurveyConfig(), + "en", + f"{ACCOUNT_SERVICE_BASE_URL}/contact-us/", ), ( - NorthernIrelandBusinessSurveyConfig(), - "https://surveys.ons.gov.uk/contact-us/", + SocialSurveyConfig(), + "en", + f"{ONS_URL}/aboutus/contactus/surveyenquiries/", + ), + ( + SocialSurveyConfig(language_code="cy"), + "cy", + f"{ONS_URL_CY}/aboutus/contactus/surveyenquiries/", + ), + ( + UKHSAONSSocialSurveyConfig(), + "en", + f"{ONS_URL}/aboutus/contactus/surveyenquiries/", + ), + ( + ONSNHSSocialSurveyConfig(), + "en", + f"{ONS_URL}/aboutus/contactus/surveyenquiries/", ), ], ) def test_contact_us_url_context( - app: Flask, survey_config: SurveyConfig, expected: dict[str, str] + app: Flask, + survey_config: SurveyConfig, + language: str, + expected: dict[str, str], ): with app.app_context(): result = ContextHelper( - language="en", + language=language, is_post_submission=False, include_csrf_token=True, survey_config=survey_config, @@ -230,7 +522,6 @@ def test_contact_us_url_context( "survey_config, expected", [ (SurveyConfig(), "Save and exit survey"), - (CensusSurveyConfig(), "Save and complete later"), ], ) def test_sign_out_button_text_context( @@ -248,41 +539,221 @@ def test_sign_out_button_text_context( @pytest.mark.parametrize( - "survey_config, expected", + "survey_config, cookie_present, expected", [ - (SurveyConfig(), "https://surveys.ons.gov.uk/cookies/"), + (SurveyConfig(), True, f"{ACCOUNT_SERVICE_BASE_URL}/cookies/"), ( BusinessSurveyConfig(), - "https://surveys.ons.gov.uk/cookies/", + True, + f"{ACCOUNT_SERVICE_BASE_URL}/cookies/", + ), + ( + NIBusinessSurveyConfig(), + True, + f"{ACCOUNT_SERVICE_BASE_URL}/cookies/", + ), + ( + DBTDSITBusinessSurveyConfig(), + True, + f"{ACCOUNT_SERVICE_BASE_URL}/cookies/", + ), + ( + DBTDSITNIBusinessSurveyConfig(), + True, + f"{ACCOUNT_SERVICE_BASE_URL}/cookies/", + ), + ( + DBTBusinessSurveyConfig(), + True, + f"{ACCOUNT_SERVICE_BASE_URL}/cookies/", + ), + ( + DBTNIBusinessSurveyConfig(), + True, + f"{ACCOUNT_SERVICE_BASE_URL}/cookies/", + ), + ( + ORRBusinessSurveyConfig(), + True, + f"{ACCOUNT_SERVICE_BASE_URL}/cookies/", + ), + ( + DESNZBusinessSurveyConfig(), + True, + f"{ACCOUNT_SERVICE_BASE_URL}/cookies/", + ), + ( + DESNZNIBusinessSurveyConfig(), + True, + f"{ACCOUNT_SERVICE_BASE_URL}/cookies/", + ), + ( + SocialSurveyConfig(), + True, + f"{ACCOUNT_SERVICE_BASE_URL_SOCIAL}/en/cookies/", ), ( - NorthernIrelandBusinessSurveyConfig(), - "https://surveys.ons.gov.uk/cookies/", + SocialSurveyConfig(language_code="cy"), + True, + f"{ACCOUNT_SERVICE_BASE_URL_SOCIAL}/cy/cookies/", + ), + ( + UKHSAONSSocialSurveyConfig(), + True, + f"{ACCOUNT_SERVICE_BASE_URL_SOCIAL}/en/cookies/", + ), + ( + ONSNHSSocialSurveyConfig(), + True, + f"{ACCOUNT_SERVICE_BASE_URL_SOCIAL}/en/cookies/", ), + (SurveyConfig(), False, None), ], ) def test_cookie_settings_url_context( - app: Flask, survey_config: SurveyConfig, expected: str + app: Flask, survey_config: SurveyConfig, cookie_present: bool, expected: str ): with app.app_context(): - result = ContextHelper( + if cookie_present: + cookie_session["theme"] = "dummy_value" + context_helper = ContextHelper( language="en", is_post_submission=False, include_csrf_token=True, survey_config=survey_config, - ).context["cookie_settings_url"] + ) + result = context_helper.context.get("cookie_settings_url") assert result == expected +@pytest.mark.parametrize( + "survey_config, language, address", + [ + (SurveyConfig(), "en", ACCOUNT_SERVICE_BASE_URL), + ( + BusinessSurveyConfig(), + "en", + ACCOUNT_SERVICE_BASE_URL, + ), + ( + NIBusinessSurveyConfig(), + "en", + ACCOUNT_SERVICE_BASE_URL, + ), + ( + DBTDSITBusinessSurveyConfig(), + "en", + ACCOUNT_SERVICE_BASE_URL, + ), + ( + DBTDSITNIBusinessSurveyConfig(), + "en", + ACCOUNT_SERVICE_BASE_URL, + ), + ( + DBTBusinessSurveyConfig(), + "en", + ACCOUNT_SERVICE_BASE_URL, + ), + ( + DBTNIBusinessSurveyConfig(), + "en", + ACCOUNT_SERVICE_BASE_URL, + ), + ( + ORRBusinessSurveyConfig(), + "en", + ACCOUNT_SERVICE_BASE_URL, + ), + ( + DESNZBusinessSurveyConfig(), + "en", + ACCOUNT_SERVICE_BASE_URL, + ), + ( + DESNZNIBusinessSurveyConfig(), + "en", + ACCOUNT_SERVICE_BASE_URL, + ), + ( + SocialSurveyConfig(), + "en", + ACCOUNT_SERVICE_BASE_URL_SOCIAL, + ), + ( + SocialSurveyConfig(), + "cy", + ACCOUNT_SERVICE_BASE_URL_SOCIAL, + ), + ( + UKHSAONSSocialSurveyConfig(), + "en", + ACCOUNT_SERVICE_BASE_URL_SOCIAL, + ), + ( + ONSNHSSocialSurveyConfig(), + "en", + ACCOUNT_SERVICE_BASE_URL_SOCIAL, + ), + ], +) +def test_cookie_domain_context( + app: Flask, survey_config: SurveyConfig, language: str, address: str +): + with app.app_context(): + cookie_session["theme"] = "dummy_value" + context_helper = ContextHelper( + language=language, + is_post_submission=False, + include_csrf_token=True, + survey_config=survey_config, + ) + + expected = address.replace("https://", "") + result = context_helper.context.get("cookie_domain") + + assert result == expected + + +@pytest.mark.parametrize( + "survey_config", + [ + SurveyConfig(), + BusinessSurveyConfig(), + SocialSurveyConfig(), + NIBusinessSurveyConfig(), + DBTBusinessSurveyConfig(), + DBTNIBusinessSurveyConfig(), + DBTDSITBusinessSurveyConfig(), + DBTDSITNIBusinessSurveyConfig(), + ORRBusinessSurveyConfig(), + UKHSAONSSocialSurveyConfig(), + ], +) +def test_cookie_domain_context_cookie_not_provided( + app: Flask, survey_config: SurveyConfig +): + with app.app_context(): + context_helper = ContextHelper( + language="en", + is_post_submission=False, + include_csrf_token=True, + survey_config=survey_config, + ) + + assert "cookie_domain" not in context_helper.context + + @pytest.mark.parametrize( "survey_config, expected", [ (SurveyConfig(), None), ( BusinessSurveyConfig(), - "https://surveys.ons.gov.uk/my-account", + f"{ACCOUNT_SERVICE_BASE_URL}/my-account", ), + (SocialSurveyConfig(), None), ], ) def test_account_service_my_account_url_context( @@ -301,7 +772,11 @@ def test_account_service_my_account_url_context( (SurveyConfig(), None), ( BusinessSurveyConfig(), - "https://surveys.ons.gov.uk/surveys/todo", + f"{ACCOUNT_SERVICE_BASE_URL}/surveys/todo", + ), + ( + SocialSurveyConfig(), + None, ), ], ) @@ -318,14 +793,55 @@ def test_account_service_my_todo_url_context( (SurveyConfig(), None), ( BusinessSurveyConfig(), - "https://surveys.ons.gov.uk/sign-in/logout", + f"{ACCOUNT_SERVICE_BASE_URL}/sign-in/logout", + ), + ( + NIBusinessSurveyConfig(), + f"{ACCOUNT_SERVICE_BASE_URL}/sign-in/logout", + ), + ( + DBTDSITBusinessSurveyConfig(), + f"{ACCOUNT_SERVICE_BASE_URL}/sign-in/logout", + ), + ( + DBTDSITNIBusinessSurveyConfig(), + f"{ACCOUNT_SERVICE_BASE_URL}/sign-in/logout", + ), + ( + DBTBusinessSurveyConfig(), + f"{ACCOUNT_SERVICE_BASE_URL}/sign-in/logout", + ), + ( + DBTNIBusinessSurveyConfig(), + f"{ACCOUNT_SERVICE_BASE_URL}/sign-in/logout", + ), + ( + ORRBusinessSurveyConfig(), + f"{ACCOUNT_SERVICE_BASE_URL}/sign-in/logout", + ), + ( + DESNZBusinessSurveyConfig(), + f"{ACCOUNT_SERVICE_BASE_URL}/sign-in/logout", + ), + ( + DESNZNIBusinessSurveyConfig(), + f"{ACCOUNT_SERVICE_BASE_URL}/sign-in/logout", ), - (CensusSurveyConfig(), "https://census.gov.uk/en/start"), - (WelshCensusSurveyConfig(), "https://cyfrifiad.gov.uk/en/start"), - (CensusNISRASurveyConfig(), "https://census.gov.uk/ni"), ( - NorthernIrelandBusinessSurveyConfig(), - "https://surveys.ons.gov.uk/sign-in/logout", + SocialSurveyConfig(), + f"{ACCOUNT_SERVICE_BASE_URL_SOCIAL}/en/start/", + ), + ( + SocialSurveyConfig(language_code="cy"), + f"{ACCOUNT_SERVICE_BASE_URL_SOCIAL}/cy/start/", + ), + ( + UKHSAONSSocialSurveyConfig(), + f"{ACCOUNT_SERVICE_BASE_URL_SOCIAL}/en/start/", + ), + ( + ONSNHSSocialSurveyConfig(), + f"{ACCOUNT_SERVICE_BASE_URL_SOCIAL}/en/start/", ), ], ) @@ -341,25 +857,25 @@ def test_account_service_log_out_url_context( @pytest.mark.parametrize( "theme, language, expected", [ - ("default", "en", SurveyConfig), - ("default", "cy", SurveyConfig), - ("business", "en", BusinessSurveyConfig), - ("business", "cy", BusinessSurveyConfig), - ("health", "en", SurveyConfig), - ("health", "cy", SurveyConfig), - ("social", "en", SurveyConfig), - ("social", "cy", SurveyConfig), - ("northernireland", "en", NorthernIrelandBusinessSurveyConfig), - ("northernireland", "cy", NorthernIrelandBusinessSurveyConfig), - ("census", "en", CensusSurveyConfig), - ("census", "cy", WelshCensusSurveyConfig), - ("census-nisra", "en", CensusNISRASurveyConfig), - ("census-nisra", "cy", CensusNISRASurveyConfig), + (SurveyType.DEFAULT, "en", SurveyConfig), + (SurveyType.DEFAULT, "cy", SurveyConfig), + (SurveyType.BUSINESS, "en", BusinessSurveyConfig), + (SurveyType.BUSINESS, "cy", BusinessSurveyConfig), + (SurveyType.HEALTH, "en", SocialSurveyConfig), + (SurveyType.SOCIAL, "en", SocialSurveyConfig), + (SurveyType.NORTHERN_IRELAND, "en", NIBusinessSurveyConfig), + (SurveyType.DBT, "en", DBTBusinessSurveyConfig), + (SurveyType.DBT_NI, "en", DBTNIBusinessSurveyConfig), + (SurveyType.DBT_DSIT, "en", DBTDSITBusinessSurveyConfig), + (SurveyType.DBT_DSIT_NI, "en", DBTDSITNIBusinessSurveyConfig), + (SurveyType.ORR, "en", ORRBusinessSurveyConfig), + (SurveyType.UKHSA_ONS, "en", UKHSAONSSocialSurveyConfig), + (SurveyType.ONS_NHS, "en", ONSNHSSocialSurveyConfig), (None, None, BusinessSurveyConfig), ], ) def test_get_survey_config( - app: Flask, theme: str, language: str, expected: SurveyConfig + app: Flask, theme: SurveyType, language: str, expected: SurveyConfig ): with app.app_context(): result = get_survey_config(theme=theme, language=language) @@ -367,17 +883,20 @@ def test_get_survey_config( @pytest.mark.parametrize( - "survey_config_type", - [SurveyConfig, BusinessSurveyConfig], + "survey_config_type, base_url", + [ + (SocialSurveyConfig, ACCOUNT_SERVICE_BASE_URL_SOCIAL), + (SurveyConfig, DEFAULT_URL), + (BusinessSurveyConfig, DEFAULT_URL), + ], ) def test_survey_config_base_url_provided_used_in_links( - app: Flask, survey_config_type: Type[SurveyConfig] + app: Flask, survey_config_type: type[SurveyConfig], base_url: str ): - base_url = "http://localhost" with app.app_context(): result = survey_config_type(base_url=base_url) - assert result.base_url == "http://localhost" + assert result.base_url == base_url urls_to_check = [ result.account_service_my_account_url, @@ -388,26 +907,29 @@ def test_survey_config_base_url_provided_used_in_links( result.privacy_and_data_protection_url, ] + if survey_config_type == SocialSurveyConfig: + urls_to_check.remove(result.contact_us_url) + for url in urls_to_check: if url: assert base_url in url def test_survey_config_base_url_duplicate_todo(app: Flask): - base_url = "http://localhost/surveys/todo" + base_url = f"{DEFAULT_URL}/surveys/todo" with app.app_context(): result = BusinessSurveyConfig(base_url=base_url) - assert result.base_url == "http://localhost" + assert result.base_url == DEFAULT_URL - assert result.account_service_log_out_url == "http://localhost/sign-in/logout" - assert result.account_service_my_account_url == "http://localhost/my-account" - assert result.account_service_todo_url == "http://localhost/surveys/todo" - assert result.contact_us_url == "http://localhost/contact-us/" - assert result.cookie_settings_url == "http://localhost/cookies/" + assert result.account_service_log_out_url == f"{DEFAULT_URL}/sign-in/logout" + assert result.account_service_my_account_url == f"{DEFAULT_URL}/my-account" + assert result.account_service_todo_url == f"{DEFAULT_URL}/surveys/todo" + assert result.contact_us_url == f"{DEFAULT_URL}/contact-us/" + assert result.cookie_settings_url == f"{DEFAULT_URL}/cookies/" assert ( result.privacy_and_data_protection_url - == "http://localhost/privacy-and-data-protection/" + == f"{DEFAULT_URL}/privacy-and-data-protection/" ) @@ -423,10 +945,7 @@ def test_context_set_from_app_config(app): current_app.config["CDN_URL"] = "test-cdn-url" current_app.config["CDN_ASSETS_PATH"] = "/test-assets-path" current_app.config["ADDRESS_LOOKUP_API_URL"] = "test-address-lookup-api-url" - current_app.config["EQ_GOOGLE_TAG_MANAGER_ID"] = "test-google-tag-manager-id" - current_app.config[ - "EQ_GOOGLE_TAG_MANAGER_AUTH" - ] = "test-google-tag-manager-auth" + current_app.config["EQ_GOOGLE_TAG_ID"] = "test-google-tag-manager-id" survey_config = SurveyConfig() context = ContextHelper( @@ -438,24 +957,28 @@ def test_context_set_from_app_config(app): assert context["cdn_url"] == "test-cdn-url/test-assets-path" assert context["address_lookup_api_url"] == "test-address-lookup-api-url" - assert context["google_tag_manager_id"] == "test-google-tag-manager-id" - assert context["google_tag_manager_auth"] == "test-google-tag-manager-auth" + assert context["google_tag_id"] == "test-google-tag-manager-id" @pytest.mark.parametrize( "theme, language, expected", [ - ("default", "en", None), - ("business", "en", None), - ("health", "en", None), - ("social", "en", None), - ("northernireland", "en", None), - ("census", "en", "census"), - ("census", "cy", "census"), - ("census-nisra", "en", "census"), + (SurveyType.DEFAULT, "en", None), + (SurveyType.BUSINESS, "en", None), + (SurveyType.HEALTH, "en", None), + (SurveyType.SOCIAL, "en", None), + (SurveyType.SOCIAL, "cy", None), + (SurveyType.NORTHERN_IRELAND, "en", None), + (SurveyType.DBT, "en", None), + (SurveyType.DBT_NI, "en", None), + (SurveyType.DBT_DSIT, "en", None), + (SurveyType.DBT_DSIT_NI, "en", None), + (SurveyType.ORR, "en", None), ], ) -def test_correct_theme_in_context(app: Flask, theme: str, language: str, expected: str): +def test_correct_theme_in_context( + app: Flask, theme: SurveyType, language: str, expected: str +): with app.app_context(): survey_config = get_survey_config(theme=theme, language=language) result = ContextHelper( @@ -470,18 +993,21 @@ def test_correct_theme_in_context(app: Flask, theme: str, language: str, expecte @pytest.mark.parametrize( "theme, language, expected", [ - ("default", "en", "ONS Business Surveys"), - ("business", "en", "ONS Business Surveys"), - ("health", "en", None), - ("social", "en", None), - ("northernireland", "en", "ONS Business Surveys"), - ("census", "en", "Census 2021"), - ("census", "cy", "Census 2021"), - ("census-nisra", "en", "Census 2021"), + (SurveyType.DEFAULT, "en", "ONS Surveys"), + (SurveyType.BUSINESS, "en", "ONS Surveys"), + (SurveyType.HEALTH, "en", "ONS Surveys"), + (SurveyType.SOCIAL, "en", "ONS Surveys"), + (SurveyType.SOCIAL, "cy", "ONS Surveys"), + (SurveyType.NORTHERN_IRELAND, "en", "ONS Surveys"), + (SurveyType.DBT, "en", "ONS Surveys"), + (SurveyType.DBT_NI, "en", "ONS Surveys"), + (SurveyType.DBT_DSIT, "en", "ONS Surveys"), + (SurveyType.DBT_DSIT_NI, "en", "ONS Surveys"), + (SurveyType.ORR, "en", "ONS Surveys"), ], ) -def test_correct_survey_title_in_context( - app: Flask, theme: str, language: str, expected: str +def test_use_default_survey_title_in_context_when_no_cookie( + app: Flask, theme: SurveyType, language: str, expected: str ): with app.app_context(): survey_config = get_survey_config(theme=theme, language=language) @@ -495,23 +1021,104 @@ def test_correct_survey_title_in_context( @pytest.mark.parametrize( - "theme, language, expected", + "theme, language, schema, expected", [ - ("default", "en", []), - ("business", "en", []), - ("health", "en", []), - ("social", "en", []), - ("northernireland", "en", []), - ("census", "en", [{"nisra": False}]), - ("census", "cy", [{"nisra": False}]), - ("census-nisra", "en", [{"nisra": True}]), + ( + SurveyType.DEFAULT, + "en", + QuestionnaireSchema({"survey_id": "001"}), + {"survey_id": "001"}, + ), + ( + SurveyType.DEFAULT, + "en", + QuestionnaireSchema({"survey_id": "001", "form_type": "test"}), + {"form_type": "test", "survey_id": "001"}, + ), + ( + SurveyType.BUSINESS, + "en", + QuestionnaireSchema( + {"survey_id": "001", "form_type": "test", "title": "test_title"} + ), + {"form_type": "test", "survey_id": "001", "title": "test_title"}, + ), + ( + SurveyType.HEALTH, + "en", + QuestionnaireSchema( + {"survey_id": "001", "form_type": "test", "title": "test_title"} + ), + {"form_type": "test", "survey_id": "001", "title": "test_title"}, + ), + ( + SurveyType.SOCIAL, + "en", + QuestionnaireSchema( + {"survey_id": "001", "form_type": "test", "title": "test_title"} + ), + {"form_type": "test", "survey_id": "001", "title": "test_title"}, + ), + ( + SurveyType.NORTHERN_IRELAND, + "en", + QuestionnaireSchema({"survey_id": "001"}), + {"survey_id": "001"}, + ), + ( + SurveyType.DBT_DSIT, + "en", + QuestionnaireSchema({"survey_id": "001"}), + {"survey_id": "001"}, + ), + ( + SurveyType.DBT_DSIT_NI, + "en", + QuestionnaireSchema({"survey_id": "001"}), + {"survey_id": "001"}, + ), + ( + SurveyType.DBT, + "en", + QuestionnaireSchema({"survey_id": "001"}), + {"survey_id": "001"}, + ), + ( + SurveyType.DBT_NI, + "en", + QuestionnaireSchema({"survey_id": "001"}), + {"survey_id": "001"}, + ), + ( + SurveyType.ORR, + "en", + QuestionnaireSchema({"survey_id": "001"}), + {"survey_id": "001"}, + ), + ( + SurveyType.DESNZ, + "en", + QuestionnaireSchema({"survey_id": "001"}), + {"survey_id": "001"}, + ), + ( + SurveyType.DESNZ_NI, + "en", + QuestionnaireSchema({"survey_id": "001"}), + {"survey_id": "001"}, + ), ], ) def test_correct_data_layer_in_context( - app: Flask, theme: str, language: str, expected: str + app: Flask, + theme: SurveyType, + language: str, + schema: QuestionnaireSchema, + expected: str, ): with app.app_context(): - survey_config = get_survey_config(theme=theme, language=language) + set_schema_context_in_cookie(schema) + survey_config = get_survey_config(theme=theme, language=language, schema=schema) result = ContextHelper( language="en", @@ -525,8 +1132,8 @@ def test_correct_data_layer_in_context( @pytest.mark.parametrize( "include_csrf_token", [ - (False), - (True), + False, + True, ], ) def test_include_csrf_token(app: Flask, include_csrf_token: bool): @@ -541,3 +1148,12 @@ def test_include_csrf_token(app: Flask, include_csrf_token: bool): ).context["include_csrf_token"] assert result == include_csrf_token + + +def test_get_survey_config_language_retrieved_from_cookie(app: Flask): + with app.app_context(): + cookie_session["language_code"] = "cy" + cookie_session["theme"] = SurveyType.SOCIAL + result = get_survey_config() + + assert result.account_service_log_out_url == f"{ACCOUNT_SERVICE_BASE_URL}/cy/start/" diff --git a/tests/app/mock_data_store.py b/tests/app/mock_data_store.py index c233d32f0b..c0e105e400 100644 --- a/tests/app/mock_data_store.py +++ b/tests/app/mock_data_store.py @@ -4,7 +4,6 @@ class MockDatastore: - # pylint: disable=unused-argument def __init__(self, **kwargs): self.storage = {} diff --git a/tests/app/oidc/__init__.py b/tests/app/oidc/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/app/oidc/test_gcp_oidc.py b/tests/app/oidc/test_gcp_oidc.py new file mode 100644 index 0000000000..cb269fd4ab --- /dev/null +++ b/tests/app/oidc/test_gcp_oidc.py @@ -0,0 +1,154 @@ +# pylint: disable=redefined-outer-name +import time + +import pytest +import responses +from cachetools.func import ttl_cache +from freezegun import freeze_time +from google.auth.exceptions import RefreshError, TransportError +from google.auth.transport.requests import Request +from mock import Mock, patch + +from app.oidc.gcp_oidc import TTL, OIDCCredentialsServiceGCP + +TEST_SDS_OAUTH2_CLIENT_ID = "TEST_SDS_OAUTH2_CLIENT_ID" +MOCK_TOKEN_URL = "http://mock-url" + + +@pytest.fixture +def oidc_credentials_service(): + oidc_credentials_service = OIDCCredentialsServiceGCP() + yield oidc_credentials_service + + # the get credentials method is static, and other tests are affected by the cache, so ensure it is cleared in the fixture teardown + oidc_credentials_service.get_credentials.cache.clear() + + +@pytest.fixture +def patch_authentication(): + # this fixture allows reaching the _metadata.get call with dummy credentials data, to observe a mocked request + with ( + patch( + "google.auth.compute_engine._metadata.get_service_account_info", + Mock(return_value={"email": "mock-email@gcp.com"}), + ), + patch( + "google.auth.compute_engine._metadata.ping", + Mock(return_value=True), + ), + patch( + "google.auth.compute_engine.credentials.jwt._unverified_decode", + Mock(return_value=(None, {"exp": 1672576200}, None, None)), + ), + patch( + "google.auth._helpers.update_query", + Mock(return_value=MOCK_TOKEN_URL), + ), + ): + yield + + +@patch("app.oidc.gcp_oidc.Request") +@patch("app.oidc.gcp_oidc.fetch_id_token_credentials") +def test_get_credentials(mock_token_fetch, mock_request, oidc_credentials_service): + """ + fetch credentials and check the gcp call is made + """ + oidc_credentials_service.get_credentials(iap_client_id=TEST_SDS_OAUTH2_CLIENT_ID) + mock_token_fetch.assert_called_once_with( + audience=TEST_SDS_OAUTH2_CLIENT_ID, request=mock_request() + ) + mock_token_fetch.return_value.refresh.assert_called_once_with(mock_request()) + + +@patch("app.oidc.gcp_oidc.Request", Mock) +@patch("app.oidc.gcp_oidc.fetch_id_token_credentials", Mock(side_effect=RefreshError)) +def test_get_credentials_failure(oidc_credentials_service): + """ + Check that if the request for credentials fails, the cache does not get updated + """ + with pytest.raises(RefreshError): + oidc_credentials_service.get_credentials( + iap_client_id=TEST_SDS_OAUTH2_CLIENT_ID + ) + + assert not oidc_credentials_service.get_credentials.cache + + +@freeze_time("2023-01-01T12:00:00") +@patch("app.oidc.gcp_oidc.Request", Mock) +@patch("app.oidc.gcp_oidc.fetch_id_token_credentials") +def test_get_credentials_ttl(mock_token_fetch, oidc_credentials_service): + """ + by default, TTLCache uses a cached version of time.monotonic for the timer + which means that mocking time with freezegun doesn't work, as it won't affect the timer + to work around this and test the caching, we can replace the timer + (as per https://github.com/spulec/freezegun/issues/477 ) + """ + # overwrite the timer + oidc_credentials_service.get_credentials = ttl_cache( + maxsize=None, ttl=TTL, timer=time.monotonic + )(oidc_credentials_service.get_credentials.__wrapped__) + + # initial fetch + oidc_credentials_service.get_credentials(iap_client_id=TEST_SDS_OAUTH2_CLIENT_ID) + assert mock_token_fetch.call_count == 1 + assert mock_token_fetch.return_value.refresh.call_count == 1 + + for datetime_to_invoke_at, expected_call_count in [ + # still valid after 30 minutes (valid until 12:55) + ("2023-01-01T12:30:00", 1), + # becomes invalid after 55 minutes (this new call makes it valid until 13:50) + ("2023-01-01T12:55:00", 2), + # this fetch is valid for another 1 minute until 13:50 + ("2023-01-01T13:49:00", 2), + # then at 13:50 it becomes invalid so new call is made + ("2023-01-01T13:50:00", 3), + ]: + # Mock the current time and check the call counter for the credentials service + with freeze_time(datetime_to_invoke_at): + oidc_credentials_service.get_credentials( + iap_client_id=TEST_SDS_OAUTH2_CLIENT_ID + ) + assert mock_token_fetch.call_count == expected_call_count + assert ( + mock_token_fetch.return_value.refresh.call_count == expected_call_count + ) + + +@pytest.mark.usefixtures("patch_authentication") +@freeze_time("2023-01-01T12:00:00") +@responses.activate +def test_get_credentials_with_retry(oidc_credentials_service): + """ + Test that fetching the credentials is implemented with a retry. + If the library changes, this test will fail, and we will need to implement our own. + """ + responses.add(responses.GET, url=MOCK_TOKEN_URL, body=TransportError()) + responses.add(responses.GET, url=MOCK_TOKEN_URL, body=TransportError()) + responses.add(responses.GET, url=MOCK_TOKEN_URL, json={}, status=200) + + # use a side effect of Request so that the code executes as normal but the call count can be tracked + tracked_request = Mock(side_effect=Request()) + + with patch("app.oidc.gcp_oidc.Request", Mock(return_value=tracked_request)): + credentials = oidc_credentials_service.get_credentials( + iap_client_id=TEST_SDS_OAUTH2_CLIENT_ID + ) + assert credentials.valid + # the request should have 3 attempts, the two transport failures and the success + assert tracked_request.call_count == 3 + + +@pytest.mark.usefixtures("patch_authentication") +def test_get_credentials_transport_failure(oidc_credentials_service): + """ + If the request repeatedly fails with transport error, we get a refresh error + don't assert the error message as it could change, but check the error is raised + """ + failing_request = Mock(side_effect=TransportError) + with patch("app.oidc.gcp_oidc.Request", Mock(return_value=failing_request)): + with pytest.raises(RefreshError): + oidc_credentials_service.get_credentials( + iap_client_id=TEST_SDS_OAUTH2_CLIENT_ID + ) diff --git a/tests/app/parser/conftest.py b/tests/app/parser/conftest.py index bbb16afce9..cca366a1d4 100644 --- a/tests/app/parser/conftest.py +++ b/tests/app/parser/conftest.py @@ -1,10 +1,23 @@ -# pylint: disable=redefined-outer-name import uuid +from datetime import datetime, timedelta, timezone import pytest +from app.authentication.auth_payload_versions import AuthPayloadVersion + + +def get_metadata(): + return fake_metadata_runner_v2() + + +def get_metadata_full(): + return fake_metadata_full_v2_business() + + +def get_metadata_social(): + return fake_metadata_full_v2_social() + -@pytest.fixture def fake_metadata_runner(): """Generate the set of claims required for runner to function""" return { @@ -16,27 +29,45 @@ def fake_metadata_runner(): "response_id": str(uuid.uuid4()), "account_service_url": "https://ras.ons.gov.uk", "case_id": str(uuid.uuid4()), + "response_expires_at": get_response_expires_at(), } -@pytest.fixture -def fake_business_metadata_runner(fake_metadata_runner): +@pytest.fixture() +def fake_business_metadata_runner(): """Generate a set of claims required for runner using business parameters instead of schema_name""" - del fake_metadata_runner["schema_name"] + metadata = get_metadata() + del metadata["schema_name"] - fake_metadata_runner["eq_id"] = "mbs" - fake_metadata_runner["form_type"] = "0253" + metadata["eq_id"] = "mbs" + metadata["form_type"] = "0253" + metadata["response_expires_at"] = get_response_expires_at() - return fake_metadata_runner + return metadata -@pytest.fixture -def fake_metadata_full(fake_metadata_runner): +def fake_metadata_runner_v2(): + """Generate the set of claims required for runner to function""" + return { + "tx_id": str(uuid.uuid4()), + "jti": str(uuid.uuid4()), + "schema_name": "2_a", + "collection_exercise_sid": "test-sid", + "response_id": str(uuid.uuid4()), + "account_service_url": "https://ras.ons.gov.uk", + "case_id": str(uuid.uuid4()), + "version": AuthPayloadVersion.V2.value, + "survey_metadata": {"data": {"key": "value"}}, + "response_expires_at": get_response_expires_at(), + } + + +def fake_metadata_full_v2_business(): """Generate a fake set of claims These claims should represent all claims known to runner, including common questionnaire level claims. """ - fake_questionnaire_claims = { + fake_survey_metadata_claims = { "user_id": "1", "period_id": "3", "period_str": "2016-01-01", @@ -45,10 +76,34 @@ def fake_metadata_full(fake_metadata_runner): "ru_name": "Apple", "return_by": "2016-07-07", "case_ref": "1000000000000001", - "case_id": str(uuid.uuid4()), + "ru_ref": "12345678901A", + "form_type": "I", + "response_expires_at": get_response_expires_at(), } - return dict(fake_metadata_runner, **fake_questionnaire_claims) + metadata = fake_metadata_runner_v2() + + metadata["survey_metadata"]["data"] = fake_survey_metadata_claims + + return metadata + + +def fake_metadata_full_v2_social(): + """Generate a fake set of claims + These claims should represent all claims known to runner, including common questionnaire + level claims. + """ + fake_survey_metadata_claims = { + "case_ref": "1000000000000001", + "qid": "2000000000000002", + } + + metadata = fake_metadata_runner_v2() + + metadata["survey_metadata"]["data"] = fake_survey_metadata_claims + metadata["survey_metadata"]["receipting_keys"] = ["qid"] + + return metadata @pytest.fixture @@ -61,3 +116,7 @@ def fake_questionnaire_metadata_requirements_full(): {"name": "ref_p_end_date", "type": "string"}, {"name": "account_service_url", "type": "url", "optional": True}, ] + + +def get_response_expires_at() -> str: + return (datetime.now(tz=timezone.utc) + timedelta(days=1)).isoformat() diff --git a/tests/app/parser/test_metadata_parser.py b/tests/app/parser/test_metadata_parser.py index 395bc18c41..feb1c586f1 100644 --- a/tests/app/parser/test_metadata_parser.py +++ b/tests/app/parser/test_metadata_parser.py @@ -4,143 +4,185 @@ from freezegun import freeze_time from marshmallow import ValidationError -from app.utilities.metadata_parser import ( +from app.utilities.metadata_parser_v2 import ( validate_questionnaire_claims, - validate_runner_claims, + validate_runner_claims_v2, +) +from tests.app.parser.conftest import ( + get_metadata, + get_metadata_full, + get_metadata_social, ) -def test_spaces_are_stripped_from_string_fields(fake_metadata_runner): - fake_metadata_runner["collection_exercise_sid"] = " stripped " +def test_spaces_are_stripped_from_string_fields(): + metadata = get_metadata() + metadata["collection_exercise_sid"] = " stripped " - output = validate_runner_claims(fake_metadata_runner) + output = validate_runner_claims_v2(metadata) assert output["collection_exercise_sid"] == "stripped" -def test_empty_strings_are_not_valid(fake_metadata_runner): - fake_metadata_runner["schema_name"] = "" +def test_empty_strings_are_not_valid(): + metadata = get_metadata() + metadata["schema_name"] = "" with pytest.raises(ValidationError): - validate_runner_claims(fake_metadata_runner) + validate_runner_claims_v2(metadata) def test_validation_does_not_change_metadata( - fake_metadata_full, fake_questionnaire_metadata_requirements_full + fake_questionnaire_metadata_requirements_full, ): - fake_metadata_copy = deepcopy(fake_metadata_full) + metadata = get_metadata_full() + + fake_metadata_copy = deepcopy(metadata) + + questionnaire_claims = metadata["survey_metadata"]["data"] + validate_questionnaire_claims( - fake_metadata_full, fake_questionnaire_metadata_requirements_full + questionnaire_claims, fake_questionnaire_metadata_requirements_full ) - assert fake_metadata_full == fake_metadata_copy + assert metadata == fake_metadata_copy + +def test_validation_no_error_when_optional_field_not_passed(): + metadata = get_metadata_full() -def test_validation_no_error_when_optional_field_not_passed(fake_metadata_runner): field_specification = [ {"name": "optional_field", "type": "string", "optional": True} ] - validate_questionnaire_claims(fake_metadata_runner, field_specification) + validate_questionnaire_claims(metadata, field_specification) + +def test_validation_field_required_by_default(): + metadata = get_metadata_full() -def test_validation_field_required_by_default(fake_metadata_runner): field_specification = [{"name": "required_field", "type": "string"}] with pytest.raises(ValidationError): - validate_questionnaire_claims(fake_metadata_runner, field_specification) + validate_questionnaire_claims(metadata, field_specification) -def test_minimum_length(fake_metadata_runner): +def test_minimum_length(): + metadata = get_metadata_full() + field_specification = [{"name": "some_field", "type": "string", "min_length": 5}] - fake_metadata_runner["some_field"] = "123456" + questionnaire_claims = metadata["survey_metadata"]["data"] + + questionnaire_claims["some_field"] = "123456" - validate_questionnaire_claims(fake_metadata_runner, field_specification) + validate_questionnaire_claims(questionnaire_claims, field_specification) - fake_metadata_runner["some_field"] = "1" + questionnaire_claims["some_field"] = "1" with pytest.raises(ValidationError): - validate_questionnaire_claims(fake_metadata_runner, field_specification) + validate_questionnaire_claims(questionnaire_claims, field_specification) + +def test_maximum_length(): + metadata = get_metadata_full() -def test_maximum_length(fake_metadata_runner): field_specification = [{"name": "some_field", "type": "string", "max_length": 5}] - fake_metadata_runner["some_field"] = "1234" + questionnaire_claims = metadata["survey_metadata"]["data"] - validate_questionnaire_claims(fake_metadata_runner, field_specification) + questionnaire_claims["some_field"] = "1234" - fake_metadata_runner["some_field"] = "123456" + validate_questionnaire_claims(questionnaire_claims, field_specification) + + questionnaire_claims["some_field"] = "123456" with pytest.raises(ValidationError): - validate_questionnaire_claims(fake_metadata_runner, field_specification) + validate_questionnaire_claims(questionnaire_claims, field_specification) + +def test_min_and_max_length(): + metadata = get_metadata_full() -def test_min_and_max_length(fake_metadata_runner): field_specification = [ {"name": "some_field", "type": "string", "min_length": 4, "max_length": 5} ] - fake_metadata_runner["some_field"] = "1234" + questionnaire_claims = metadata["survey_metadata"]["data"] - validate_questionnaire_claims(fake_metadata_runner, field_specification) + questionnaire_claims["some_field"] = "1234" - fake_metadata_runner["some_field"] = "123456" + validate_questionnaire_claims(questionnaire_claims, field_specification) + + questionnaire_claims["some_field"] = "123456" with pytest.raises(ValidationError): - validate_questionnaire_claims(fake_metadata_runner, field_specification) + validate_questionnaire_claims(questionnaire_claims, field_specification) - fake_metadata_runner["some_field"] = "123" + questionnaire_claims["some_field"] = "123" with pytest.raises(ValidationError): - validate_questionnaire_claims(fake_metadata_runner, field_specification) + validate_questionnaire_claims(questionnaire_claims, field_specification) + +def test_length_equals(): + metadata = get_metadata_full() -def test_length_equals(fake_metadata_runner): field_specification = [{"name": "some_field", "type": "string", "length": 4}] - fake_metadata_runner["some_field"] = "1234" + questionnaire_claims = metadata["survey_metadata"]["data"] + + questionnaire_claims["some_field"] = "1234" - validate_questionnaire_claims(fake_metadata_runner, field_specification) + validate_questionnaire_claims(questionnaire_claims, field_specification) - fake_metadata_runner["some_field"] = "123456" + questionnaire_claims["some_field"] = "123456" with pytest.raises(ValidationError): - validate_questionnaire_claims(fake_metadata_runner, field_specification) + validate_questionnaire_claims(questionnaire_claims, field_specification) - fake_metadata_runner["some_field"] = "123" + questionnaire_claims["some_field"] = "123" with pytest.raises(ValidationError): - validate_questionnaire_claims(fake_metadata_runner, field_specification) + validate_questionnaire_claims(questionnaire_claims, field_specification) -def test_uuid_deserialisation(fake_metadata_runner): - claims = validate_runner_claims(fake_metadata_runner) +def test_uuid_deserialisation(): + metadata = get_metadata_full() + + claims = validate_runner_claims_v2(metadata) assert isinstance(claims["tx_id"], str) -def test_unknown_claims_are_not_deserialized(fake_metadata_runner): - fake_metadata_runner["unknown_key"] = "some value" - claims = validate_runner_claims(fake_metadata_runner) +def test_unknown_claims_are_not_deserialized(): + metadata = get_metadata_full() + + metadata["unknown_key"] = "some value" + claims = validate_runner_claims_v2(metadata) assert "unknown_key" not in claims -def test_minimum_length_on_runner_metadata(fake_metadata_runner): - validate_runner_claims(fake_metadata_runner) +def test_minimum_length_on_runner_metadata(): + metadata = get_metadata_full() - fake_metadata_runner["schema_name"] = "" + validate_runner_claims_v2(metadata) + + metadata["collection_exercise_sid"] = "" with pytest.raises(ValidationError): - validate_runner_claims(fake_metadata_runner) + validate_runner_claims_v2(metadata) -def test_deserialisation_iso_8601_dates(fake_metadata_runner): +def test_deserialisation_iso_8601_dates(): """Runner cannot currently handle date objects in metadata""" + metadata = get_metadata_full() + field_specification = [{"name": "birthday", "type": "date"}] - fake_metadata_runner["birthday"] = "2019-11-1" - claims = validate_questionnaire_claims(fake_metadata_runner, field_specification) + questionnaire_claims = metadata["survey_metadata"]["data"] + + questionnaire_claims["birthday"] = "2019-11-1" + claims = validate_questionnaire_claims(questionnaire_claims, field_specification) assert isinstance(claims["birthday"], str) @@ -148,72 +190,100 @@ def test_deserialisation_iso_8601_dates(fake_metadata_runner): @freeze_time("2021-11-15T15:34:54+00:00") @pytest.mark.parametrize( "date_string", - ["2021-11-22T15:34:54+00:00", "2021-11-22T15:34:54Z"], + [ + ("2021-11-22T15:34:54+00:00"), + ("2021-11-22T15:34:54Z"), + ], ) -def test_deserialisation_iso_8601_date(date_string, fake_metadata_runner): - fake_metadata_runner["response_expires_at"] = date_string - claims = validate_runner_claims(fake_metadata_runner) +def test_deserialisation_iso_8601_date(date_string): + metadata = get_metadata_full() + + metadata["response_expires_at"] = date_string + + claims = validate_runner_claims_v2(metadata) + assert claims["response_expires_at"] == "2021-11-22T15:34:54+00:00" -def test_deserialisation_iso_8601_datetime_past_datetime_raises_ValidationError( - fake_metadata_runner, -): - fake_metadata_runner["response_expires_at"] = "1900-11-22T15:34:54+00:00" +def test_deserialisation_iso_8601_datetime_past_datetime_raises_ValidationError(): + metadata = get_metadata_full() + + metadata["response_expires_at"] = "1900-11-22T15:34:54+00:00" with pytest.raises(ValidationError): - validate_runner_claims(fake_metadata_runner) + validate_runner_claims_v2(metadata) @freeze_time("2021-11-15T15:34:54+00:00") -def test_deserialisation_iso_8601_datetime_bad_datetime_raises_ValidationError( - fake_metadata_runner, -): - fake_metadata_runner["response_expires_at"] = "2021-11-22" - with pytest.raises(ValidationError): - validate_runner_claims(fake_metadata_runner) +def test_deserialisation_iso_8601_datetime_bad_datetime_raises_ValidationError(): + metadata = get_metadata_full() + metadata["response_expires_at"] = "2021-11-22" + with pytest.raises(ValidationError): + validate_runner_claims_v2(metadata) -def test_business_params_without_schema_name(fake_business_metadata_runner): - claims = validate_runner_claims(fake_business_metadata_runner) - assert claims["schema_name"] == "mbs_0253" +def test_empty_schema_name_and_schema_url_and_cir_instrument_id_not_valid_v2(): + metadata = get_metadata_full() + del metadata["schema_name"] + with pytest.raises(ValidationError) as exc: + validate_runner_claims_v2(metadata) -def test_when_response_id_is_missing(fake_business_metadata_runner): - expected = ( - f"{fake_business_metadata_runner['ru_ref']}" - f"{fake_business_metadata_runner['collection_exercise_sid']}" - f"{fake_business_metadata_runner['eq_id']}" - f"{fake_business_metadata_runner['form_type']}" + assert ( + "Neither schema_name, schema_url or cir_instrument_id has been set in metadata" + in str(exc) ) - del fake_business_metadata_runner["response_id"] - claims = validate_runner_claims(fake_business_metadata_runner) - assert claims["response_id"] == expected - - -def test_when_response_id_is_present(fake_business_metadata_runner): - claims = validate_runner_claims(fake_business_metadata_runner) - assert claims["response_id"] == fake_business_metadata_runner["response_id"] @pytest.mark.parametrize( - "metadata", ["eq_id", "form_type", "ru_ref", "collection_exercise_sid"] + "options", + [ + { + "schema_name": "test_name", + "cir_instrument_id": "f0519981-426c-8b93-75c0-bfc40c66fe25", + }, + { + "schema_url": "http://test.json", + "cir_instrument_id": "f0519981-426c-8b93-75c0-bfc40c66fe25", + }, + { + "schema_name": "test_name", + "schema_url": "http://test.json", + "cir_instrument_id": "f0519981-426c-8b93-75c0-bfc40c66fe25", + }, + {"schema_name": "test_name", "schema_url": "http://test.json"}, + ], ) -def test_response_id_for_missing_metadata(metadata, fake_business_metadata_runner): - fake_business_metadata_runner["schema_name"] = "schema_name" - del fake_business_metadata_runner["response_id"] - del fake_business_metadata_runner[metadata] - with pytest.raises(ValidationError): - validate_runner_claims(fake_business_metadata_runner) +def test_too_many_of_schema_name_schema_url_and_cir_instrument_id_not_valid_v2(options): + metadata = get_metadata_full() + del metadata["schema_name"] + + metadata.update(options) + provided = ", ".join(options) + with pytest.raises(ValidationError) as exc: + validate_runner_claims_v2(metadata) -def test_response_id_for_empty_value(fake_business_metadata_runner): - expected = ( - f"{fake_business_metadata_runner['ru_ref']}" - f"{fake_business_metadata_runner['collection_exercise_sid']}" - f"{fake_business_metadata_runner['eq_id']}" - f"{fake_business_metadata_runner['form_type']}" + assert ( + f"Only one of schema_name, schema_url or cir_instrument_id should be specified in metadata, but {provided} were provided" + in str(exc) ) - fake_business_metadata_runner["response_id"] = "" - claims = validate_runner_claims(fake_business_metadata_runner) - assert claims["response_id"] == expected + + +def test_valid_v2_social_claims(): + metadata = get_metadata_social() + + fake_metadata_copy = deepcopy(metadata) + + claims = validate_runner_claims_v2(metadata) + + assert claims == fake_metadata_copy + + +def test_invalid_v2_social_claims_missing_receipting_key_raises_error(): + metadata = get_metadata_social() + + del metadata["survey_metadata"]["data"]["qid"] + + with pytest.raises(ValidationError): + validate_runner_claims_v2(metadata) diff --git a/tests/app/parser/test_supplementary_data_parser.py b/tests/app/parser/test_supplementary_data_parser.py new file mode 100644 index 0000000000..c5243b1c87 --- /dev/null +++ b/tests/app/parser/test_supplementary_data_parser.py @@ -0,0 +1,244 @@ +from contextlib import contextmanager +from copy import deepcopy + +import pytest +from marshmallow import ValidationError + +from app.services.supplementary_data import validate_supplementary_data +from app.utilities.supplementary_data_parser import validate_supplementary_data_v1 + + +@contextmanager +def not_raises(exception): + try: + yield + except exception as validation_error: + raise pytest.fail(f"{validation_error} RAISED") + + +SUPPLEMENTARY_DATA_PAYLOAD = { + "dataset_id": "44f1b432-9421-49e5-bd26-e63e18a30b69", + "survey_id": "123", + "data": { + "schema_version": "v1", + "identifier": "12345678901", + "items": { + "local_units": [ + { + "identifier": 1, + "lu_name": "TEST NAME. 1", + "lu_address": [ + "FIRST ADDRESS 1", + "FIRST ADDRESS 2", + "TOWN", + "COUNTY", + "POST CODE", + ], + }, + { + "identifier": "0002", + "lu_name": "TEST NAME 2", + "lu_address": [ + "SECOND ADDRESS 1", + "SECOND ADDRESS 1", + "TOWN", + "COUNTY", + "POSTCODE", + ], + }, + ] + }, + }, +} + + +def test_invalid_supplementary_data_payload_raises_error(): + with pytest.raises(ValidationError) as error: + validate_supplementary_data( + supplementary_data={}, + dataset_id="44f1b432-9421-49e5-bd26-e63e18a30b69", + identifier="12345678901", + survey_id="123", + ) + + assert str(error.value) == "Invalid supplementary data" + + +def test_invalid_supplementary_dataset_version_raises_error(): + with pytest.raises(ValidationError) as error: + validate_supplementary_data_v1( + supplementary_data=SUPPLEMENTARY_DATA_PAYLOAD, + dataset_id="44f1b432-9421-49e5-bd26-e63e18a30b69", + identifier="12345678901", + survey_id="123", + sds_schema_version="v6", + ) + + assert ( + str(error.value) + == "{'_schema': ['The Supplementary Dataset Schema Version does not match the version set in the Questionnaire Schema']}" + ) + + +def test_valid_supplementary_dataset_version_does_not_raise_error(): + with not_raises(ValidationError): + validated_payload = validate_supplementary_data_v1( + supplementary_data=SUPPLEMENTARY_DATA_PAYLOAD, + dataset_id="44f1b432-9421-49e5-bd26-e63e18a30b69", + identifier="12345678901", + survey_id="123", + sds_schema_version="v1", + ) + + assert validated_payload == SUPPLEMENTARY_DATA_PAYLOAD + + +def test_validate_supplementary_data_payload(): + validated_payload = validate_supplementary_data_v1( + supplementary_data=SUPPLEMENTARY_DATA_PAYLOAD, + dataset_id="44f1b432-9421-49e5-bd26-e63e18a30b69", + identifier="12345678901", + survey_id="123", + ) + + assert validated_payload == SUPPLEMENTARY_DATA_PAYLOAD + + +def test_validate_supplementary_data_payload_incorrect_dataset_id(): + with pytest.raises(ValidationError) as error: + validate_supplementary_data_v1( + supplementary_data=SUPPLEMENTARY_DATA_PAYLOAD, + dataset_id="331507ca-1039-4624-a342-7cbc3630e217", + identifier="12345678901", + survey_id="123", + ) + + assert ( + str(error.value) + == "{'_schema': ['Supplementary data did not return the specified Dataset ID']}" + ) + + +def test_validate_supplementary_data_payload_incorrect_survey_id(): + with pytest.raises(ValidationError) as error: + validate_supplementary_data_v1( + supplementary_data=SUPPLEMENTARY_DATA_PAYLOAD, + dataset_id="44f1b432-9421-49e5-bd26-e63e18a30b69", + identifier="12345678901", + survey_id="234", + ) + + assert ( + str(error.value) + == "{'_schema': ['Supplementary data did not return the specified Survey ID']}" + ) + + +def test_validate_supplementary_data_payload_incorrect_identifier(): + with pytest.raises(ValidationError) as error: + validate_supplementary_data_v1( + supplementary_data=SUPPLEMENTARY_DATA_PAYLOAD, + dataset_id="44f1b432-9421-49e5-bd26-e63e18a30b69", + identifier="000000000001", + survey_id="123", + ) + + assert ( + str(error.value) + == "{'data': {'_schema': ['Supplementary data did not return the specified Identifier']}}" + ) + + +def test_supplementary_data_payload_with_no_items_is_validated(): + payload = { + "dataset_id": "44f1b432-9421-49e5-bd26-e63e18a30b69", + "survey_id": "123", + "data": { + "schema_version": "v1", + "identifier": "12345678901", + }, + } + + validated_payload = validate_supplementary_data_v1( + supplementary_data=payload, + dataset_id="44f1b432-9421-49e5-bd26-e63e18a30b69", + identifier="12345678901", + survey_id="123", + ) + + assert validated_payload == payload + + +def test_validate_supplementary_data_payload_missing_survey_id(): + payload = { + "dataset_id": "44f1b432-9421-49e5-bd26-e63e18a30b69", + "data": { + "schema_version": "v1", + "identifier": "12345678901", + }, + } + + with pytest.raises(ValidationError) as error: + validate_supplementary_data_v1( + supplementary_data=payload, + dataset_id="44f1b432-9421-49e5-bd26-e63e18a30b69", + identifier="12345678901", + survey_id="123", + ) + + assert str(error.value) == "{'survey_id': ['Missing data for required field.']}" + + +def test_validate_supplementary_data_payload_with_unknown_field(): + payload = { + "dataset_id": "44f1b432-9421-49e5-bd26-e63e18a30b69", + "survey_id": "123", + "some_field": "value", + "data": { + "schema_version": "v1", + "identifier": "12345678901", + }, + } + + validated_payload = validate_supplementary_data_v1( + supplementary_data=payload, + dataset_id="44f1b432-9421-49e5-bd26-e63e18a30b69", + identifier="12345678901", + survey_id="123", + ) + + assert validated_payload == payload + + +def test_validate_supplementary_data_payload_missing_identifier_in_items(): + payload = deepcopy(SUPPLEMENTARY_DATA_PAYLOAD) + payload["data"]["items"]["local_units"][0].pop("identifier") + + with pytest.raises(ValidationError) as error: + validate_supplementary_data_v1( + supplementary_data=payload, + dataset_id="44f1b432-9421-49e5-bd26-e63e18a30b69", + identifier="12345678901", + survey_id="123", + ) + + assert str(error.value) == "{'identifier': ['Missing data for required field.']}" + + +@pytest.mark.parametrize("invalid_identifier", ["", ["invalid"], -1, {}]) +def test_validate_supplementary_data_payload_invalid_identifier(invalid_identifier): + payload = deepcopy(SUPPLEMENTARY_DATA_PAYLOAD) + payload["data"]["items"]["local_units"][0]["identifier"] = invalid_identifier + + with pytest.raises(ValidationError) as error: + validate_supplementary_data_v1( + supplementary_data=payload, + dataset_id="44f1b432-9421-49e5-bd26-e63e18a30b69", + identifier="12345678901", + survey_id="123", + ) + + assert ( + str(error.value) + == "{'identifier': ['Item identifier must be a non-empty string or non-negative integer']}" + ) diff --git a/tests/app/publisher/test_publisher.py b/tests/app/publisher/test_publisher.py index d6f9901c05..569fc18191 100644 --- a/tests/app/publisher/test_publisher.py +++ b/tests/app/publisher/test_publisher.py @@ -1,6 +1,10 @@ +from unittest.mock import Mock, patch from uuid import uuid4 import pytest +from google.cloud.pubsub_v1.open_telemetry.publish_message_wrapper import ( + PublishMessageWrapper, +) from google.pubsub_v1.types.pubsub import PubsubMessage from app.publisher.exceptions import PublicationFailed @@ -28,16 +32,21 @@ def test_publish(publisher, mocker): assert future is mocker.sentinel.future # Check mock. - batch.publish.assert_has_calls([mocker.call(PubsubMessage(data=b"test-message"))]) + batch.publish.assert_has_calls( + [mocker.call(PublishMessageWrapper(PubsubMessage({"data": b"test-message"})))] + ) def test_resolving_message_raises_exception_on_error(publisher): - with pytest.raises(PublicationFailed) as ex: - # Try resolve the future with an invalid credentials - publisher.publish( - "test-topic-id", - b"test-message", - fulfilment_request_transaction_id=str(uuid4()), - ) - - assert "403 The request is missing a valid API key." in str(ex.value) + mock_future = Mock() + mock_future.result.side_effect = Exception() + + with patch( + "app.publisher.publisher.PubSubPublisher._publish", return_value=mock_future + ): + with pytest.raises(PublicationFailed): + publisher.publish( + "test-topic-id", + b"test-message", + fulfilment_request_transaction_id=str(uuid4()), + ) diff --git a/tests/app/questionnaire/conftest.py b/tests/app/questionnaire/conftest.py index 83229a2326..239f251a90 100644 --- a/tests/app/questionnaire/conftest.py +++ b/tests/app/questionnaire/conftest.py @@ -1,11 +1,13 @@ -# pylint: disable=redefined-outer-name +# pylint: disable=redefined-outer-name, too-many-lines import pytest -from werkzeug.datastructures import ImmutableDict -from app.data_models import QuestionnaireStore +from app.data_models import CompletionStatus, QuestionnaireStore, SupplementaryDataStore from app.data_models.answer_store import Answer, AnswerStore +from app.data_models.data_stores import DataStores from app.data_models.list_store import ListStore +from app.data_models.metadata_proxy import TOP_LEVEL_METADATA_KEYS, MetadataProxy +from app.data_models.progress import ProgressDict from app.data_models.progress_store import ProgressStore from app.questionnaire import QuestionnaireSchema from app.questionnaire.location import Location @@ -15,10 +17,37 @@ find_pointers_containing, ) from app.questionnaire.placeholder_transforms import PlaceholderTransforms +from app.questionnaire.router import Router from app.questionnaire.routing_path import RoutingPath from app.utilities.schema import load_schema_from_name +def get_metadata(extra_metadata: dict | None = None): + metadata = { + "response_id": "1", + "account_service_url": "account_service_url", + "tx_id": "tx_id", + "collection_exercise_sid": "collection_exercise_sid", + "case_id": "case_id", + "version": "v2", + "survey_metadata": {"data": {}}, + } + + if extra_metadata: + for key, value in extra_metadata.items(): + if key in TOP_LEVEL_METADATA_KEYS: + metadata[key] = value + else: + metadata["survey_metadata"]["data"][key] = value + + return MetadataProxy.from_dict(metadata) + + +@pytest.fixture +def supplementary_data_schema(): + return load_schema_from_name("test_supplementary_data") + + @pytest.fixture def placeholder_list(): return [ @@ -34,11 +63,6 @@ def answer_store(): return AnswerStore([{"answer_id": "first-name", "value": "Joe"}]) -@pytest.fixture -def location(): - return Location("test-section", "test-block", "test-list", "list_item_id") - - @pytest.fixture def response_metadata(): return {"started_at": "2021-01-01T09:00:00.220038+00:00"} @@ -48,10 +72,7 @@ def response_metadata(): def parser(answer_store, location, mock_schema, mock_renderer): return PlaceholderParser( language="en", - answer_store=answer_store, - list_store=ListStore(), - metadata={}, - response_metadata={}, + data_stores=DataStores(answer_store=answer_store, metadata=get_metadata()), schema=mock_schema, location=location, renderer=mock_renderer, @@ -95,13 +116,15 @@ def question_variant_schema(): "title": "Block 1", "question_variants": [ { - "when": [ - { - "id": "when-answer", - "condition": "equals", - "value": "yes", - } - ], + "when": { + "==": [ + { + "identifier": "when-answer", + "source": "answers", + }, + "yes", + ] + }, "question": { "id": "question1", "type": "General", @@ -110,18 +133,21 @@ def question_variant_schema(): { "id": "answer1", "label": "Answer 1 Variant 1", + "type": "General", } ], }, }, { - "when": [ - { - "id": "when-answer", - "condition": "not equals", - "value": "yes", - } - ], + "when": { + "!=": [ + { + "identifier": "when-answer", + "source": "answers", + }, + "yes", + ] + }, "question": { "id": "question1", "type": "General", @@ -168,6 +194,7 @@ def single_question_schema(): { "id": "answer1", "label": "Answer 1", + "type": "General", "default": "test", } ], @@ -189,8 +216,8 @@ def list_collector_variant_schema(): "id": "section", "groups": [ { - "id": "group", - "title": "List", + "id": "when-group", + "title": "When Group", "blocks": [ { "type": "Question", @@ -211,7 +238,18 @@ def list_collector_variant_schema(): } ], }, - }, + } + ], + } + ], + }, + { + "id": "list-section", + "groups": [ + { + "id": "list-group", + "title": "List", + "blocks": [ { "id": "block1", "type": "ListCollector", @@ -226,19 +264,22 @@ def list_collector_variant_schema(): { "id": "answer1", "label": "Collector Answer 1 Variant Yes", + "type": "General", "action": { "type": "RedirectToListAddBlock" }, } ], }, - "when": [ - { - "id": "when-answer", - "condition": "equals", - "value": "yes", - } - ], + "when": { + "==": [ + { + "identifier": "when-answer", + "source": "answers", + }, + "yes", + ] + }, }, { "question": { @@ -252,13 +293,15 @@ def list_collector_variant_schema(): } ], }, - "when": [ - { - "id": "when-answer", - "condition": "equals", - "value": "no", - } - ], + "when": { + "==": [ + { + "identifier": "when-answer", + "source": "answers", + }, + "no", + ] + }, }, ], "add_block": { @@ -277,13 +320,15 @@ def list_collector_variant_schema(): } ], }, - "when": [ - { - "id": "when-answer", - "condition": "equals", - "value": "yes", - } - ], + "when": { + "==": [ + { + "identifier": "when-answer", + "source": "answers", + }, + "yes", + ] + }, }, { "question": { @@ -297,13 +342,15 @@ def list_collector_variant_schema(): } ], }, - "when": [ - { - "id": "when-answer", - "condition": "equals", - "value": "no", - } - ], + "when": { + "==": [ + { + "identifier": "when-answer", + "source": "answers", + }, + "no", + ] + }, }, ], }, @@ -323,13 +370,15 @@ def list_collector_variant_schema(): } ], }, - "when": [ - { - "id": "when-answer", - "condition": "equals", - "value": "yes", - } - ], + "when": { + "==": [ + { + "identifier": "when-answer", + "source": "answers", + }, + "yes", + ] + }, }, { "question": { @@ -343,13 +392,15 @@ def list_collector_variant_schema(): } ], }, - "when": [ - { - "id": "when-answer", - "condition": "equals", - "value": "no", - } - ], + "when": { + "==": [ + { + "identifier": "when-answer", + "source": "answers", + }, + "no", + ] + }, }, ], }, @@ -372,13 +423,15 @@ def list_collector_variant_schema(): } ], }, - "when": [ - { - "id": "when-answer", - "condition": "equals", - "value": "yes", - } - ], + "when": { + "==": [ + { + "identifier": "when-answer", + "source": "answers", + }, + "yes", + ] + }, }, { "question": { @@ -392,21 +445,95 @@ def list_collector_variant_schema(): } ], }, - "when": [ + "when": { + "==": [ + { + "identifier": "when-answer", + "source": "answers", + }, + "no", + ] + }, + }, + ], + }, + }, + ], + } + ], + }, + { + "id": "content-section", + "groups": [ + { + "id": "group-content", + "blocks": [ + { + "type": "Interstitial", + "id": "household-occupancy", + "content_variants": [ + { + "content": { + "title": "Household Occupancy", + "contents": [ + { + "description": "According to your answer this household is occupied" + } + ], + }, + "when": { + ">": [ + { + "source": "list", + "identifier": "people", + "selector": "count", + }, + 0, + ] + }, + }, + { + "content": { + "title": "Household Occupancy", + "contents": [ { - "id": "when-answer", - "condition": "equals", - "value": "no", + "description": "According to your answer this household is unoccupied" } ], }, + "when": { + "==": [ + { + "source": "list", + "identifier": "people", + "selector": "count", + }, + 0, + ] + }, + }, + ], + }, + { + "type": "Question", + "id": "block-occupancy", + "question": { + "answers": [ + { + "id": "answer-occupancy", + "mandatory": True, + "type": "General", + } ], + "id": "question-occupancy", + "title": {"text": "Does anyone else live here?"}, + "type": "General", }, }, ], } ], - } + }, ] } @@ -467,7 +594,6 @@ def sections_dependent_on_list_schema(): }, "when": { ">": [ - 0, { "count": [ { @@ -476,6 +602,7 @@ def sections_dependent_on_list_schema(): } ] }, + 0, ] }, }, @@ -521,7 +648,6 @@ def sections_dependent_on_list_schema(): }, "when": { ">": [ - 0, { "count": [ { @@ -530,6 +656,7 @@ def sections_dependent_on_list_schema(): } ] }, + 0, ] }, }, @@ -558,13 +685,16 @@ def sections_dependent_on_list_schema(): "title": {"text": "Does anyone else live here?"}, "type": "General", }, - "when": [ - { - "condition": "greater than", - "list": "not-the-list", - "value": 0, - } - ], + "when": { + "<": [ + 0, + { + "identifier": "not-the-list", + "source": "list", + "selector": "count", + }, + ] + }, } ], } @@ -591,13 +721,16 @@ def sections_dependent_on_list_schema(): "title": {"text": "Does anyone else live here?"}, "type": "General", }, - "when": [ - { - "condition": "greater than", - "list": "list", - "value": 0, - } - ], + "when": { + ">": [ + { + "identifier": "list", + "source": "list", + "selector": "count", + }, + 0, + ] + }, } ], } @@ -642,6 +775,64 @@ def sections_dependent_on_list_schema(): } ], }, + { + "id": "section5", + "groups": [ + { + "id": "group5", + "blocks": [ + { + "type": "Question", + "id": "block5", + "question": { + "answers": [ + { + "id": "answer1", + "mandatory": True, + "type": "General", + } + ], + "id": "question1", + "title": {"text": "Does anyone else live here?"}, + "type": "General", + }, + "when": { + ">": [ + { + "identifier": "missing-the-source-attribute", + "selector": "count", + }, + 0, + ] + }, + } + ], + } + ], + }, + { + "id": "section6", + "enabled": { + "when": { + ">": [ + {"count": [{"source": "list", "identifier": "list"}]}, + 0, + ] + } + }, + "groups": [ + { + "id": "group6", + "blocks": [ + { + "type": "Question", + "id": "block6", + "question": {}, + } + ], + } + ], + }, ] } @@ -680,30 +871,40 @@ def content_variant_schema(): "title": "Block 1", "content_variants": [ { - "content": [{"title": "You are over 16"}], - "when": [ - { - "id": "age-answer", - "condition": "greater than", - "value": "16", - } - ], + "content": {"title": "You are over 16"}, + "when": { + ">": [ + { + "identifier": "age-answer", + "source": "answers", + }, + 16, + ] + }, }, { - "content": [{"title": "You are under 16"}], - "when": [ - { - "id": "age-answer", - "condition": "less than or equal to", - "value": "16", - } - ], + "content": {"title": "You are under 16"}, + "when": { + "<=": [ + { + "identifier": "age-answer", + "source": "answers", + }, + 16, + ] + }, }, { - "content": [{"title": "You are ageless"}], - "when": [ - {"id": "age-answer", "condition": "not set"} - ], + "content": {"title": "You are ageless"}, + "when": { + "==": [ + { + "identifier": "age-answer", + "source": "answers", + }, + None, + ] + }, }, ], }, @@ -734,7 +935,13 @@ def question_schema(): "id": "question1", "title": "A Question", "type": "General", - "answers": [{"id": "answer1", "label": "Answer 1"}], + "answers": [ + { + "id": "answer1", + "label": "Answer 1", + "type": "General", + } + ], }, } ], @@ -902,6 +1109,10 @@ def mock_schema(mocker): } ) ) + schema.is_answer_dynamic = mocker.MagicMock(return_value=False) + schema.is_answer_in_list_collector_repeating_block = mocker.MagicMock( + return_value=False + ) return schema @@ -913,25 +1124,22 @@ def placeholder_renderer(option_label_from_value_schema): {"answer_id": "mandatory-checkbox-answer", "value": ["Body"]}, ] ) - renderer = PlaceholderRenderer( + return PlaceholderRenderer( language="en", - answer_store=answer_store, - list_store=ListStore(), - metadata=ImmutableDict({"trad_as": "ESSENTIAL SERVICES LTD"}), - response_metadata={}, + data_stores=DataStores( + answer_store=answer_store, + metadata=get_metadata(extra_metadata={"trad_as": "ESSENTIAL SERVICES LTD"}), + ), schema=option_label_from_value_schema, + location=Location(section_id="checkbox-section"), ) - return renderer @pytest.fixture def mock_renderer(mock_schema): return PlaceholderRenderer( language="en", - answer_store=AnswerStore(), - list_store=ListStore(), - metadata=ImmutableDict({}), - response_metadata={}, + data_stores=DataStores(), schema=mock_schema, ) @@ -947,8 +1155,8 @@ def default_placeholder_value_schema(): @pytest.fixture -def transformer(mock_renderer, mock_schema): - def _transform(language="en"): +def transformer(mock_renderer, mock_schema, locale_string="en_GB"): + def _transform(language=locale_string): return PlaceholderTransforms( language=language, schema=mock_schema, renderer=mock_renderer ) @@ -1067,6 +1275,11 @@ def mock_empty_answer_store(mocker): return mocker.MagicMock(spec=AnswerStore) +@pytest.fixture +def mock_router(mocker): + return mocker.MagicMock(spec=Router) + + @pytest.fixture def mock_empty_progress_store(mocker): progress_store = mocker.MagicMock(spec=ProgressStore) @@ -1074,16 +1287,29 @@ def mock_empty_progress_store(mocker): return progress_store +@pytest.fixture +def mock_empty_supplementary_data_store(mocker): + supplementary_data_store = mocker.MagicMock(spec=SupplementaryDataStore) + return supplementary_data_store + + @pytest.fixture def mock_questionnaire_store( - populated_list_store, mock_empty_answer_store, mock_empty_progress_store, mocker + populated_list_store, + mock_empty_answer_store, + mock_empty_progress_store, + mock_empty_supplementary_data_store, + mocker, ): return mocker.MagicMock( spec=QuestionnaireStore, completed_blocks=[], - answer_store=mock_empty_answer_store, - list_store=populated_list_store, - progress_store=mock_empty_progress_store, + data_stores=DataStores( + answer_store=mock_empty_answer_store, + list_store=populated_list_store, + progress_store=mock_empty_progress_store, + supplementary_data_store=mock_empty_supplementary_data_store, + ), ) @@ -1095,7 +1321,7 @@ def block_ids(): @pytest.fixture def routing_path(block_ids): return RoutingPath( - block_ids, + block_ids=block_ids, section_id="section-1", list_item_id="list_item_id", list_name="list_name", @@ -1154,6 +1380,11 @@ def calculated_question_with_dependent_sections_schema_repeating(): ) +@pytest.fixture +def calculated_question_with_dependent_sections_schema(): + return load_schema_from_name("test_validation_sum_against_value_source") + + @pytest.fixture def calculated_summary_schema(): return load_schema_from_name("test_calculated_summary") @@ -1172,3 +1403,248 @@ def dynamic_radio_options_from_checkbox_schema(): @pytest.fixture def dynamic_answer_options_function_driven_schema(): return load_schema_from_name("test_dynamic_answer_options_function_driven") + + +@pytest.fixture +def skipping_section_dependencies_schema(): + return load_schema_from_name("test_routing_and_skipping_section_dependencies") + + +@pytest.fixture +def section_dependencies_calculated_summary_schema(): + return load_schema_from_name( + "test_routing_and_skipping_section_dependencies_calculated_summary" + ) + + +@pytest.fixture +def section_dependencies_new_calculated_summary_schema(): + return load_schema_from_name( + "test_routing_and_skipping_section_dependencies_new_calculated_summary" + ) + + +@pytest.fixture +def progress_block_dependencies_schema(): + return load_schema_from_name("test_progress_value_source_calculated_summary") + + +@pytest.fixture +def progress_section_dependencies_schema(): + return load_schema_from_name( + "test_progress_value_source_section_enabled_hub_complex" + ) + + +@pytest.fixture +def progress_dependencies_schema(): + return load_schema_from_name( + "test_progress_value_source_calculated_summary_extended" + ) + + +@pytest.fixture +def grand_calculated_summary_schema(): + return load_schema_from_name("test_grand_calculated_summary") + + +@pytest.fixture +def grand_calculated_summary_in_repeating_section_schema(): + return load_schema_from_name( + "test_grand_calculated_summary_inside_repeating_section" + ) + + +@pytest.fixture +def grand_calculated_summary_progress_store(): + return ProgressStore( + [ + ProgressDict( + section_id="section-1", + block_ids=[ + "first-number-block", + "second-number-block", + "distance-calculated-summary-1", + "number-calculated-summary-1", + ], + status=CompletionStatus.COMPLETED, + ) + ] + ) + + +@pytest.fixture +def grand_calculated_summary_repeating_answers_schema(): + return load_schema_from_name("test_grand_calculated_summary_repeating_answers") + + +@pytest.fixture +def grand_calculated_summary_repeating_answers_progress_store(): + return ProgressStore( + [ + ProgressDict( + section_id="section-5", + block_ids=[ + "any-streaming-services", + "any-other-streaming-services", + "calculated-summary-6", + "calculated-summary-7", + "other-internet-usage", + "calculated-summary-8", + ], + status=CompletionStatus.COMPLETED, + ), + ProgressDict( + section_id="section-5", + block_ids=[ + "streaming-service-repeating-block-1", + "streaming-service-repeating-block-2", + ], + status=CompletionStatus.COMPLETED, + list_item_id="item-1", + ), + ProgressDict( + section_id="section-5", + block_ids=[ + "streaming-service-repeating-block-1", + "streaming-service-repeating-block-2", + ], + status=CompletionStatus.COMPLETED, + list_item_id="item-2", + ), + ProgressDict( + section_id="section-4", + block_ids=[ + "any-utility-bills", + "any-other-utility-bills", + "dynamic-answer", + "calculated-summary-5", + ], + status=CompletionStatus.COMPLETED, + ), + ProgressDict( + section_id="section-1", + block_ids=[ + "block-1", + "block-2", + "calculated-summary-1", + "block-3", + "calculated-summary-2", + "calculated-summary-3", + "grand-calculated-summary-1", + ], + status=CompletionStatus.COMPLETED, + ), + ProgressDict( + section_id="section-2", + block_ids=["block-4", "calculated-summary-4"], + status=CompletionStatus.COMPLETED, + ), + ] + ) + + +@pytest.fixture +@pytest.mark.usefixtures("app", "gb_locale") +def placeholder_transform_question_dynamic_answers_json(): + return { + "dynamic_answers": { + "values": {"source": "list", "identifier": "supermarkets"}, + "answers": [ + { + "label": { + "text": "Percentage of shopping at {transformed_value}", + "placeholders": [ + { + "placeholder": "transformed_value", + "value": { + "source": "answers", + "identifier": "supermarket-name", + }, + } + ], + }, + "id": "percentage-of-shopping", + "mandatory": False, + "type": "Percentage", + "maximum": {"value": 100}, + "decimal_places": 0, + } + ], + }, + "answers": [], + "id": "dynamic-answer-question", + "title": "What percent of your shopping do you do at each of the following supermarket?", + "type": "General", + } + + +@pytest.fixture +@pytest.mark.usefixtures("app", "gb_locale") +def placeholder_transform_question_repeating_block(): + return { + "id": "repeating-block-1", + "type": "ListRepeatingQuestion", + "question": { + "id": "transport-repeating-block-1-question", + "type": "General", + "title": "title", + }, + "answers": [ + { + "id": "transport-cost", + "label": { + "placeholders": [ + { + "placeholder": "transport_name", + "value": { + "source": "answers", + "identifier": "transport-name", + }, + } + ], + "text": "What is your monthly expenditure travelling by {transport_name}?", + }, + "mandatory": True, + "type": "Currency", + "currency": "GBP", + } + ], + } + + +@pytest.fixture +@pytest.mark.usefixtures("app", "gb_locale") +def placeholder_transform_question_dynamic_answers_pointer_json(): + return { + "question": { + "dynamic_answers": { + "values": {"source": "list", "identifier": "supermarkets"}, + "answers": [ + { + "label": { + "text": "Percentage of shopping at {transformed_value}", + "placeholders": [ + { + "placeholder": "transformed_value", + "value": { + "source": "answers", + "identifier": "supermarket-name", + }, + } + ], + }, + "id": "percentage-of-shopping", + "mandatory": False, + "type": "Percentage", + "maximum": {"value": 100}, + "decimal_places": 0, + } + ], + }, + "answers": [], + "id": "dynamic-answer-question", + "title": "What percent of your shopping do you do at each of the following supermarket?", + "type": "General", + } + } diff --git a/tests/app/questionnaire/rules/test_operators.py b/tests/app/questionnaire/rules/test_operators.py index 1953d79888..3b67da88ca 100644 --- a/tests/app/questionnaire/rules/test_operators.py +++ b/tests/app/questionnaire/rules/test_operators.py @@ -498,3 +498,20 @@ def test_operation_option_label_from_value(get_operator, mock_schema, mocker): mock_schema.get_answers_by_answer_id = mocker.Mock(return_value=answer_schema) assert operator.evaluate(operands) == "Head-label" + + +@pytest.mark.parametrize( + "operands, expected_result", + [ + ([1, 2, 3, 4], 10), + ([1.1, 2.2, 3.3, 4.4], 11.0), + ([[]], 0), + ([None], 0), + ([1, 2.2, 3, 4], 10.2), + (["a", 1], 1), + ], +) +def test_operator_sum(get_operator, operands, expected_result): + operator = get_operator(Operator.SUM) + + assert operator.evaluate(operands) == expected_result diff --git a/tests/app/questionnaire/rules/test_rule_evaluator.py b/tests/app/questionnaire/rules/test_rule_evaluator.py index 5143de59b8..65283a1b5f 100644 --- a/tests/app/questionnaire/rules/test_rule_evaluator.py +++ b/tests/app/questionnaire/rules/test_rule_evaluator.py @@ -1,16 +1,24 @@ from datetime import datetime, timezone -from typing import Mapping, Optional, Union -from unittest.mock import Mock import pytest from freezegun import freeze_time +from mock import MagicMock, Mock -from app.data_models import AnswerStore, ListStore +from app.data_models import ( + AnswerStore, + ListStore, + ProgressStore, + SupplementaryDataStore, +) from app.data_models.answer import Answer +from app.data_models.data_stores import DataStores +from app.data_models.progress import CompletionStatus, ProgressDict from app.questionnaire import Location, QuestionnaireSchema from app.questionnaire.relationship_location import RelationshipLocation from app.questionnaire.rules.operator import Operator from app.questionnaire.rules.rule_evaluator import RuleEvaluator +from app.utilities.schema import load_schema_from_name +from tests.app.questionnaire.conftest import get_metadata from tests.app.questionnaire.test_value_source_resolver import get_list_items current_date = datetime.now(timezone.utc).date() @@ -18,7 +26,7 @@ def get_mock_schema(): - schema = Mock( + schema = MagicMock( QuestionnaireSchema( { "questionnaire_flow": { @@ -28,6 +36,8 @@ def get_mock_schema(): } ) ) + schema.is_answer_dynamic = Mock(return_value=False) + schema.is_answer_in_list_collector_repeating_block = Mock(return_value=False) return schema @@ -35,27 +45,23 @@ def get_rule_evaluator( *, language="en", schema: QuestionnaireSchema = None, - answer_store: AnswerStore = AnswerStore(), - list_store: ListStore = ListStore(), - metadata: Optional[dict] = None, - response_metadata: Mapping = None, - location: Union[Location, RelationshipLocation] = Location( + data_stores: DataStores = None, + location: Location | RelationshipLocation = Location( section_id="test-section", block_id="test-block" ), - routing_path_block_ids: Optional[list] = None, + routing_path_block_ids: list | None = None, ): if not schema: schema = get_mock_schema() - schema.is_repeating_answer = Mock(return_value=True) + schema.get_list_name_for_answer_id = Mock(return_value="mock-list") schema.get_default_answer = Mock(return_value=None) + schema.is_answer_dynamic = Mock(return_value=False) + schema.is_answer_in_list_collector_repeating_block = Mock(return_value=False) return RuleEvaluator( language=language, schema=schema, - metadata=metadata or {}, - response_metadata=response_metadata, - answer_store=answer_store, - list_store=list_store, + data_stores=data_stores, location=location, routing_path_block_ids=routing_path_block_ids, ) @@ -103,7 +109,11 @@ def test_boolean_operators_as_rule(rule, expected_result): ) def test_answer_source(answer_value, expected_result): rule_evaluator = get_rule_evaluator( - answer_store=AnswerStore([{"answer_id": "some-answer", "value": answer_value}]), + data_stores=DataStores( + answer_store=AnswerStore( + [{"answer_id": "some-answer", "value": answer_value}] + ) + ), ) assert ( @@ -122,14 +132,16 @@ def test_answer_source(answer_value, expected_result): ) def test_answer_source_with_list_item_selector_location(answer_value, expected_result): rule_evaluator = get_rule_evaluator( - answer_store=AnswerStore( - [ - { - "answer_id": "some-answer", - "list_item_id": "item-1", - "value": answer_value, - } - ] + data_stores=DataStores( + answer_store=AnswerStore( + [ + { + "answer_id": "some-answer", + "list_item_id": "item-1", + "value": answer_value, + } + ] + ) ), location=Location( section_id="some-section", block_id="some-block", list_item_id="item-1" @@ -164,16 +176,18 @@ def test_answer_source_with_list_item_selector_list_first_item( answer_value, expected_result ): rule_evaluator = get_rule_evaluator( - answer_store=AnswerStore( - [ - { - "answer_id": "some-answer", - "list_item_id": "item-1", - "value": answer_value, - } - ] + data_stores=DataStores( + answer_store=AnswerStore( + [ + { + "answer_id": "some-answer", + "list_item_id": "item-1", + "value": answer_value, + } + ] + ), + list_store=ListStore([{"name": "some-list", "items": get_list_items(3)}]), ), - list_store=ListStore([{"name": "some-list", "items": get_list_items(3)}]), ) assert ( @@ -203,14 +217,16 @@ def test_answer_source_with_list_item_selector_list_first_item( ) def test_answer_source_with_dict_answer_selector(answer_value, expected_result): rule_evaluator = get_rule_evaluator( - answer_store=AnswerStore( - [ - { - "answer_id": "some-answer", - "value": {"years": answer_value, "months": 10}, - } - ] - ), + data_stores=DataStores( + answer_store=AnswerStore( + [ + { + "answer_id": "some-answer", + "value": {"years": answer_value, "months": 10}, + } + ] + ), + ) ) assert ( @@ -236,14 +252,16 @@ def test_answer_source_with_dict_answer_selector(answer_value, expected_result): ) def test_metadata_source(metadata_value, expected_result): rule_evaluator = get_rule_evaluator( - metadata={"some-metadata": metadata_value}, + data_stores=DataStores( + metadata=get_metadata(extra_metadata={"some_key": metadata_value}) + ) ) assert ( rule_evaluator.evaluate( rule={ Operator.EQUAL: [ - {"source": "metadata", "identifier": "some-metadata"}, + {"source": "metadata", "identifier": "some_key"}, 3, ] }, @@ -252,6 +270,45 @@ def test_metadata_source(metadata_value, expected_result): ) +@pytest.mark.parametrize( + "identifier, selector, expected_result", + [("s1-b1", "block", True), ("s1-b2", "block", True), ("s1-b3", "block", False)], +) +def test_progress_source(identifier, selector, expected_result): + schema = load_schema_from_name("test_progress_value_source_blocks") + in_progress_sections = [ + ProgressDict( + section_id="section-1", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["s1-b1", "s1-b2"], + ), + ] + + rule_evaluator = get_rule_evaluator( + schema=schema, + location=Location(section_id="section-1", block_id="s1-b3", list_item_id=None), + data_stores=DataStores(progress_store=ProgressStore(in_progress_sections)), + routing_path_block_ids=["s1-b1", "s1-b2", "s1-b3"], + ) + + assert ( + rule_evaluator.evaluate( + rule={ + Operator.EQUAL: [ + { + "source": "progress", + "selector": selector, + "identifier": identifier, + }, + "COMPLETED", + ] + }, + ) + is expected_result + ) + + @pytest.mark.parametrize( "response_metadata_value, expected_result", [ @@ -261,7 +318,9 @@ def test_metadata_source(metadata_value, expected_result): ) def test_response_metadata_source(response_metadata_value, expected_result): rule_evaluator = get_rule_evaluator( - response_metadata={"started_at": response_metadata_value}, + data_stores=DataStores( + response_metadata={"started_at": response_metadata_value}, + ) ) assert ( @@ -283,9 +342,11 @@ def test_response_metadata_source(response_metadata_value, expected_result): ) def test_list_source(list_count, expected_result): rule_evaluator = get_rule_evaluator( - list_store=ListStore( - [{"name": "some-list", "items": get_list_items(list_count)}] - ), + data_stores=DataStores( + list_store=ListStore( + [{"name": "some-list", "items": get_list_items(list_count)}] + ), + ) ) assert ( @@ -307,7 +368,9 @@ def test_list_source(list_count, expected_result): ) def test_list_source_with_id_selector_first(list_item_id, expected_result): rule_evaluator = get_rule_evaluator( - list_store=ListStore([{"name": "some-list", "items": get_list_items(1)}]), + data_stores=DataStores( + list_store=ListStore([{"name": "some-list", "items": get_list_items(1)}]), + ) ) assert ( @@ -333,15 +396,17 @@ def test_list_source_with_id_selector_first(list_item_id, expected_result): ) def test_list_source_with_id_selector_same_name_items(list_item_id, expected_result): rule_evaluator = get_rule_evaluator( - list_store=ListStore( - [ - { - "name": "some-list", - "items": get_list_items(5), - "same_name_items": get_list_items(3), - } - ] - ), + data_stores=DataStores( + list_store=ListStore( + [ + { + "name": "some-list", + "items": get_list_items(5), + "same_name_items": get_list_items(3), + } + ] + ), + ) ) assert ( @@ -377,14 +442,16 @@ def test_list_source_id_selector_primary_person( ) rule_evaluator = get_rule_evaluator( - list_store=ListStore( - [ - { - "name": "some-list", - "primary_person": primary_person_list_item_id, - "items": get_list_items(3), - } - ] + data_stores=DataStores( + list_store=ListStore( + [ + { + "name": "some-list", + "primary_person": primary_person_list_item_id, + "items": get_list_items(3), + } + ] + ) ), location=location, ) @@ -415,6 +482,7 @@ def test_current_location_source(list_item_id, expected_result): location=Location( section_id="some-section", block_id="some-block", list_item_id=list_item_id ), + data_stores=DataStores(), ) assert ( @@ -503,29 +571,33 @@ def test_current_location_source(list_item_id, expected_result): ) def test_nested_rules(operator, operands, expected_result): rule_evaluator = get_rule_evaluator( - answer_store=AnswerStore( - [ - { - "answer_id": "answer-1", - "list_item_id": "item-1", - "value": "Yes, I do", - }, - { - "answer_id": "answer-2", - "list_item_id": "item-1", - "value": 10, - }, - ] - ), - metadata={"region_code": "GB-NIR", "language_code": "en"}, - list_store=ListStore( - [ - { - "name": "some-list", - "items": get_list_items(5), - "same_name_items": get_list_items(3), - } - ], + data_stores=DataStores( + answer_store=AnswerStore( + [ + { + "answer_id": "answer-1", + "list_item_id": "item-1", + "value": "Yes, I do", + }, + { + "answer_id": "answer-2", + "list_item_id": "item-1", + "value": 10, + }, + ] + ), + metadata=get_metadata( + extra_metadata={"region_code": "GB-NIR", "language_code": "en"} + ), + list_store=ListStore( + [ + { + "name": "some-list", + "items": get_list_items(5), + "same_name_items": get_list_items(3), + } + ], + ), ), location=Location( section_id="some-section", block_id="some-block", list_item_id="item-1" @@ -556,7 +628,7 @@ def test_nested_rules(operator, operands, expected_result): ], ) def test_comparison_operator_rule_with_nonetype_operands(operator_name, operands): - rule_evaluator = get_rule_evaluator() + rule_evaluator = get_rule_evaluator(data_stores=DataStores(metadata=get_metadata())) assert rule_evaluator.evaluate(rule={operator_name: operands}) is False @@ -575,7 +647,9 @@ def test_comparison_operator_rule_with_nonetype_operands(operator_name, operands "operator_name", [Operator.ALL_IN, Operator.ANY_IN, Operator.IN] ) def test_array_operator_rule_with_nonetype_operands(operator_name, operands): - rule_evaluator = get_rule_evaluator() + rule_evaluator = get_rule_evaluator( + data_stores=DataStores(metadata=get_metadata()), + ) assert ( rule_evaluator.evaluate( rule={operator_name: operands}, @@ -698,15 +772,19 @@ def test_array_operator_rule_with_nonetype_operands(operator_name, operands): ) def test_date_value(rule, expected_result): rule_evaluator = get_rule_evaluator( - answer_store=AnswerStore( - [ - { - "answer_id": "some-answer", - "value": current_date_as_yyyy_mm_dd, - } - ] + data_stores=DataStores( + answer_store=AnswerStore( + [ + { + "answer_id": "some-answer", + "value": current_date_as_yyyy_mm_dd, + } + ] + ), + metadata=get_metadata( + extra_metadata={"some-metadata": current_date_as_yyyy_mm_dd} + ), ), - metadata={"some-metadata": current_date_as_yyyy_mm_dd}, ) assert ( @@ -720,12 +798,12 @@ def test_date_value(rule, expected_result): def test_answer_source_outside_of_repeating_section(): schema = get_mock_schema() - schema.is_repeating_answer = Mock(return_value=False) + schema.get_list_name_for_answer_id = Mock(return_value=None) answer_store = AnswerStore([{"answer_id": "some-answer", "value": "Yes"}]) rule_evaluator = get_rule_evaluator( schema=schema, - answer_store=answer_store, + data_stores=DataStores(answer_store=answer_store), location=Location( section_id="some-section", block_id="some-block", list_item_id="item-1" ), @@ -763,7 +841,7 @@ def test_answer_source_not_on_path_non_repeating_section(is_answer_on_path): rule_evaluator = get_rule_evaluator( schema=schema, - answer_store=AnswerStore([answer.to_dict()]), + data_stores=DataStores(answer_store=AnswerStore([answer.to_dict()])), location=location, routing_path_block_ids=["block-on-path"], ) @@ -784,7 +862,7 @@ def test_answer_source_not_on_path_non_repeating_section(is_answer_on_path): @pytest.mark.parametrize("is_answer_on_path", [True, False]) def test_answer_source_not_on_path_repeating_section(is_answer_on_path): schema = get_mock_schema() - schema.is_repeating_answer = Mock(return_value=True) + schema.get_list_name_for_answer_id = Mock(return_value="mock-list") location = Location( section_id="test-section", block_id="test-block", list_item_id="item-1" ) @@ -802,7 +880,7 @@ def test_answer_source_not_on_path_repeating_section(is_answer_on_path): rule_evaluator = get_rule_evaluator( schema=schema, - answer_store=AnswerStore([answer.to_dict()]), + data_stores=DataStores(answer_store=AnswerStore([answer.to_dict()])), location=location, routing_path_block_ids=["block-on-path"], ) @@ -831,7 +909,9 @@ def test_answer_source_default_answer_used_when_no_answer( rule_evaluator = get_rule_evaluator( schema=schema, - answer_store=AnswerStore([{"answer_id": "some-answer", "value": "No"}]), + data_stores=DataStores( + answer_store=AnswerStore([{"answer_id": "some-answer", "value": "No"}]) + ), ) assert ( @@ -849,8 +929,8 @@ def test_answer_source_default_answer_used_when_no_answer( def test_raises_exception_when_bad_operand_type(): with pytest.raises(TypeError): - rule_evaluator = get_rule_evaluator() - rule_evaluator.evaluate(rule={Operator.EQUAL: {1, 1}}) + rule_evaluator = get_rule_evaluator(data_stores=DataStores()) + rule_evaluator.evaluate(rule={Operator.EQUAL: {1, 2}}) @pytest.mark.parametrize( @@ -873,14 +953,16 @@ def test_raises_exception_when_bad_operand_type(): ) def test_answer_source_count(rule, expected_result): rule_evaluator = get_rule_evaluator( - answer_store=AnswerStore( - [ - { - "answer_id": "some-answer", - "value": ["array element 1", "array element 2"], - } - ] - ), + data_stores=DataStores( + answer_store=AnswerStore( + [ + { + "answer_id": "some-answer", + "value": ["array element 1", "array element 2"], + } + ] + ), + ) ) assert rule_evaluator.evaluate(rule=rule) is expected_result @@ -918,20 +1000,23 @@ def test_answer_source_count(rule, expected_result): ) def test_format_date(rule, expected_result): rule_evaluator = get_rule_evaluator( - answer_store=AnswerStore( - [ - { - "answer_id": "some-answer", - "value": "2021-01-01", - } - ] - ), + data_stores=DataStores( + answer_store=AnswerStore( + [ + { + "answer_id": "some-answer", + "value": "2021-01-01", + } + ] + ), + ) ) assert rule_evaluator.evaluate(rule=rule) == expected_result @freeze_time("2021-01-01") -def test_map_without_nested_date_operator(): +@pytest.mark.parametrize("source", ("response_metadata", "supplementary_data")) +def test_map_without_nested_date_operator(source): rule = { Operator.MAP: [ {Operator.FORMAT_DATE: ["self", "yyyy-MM-dd"]}, @@ -939,7 +1024,7 @@ def test_map_without_nested_date_operator(): Operator.DATE_RANGE: [ { Operator.DATE: [ - {"source": "response_metadata", "identifier": "started_at"}, + {"source": source, "identifier": "started_at"}, {"days": -7, "day_of_week": "MONDAY"}, ] }, @@ -949,8 +1034,12 @@ def test_map_without_nested_date_operator(): ] } + date_map = {"started_at": datetime.now(timezone.utc).isoformat()} rule_evaluator = get_rule_evaluator( - response_metadata={"started_at": datetime.now(timezone.utc).isoformat()} + data_stores=DataStores( + response_metadata=date_map, + supplementary_data_store=SupplementaryDataStore(date_map), + ) ) assert rule_evaluator.evaluate(rule=rule) == [ @@ -987,14 +1076,59 @@ def test_map_with_nested_date_operator(offset, expected_result): } rule_evaluator = get_rule_evaluator( - answer_store=AnswerStore( - [ - { - "answer_id": "checkbox-answer", - "value": ["2021-01-01", "2021-01-02", "2021-01-03"], - } - ] + data_stores=DataStores( + answer_store=AnswerStore( + [ + { + "answer_id": "checkbox-answer", + "value": ["2021-01-01", "2021-01-02", "2021-01-03"], + } + ] + ) ) ) assert rule_evaluator.evaluate(rule=rule) == expected_result + + +@pytest.mark.parametrize( + "identifier,selectors,value", + [ + ("note", ["title"], "Volume of total production"), + ("products", ["name"], "Articles and equipment for sports or outdoor games"), + ], +) +def test_supplementary_data_source( + supplementary_data_store_with_data, identifier, selectors, value +): + """Tests rule evaluation of repeating and non-repeating supplementary data source inside a repeat""" + schema = get_mock_schema() + schema.get_list_name_for_answer_id = Mock(return_value=None) + answer_store = AnswerStore([{"answer_id": "same-answer", "value": value}]) + + rule_evaluator = get_rule_evaluator( + schema=schema, + data_stores=DataStores( + answer_store=answer_store, + supplementary_data_store=supplementary_data_store_with_data, + ), + location=Location( + section_id="some-section", block_id="some-block", list_item_id="item-1" + ), + ) + + assert ( + rule_evaluator.evaluate( + rule={ + Operator.EQUAL: [ + { + "source": "supplementary_data", + "identifier": identifier, + "selectors": selectors, + }, + {"source": "answers", "identifier": "same-answer"}, + ] + } + ) + is True + ) diff --git a/tests/app/questionnaire/test_date_rules.py b/tests/app/questionnaire/test_date_rules.py deleted file mode 100644 index fbf7a290ab..0000000000 --- a/tests/app/questionnaire/test_date_rules.py +++ /dev/null @@ -1,132 +0,0 @@ -from datetime import datetime, timezone - -import pytest - -from app.data_models.answer_store import Answer, AnswerStore -from app.data_models.list_store import ListStore -from app.questionnaire.when_rules import evaluate_date_rule, evaluate_goto - - -@pytest.mark.parametrize( - "date, condition, comparison, expected", - ( - ( - datetime.now(tz=timezone.utc).strftime("%Y-%m-%d"), - "equals", - {"value": "now"}, - True, - ), - ("2000-01-01", "equals", {"value": "now"}, False), - ( - "2020-05-01", - "equals", - { - "value": "2019-03-31", - "offset_by": {"days": 1, "months": 1, "years": 1}, - }, - True, - ), - ( - "2020-02-29", - "equals", - { - "value": "2021-04-01", - "offset_by": {"days": -1, "months": -1, "years": -1}, - }, - True, - ), - ( - "2018-02", - "not equals", - {"value": "2018-01"}, - True, - ), - ( - "2018-01", - "not equals", - {"value": "2018-01"}, - False, - ), - ( - "2018-01", - "not equals", - {"value": "2018-01"}, - False, - ), - ( - "2016-06-11", - "less than", - {"meta": "return_by"}, - True, - ), - ( - "2016-06-12", - "less than", - {"meta": "return_by"}, - False, - ), - ( - "2018-02-04", - "greater than", - {"id": "compare_date_answer"}, - True, - ), - ( - "2018-02-03", - "greater than", - {"id": "compare_date_answer"}, - False, - ), - ( - "2018-02-03", - "greater than", - {"id": "non_existent_answer"}, - False, - ), - ), -) -def test_evaluate_date_rule_equals_with_value( - date, condition, comparison, expected, questionnaire_schema -): - when = { - "id": "date-answer", - "condition": condition, - "date_comparison": comparison, - } - metadata = {"return_by": "2016-06-12"} - answer_store = AnswerStore({}) - answer_store.add_or_update( - Answer(answer_id="compare_date_answer", value="2018-02-03") - ) - - assert ( - evaluate_date_rule(when, answer_store, questionnaire_schema, metadata, date) - is expected - ) - - -def test_do_not_go_to_next_question_for_date_answer( - current_location, questionnaire_schema -): - goto_rule = { - "id": "next-question", - "when": [ - { - "id": "date-answer", - "condition": "equals", - "date_comparison": {"value": "2018-01"}, - } - ], - } - - answer_store = AnswerStore({}) - answer_store.add_or_update(Answer(answer_id="date-answer", value="2018-02-01")) - - assert not evaluate_goto( - goto_rule=goto_rule, - schema=questionnaire_schema, - metadata={}, - answer_store=answer_store, - list_store=ListStore(), - current_location=current_location, - ) diff --git a/tests/app/questionnaire/test_dynamic_answer_options.py b/tests/app/questionnaire/test_dynamic_answer_options.py index 78bcad596f..5a3db0196b 100644 --- a/tests/app/questionnaire/test_dynamic_answer_options.py +++ b/tests/app/questionnaire/test_dynamic_answer_options.py @@ -1,8 +1,8 @@ # pylint: disable=redefined-outer-name import pytest -from app.data_models import AnswerStore, ListStore from app.data_models.answer_store import Answer +from app.data_models.data_stores import DataStores from app.questionnaire.dynamic_answer_options import DynamicAnswerOptions from app.questionnaire.rules.rule_evaluator import RuleEvaluator from app.questionnaire.value_source_resolver import ValueSourceResolver @@ -11,10 +11,7 @@ @pytest.fixture def rule_evaluator(mock_schema, response_metadata): evaluator = RuleEvaluator( - answer_store=AnswerStore(), - list_store=ListStore(), - metadata={}, - response_metadata=response_metadata, + data_stores=DataStores(response_metadata=response_metadata), schema=mock_schema, location=None, ) @@ -25,10 +22,7 @@ def rule_evaluator(mock_schema, response_metadata): @pytest.fixture def value_source_resolver(mock_schema, response_metadata): resolver = ValueSourceResolver( - answer_store=AnswerStore(), - list_store=ListStore(), - metadata={}, - response_metadata=response_metadata, + data_stores=DataStores(response_metadata=response_metadata), schema=mock_schema, location=None, list_item_id=None, @@ -40,7 +34,6 @@ def value_source_resolver(mock_schema, response_metadata): def test_dynamic_answer_options(rule_evaluator, value_source_resolver): - dynamic_options = DynamicAnswerOptions( { "values": { @@ -116,9 +109,13 @@ def test_dynamic_answer_options_answer_source( mock_schema.get_answers_by_answer_id = mocker.Mock(return_value=answer_schema) mock_schema.get_default_answer = mocker.Mock(return_value=None) + mock_schema.is_answer_dynamic = mocker.Mock(return_value=False) + mock_schema.is_answer_in_list_collector_repeating_block = mocker.Mock( + return_value=False + ) if checkbox_answer: - value_source_resolver.answer_store.add_or_update( + value_source_resolver.data_stores.answer_store.add_or_update( Answer(answer_id="injury-sustained-answer", value=checkbox_answer) ) diff --git a/tests/app/questionnaire/test_path_finder.py b/tests/app/questionnaire/test_path_finder.py index 0c1c9096f7..f47562f025 100644 --- a/tests/app/questionnaire/test_path_finder.py +++ b/tests/app/questionnaire/test_path_finder.py @@ -1,100 +1,71 @@ import pytest -from app.data_models import ListStore +from app.data_models import CompletionStatus, ListStore from app.data_models.answer_store import Answer, AnswerStore -from app.data_models.progress_store import CompletionStatus, ProgressStore +from app.data_models.data_stores import DataStores +from app.data_models.progress import ProgressDict +from app.data_models.progress_store import ProgressStore from app.questionnaire.path_finder import PathFinder from app.questionnaire.routing_path import RoutingPath from app.utilities.schema import load_schema_from_name +from app.utilities.types import SectionKey +from tests.app.questionnaire.conftest import get_metadata -def test_simple_path(answer_store, list_store): +def test_simple_path(): schema = load_schema_from_name("test_textfield") progress_store = ProgressStore( [ - { - "section_id": "default-section", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["name-block"], - } + ProgressDict( + section_id="default-section", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["name-block"], + ) ] ) - path_finder = PathFinder(schema, answer_store, list_store, progress_store, {}, {}) + path_finder = PathFinder( + schema, data_stores=DataStores(progress_store=progress_store) + ) section_id = schema.get_section_id_for_block_id("name-block") - routing_path = path_finder.routing_path(section_id=section_id) + routing_path = path_finder.routing_path(SectionKey(section_id)) - assumed_routing_path = RoutingPath(["name-block"], section_id="default-section") + assumed_routing_path = RoutingPath( + block_ids=["name-block"], section_id="default-section" + ) assert routing_path == assumed_routing_path -def test_introduction_in_path_when_in_schema(answer_store, list_store, progress_store): +def test_introduction_in_path_when_in_schema(data_stores): schema = load_schema_from_name("test_introduction") current_section = schema.get_section("introduction-section") - path_finder = PathFinder(schema, answer_store, list_store, progress_store, {}, {}) + path_finder = PathFinder(schema, data_stores=data_stores) - routing_path = path_finder.routing_path(section_id=current_section["id"]) + routing_path = path_finder.routing_path(SectionKey(current_section["id"])) assert "introduction" in routing_path -def test_introduction_not_in_path_when_not_in_schema( - answer_store, list_store, progress_store, mocker -): +def test_introduction_not_in_path_when_not_in_schema(data_stores): schema = load_schema_from_name("test_checkbox") current_section = schema.get_section("default-section") - path_finder = PathFinder(schema, answer_store, list_store, progress_store, {}, {}) + path_finder = PathFinder(schema, data_stores=data_stores) - mocker.patch("app.questionnaire.when_rules.evaluate_when_rules", return_value=False) - routing_path = path_finder.routing_path(section_id=current_section["id"]) + routing_path = path_finder.routing_path(SectionKey(current_section["id"])) assert "introduction" not in routing_path -@pytest.mark.parametrize( - "schema", - ( - "test_new_routing_number_equals", - "test_routing_number_equals", - ), -) -def test_routing_path_with_conditional_path(schema, answer_store, list_store): - schema = load_schema_from_name(schema) - section_id = schema.get_section_id_for_block_id("number-question") - expected_path = RoutingPath( - ["number-question", "correct-answer"], - section_id="default-section", - ) - - answer = Answer(answer_id="answer", value=123) - answer_store.add_or_update(answer) - progress_store = ProgressStore( - [ - { - "section_id": "default-section", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["number-question"], - } - ] - ) - path_finder = PathFinder(schema, answer_store, list_store, progress_store, {}, {}) - - routing_path = path_finder.routing_path(section_id=section_id) - - assert routing_path == expected_path - - -def test_new_routing_basic_and_conditional_path( - answer_store, list_store, progress_store +def test_routing_basic_and_conditional_path( + answer_store, ): # Given - schema = load_schema_from_name("test_new_routing_number_equals") + schema = load_schema_from_name("test_routing_number_equals") section_id = schema.get_section_id_for_block_id("number-question") expected_path = RoutingPath( - ["number-question", "correct-answer"], + block_ids=["number-question", "correct-answer"], section_id="default-section", ) @@ -103,42 +74,54 @@ def test_new_routing_basic_and_conditional_path( answer_store.add_or_update(answer_1) # When - path_finder = PathFinder(schema, answer_store, list_store, progress_store, {}, {}) - routing_path = path_finder.routing_path(section_id=section_id) + path_finder = PathFinder(schema, data_stores=DataStores(answer_store=answer_store)) + routing_path = path_finder.routing_path(SectionKey(section_id)) # Then assert routing_path == expected_path -def test_routing_path_with_complete_introduction(answer_store, list_store): +def test_routing_path_with_complete_introduction(): schema = load_schema_from_name("test_introduction") section_id = schema.get_section_id_for_block_id("introduction") progress_store = ProgressStore( [ - { - "section_id": "introduction-section", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["introduction"], - } + ProgressDict( + section_id="introduction-section", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["introduction"], + ) ] ) expected_routing_path = RoutingPath( - ["introduction", "general-business-information-completed"], + block_ids=[ + "introduction", + "report-radio", + "reporting-date", + "report-radio-second", + "projects-checkbox", + "turnover-variants-block", + "address-mutually-exclusive-checkbox", + "further-details-text-area", + "general-business-information-completed", + ], section_id="introduction-section", ) - path_finder = PathFinder(schema, answer_store, list_store, progress_store, {}, {}) - routing_path = path_finder.routing_path(section_id=section_id) + path_finder = PathFinder( + schema, data_stores=DataStores(progress_store=progress_store) + ) + routing_path = path_finder.routing_path(SectionKey(section_id)) assert routing_path == expected_routing_path -def test_routing_path(answer_store, list_store): +def test_routing_path(): schema = load_schema_from_name("test_submit_with_summary") section_id = schema.get_section_id_for_block_id("dessert") expected_path = RoutingPath( - ["radio", "dessert", "dessert-confirmation", "numbers"], + block_ids=["radio", "dessert", "dessert-confirmation", "numbers"], section_id="default-section", ) @@ -157,13 +140,15 @@ def test_routing_path(answer_store, list_store): } ] ) - path_finder = PathFinder(schema, answer_store, list_store, progress_store, {}, {}) - routing_path = path_finder.routing_path(section_id=section_id) + path_finder = PathFinder( + schema, data_stores=DataStores(progress_store=progress_store) + ) + routing_path = path_finder.routing_path(SectionKey(section_id)) assert routing_path == expected_path -def test_routing_path_with_repeating_sections(answer_store, list_store): +def test_routing_path_with_repeating_sections(): schema = load_schema_from_name("test_repeating_sections_with_hub_and_spoke") progress_store = ProgressStore( @@ -180,15 +165,15 @@ def test_routing_path_with_repeating_sections(answer_store, list_store): } ] ) - path_finder = PathFinder(schema, answer_store, list_store, progress_store, {}, {}) + path_finder = PathFinder( + schema, data_stores=DataStores(progress_store=progress_store) + ) repeating_section_id = "personal-details-section" - routing_path = path_finder.routing_path( - section_id=repeating_section_id, list_item_id="abc123" - ) + routing_path = path_finder.routing_path(SectionKey(repeating_section_id, "abc123")) expected_path = RoutingPath( - ["proxy", "date-of-birth", "confirm-dob", "sex"], + block_ids=["proxy", "date-of-birth", "confirm-dob", "sex"], section_id="personal-details-section", list_name="people", list_item_id="abc123", @@ -197,11 +182,11 @@ def test_routing_path_with_repeating_sections(answer_store, list_store): assert routing_path == expected_path -def test_routing_path_empty_routing_rules(answer_store, list_store): +def test_routing_path_empty_routing_rules(answer_store): schema = load_schema_from_name("test_checkbox") section_id = schema.get_section_id_for_block_id("mandatory-checkbox") expected_path = RoutingPath( - ["mandatory-checkbox", "non-mandatory-checkbox", "single-checkbox"], + block_ids=["mandatory-checkbox", "non-mandatory-checkbox", "single-checkbox"], section_id="default-section", ) @@ -215,57 +200,61 @@ def test_routing_path_empty_routing_rules(answer_store, list_store): progress_store = ProgressStore( [ - { - "section_id": "default-section", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["mandatory-checkbox"], - } + ProgressDict( + section_id="default-section", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["mandatory-checkbox"], + ) ] ) - path_finder = PathFinder(schema, answer_store, list_store, progress_store, {}, {}) - routing_path = path_finder.routing_path(section_id=section_id) + path_finder = PathFinder( + schema, + data_stores=DataStores( + progress_store=progress_store, answer_store=answer_store + ), + ) + routing_path = path_finder.routing_path(SectionKey(section_id)) assert routing_path == expected_path -def test_routing_path_with_conditional_value_not_in_metadata(answer_store, list_store): +def test_routing_path_with_conditional_value_not_in_metadata(answer_store): schema = load_schema_from_name("test_metadata_routing") section_id = schema.get_section_id_for_block_id("block1") expected_path = RoutingPath( - ["block1", "block2", "block3"], section_id="default-section" + block_ids=["block1", "block2", "block3"], section_id="default-section" ) progress_store = ProgressStore( [ - { - "section_id": "default-section", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["block1"], - } + ProgressDict( + section_id="default-section", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["block1"], + ) ] ) - path_finder = PathFinder(schema, answer_store, list_store, progress_store, {}, {}) - routing_path = path_finder.routing_path(section_id=section_id) + path_finder = PathFinder( + schema, + data_stores=DataStores( + progress_store=progress_store, + answer_store=answer_store, + metadata=get_metadata(), + ), + ) + + routing_path = path_finder.routing_path(SectionKey(section_id)) assert routing_path == expected_path -@pytest.mark.parametrize( - "schema, expected_routing_path_ids", - ( - ("test_new_skip_condition_block", ["do-you-want-to-skip"]), - ("test_skip_condition_block", ["do-you-want-to-skip", "a-non-skipped-block"]), - ), -) -def test_new_routing_path_should_skip_block( - schema, expected_routing_path_ids, answer_store, list_store -): +def test_routing_path_should_skip_block(answer_store): # Given - schema = load_schema_from_name(schema) + schema = load_schema_from_name("test_skip_condition_block") section_id = schema.get_section_id_for_block_id("should-skip") answer_store.add_or_update( Answer(answer_id="do-you-want-to-skip-answer", value="Yes") @@ -273,38 +262,37 @@ def test_new_routing_path_should_skip_block( progress_store = ProgressStore( [ - { - "section_id": "introduction-section", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["do-you-want-to-skip"], - } + ProgressDict( + section_id="introduction-section", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["do-you-want-to-skip"], + ) ] ) # When - path_finder = PathFinder(schema, answer_store, list_store, progress_store, {}, {}) - routing_path = path_finder.routing_path(section_id=section_id) + path_finder = PathFinder( + schema, + data_stores=DataStores( + progress_store=progress_store, answer_store=answer_store + ), + ) + routing_path = path_finder.routing_path(SectionKey(section_id)) # Then + expected_routing_path_ids = ["do-you-want-to-skip"] expected_routing_path = RoutingPath( - expected_routing_path_ids, + block_ids=expected_routing_path_ids, section_id="default-section", ) assert routing_path == expected_routing_path -@pytest.mark.parametrize( - "schema", - ( - "test_skip_condition_group", - "test_new_skip_condition_group", - ), -) -def test_routing_path_should_skip_group(schema, answer_store, list_store): +def test_routing_path_should_skip_group(answer_store): # Given - schema = load_schema_from_name(schema) + schema = load_schema_from_name("test_skip_condition_group") section_id = schema.get_section_id_for_block_id("do-you-want-to-skip") answer_store.add_or_update( @@ -312,38 +300,36 @@ def test_routing_path_should_skip_group(schema, answer_store, list_store): ) progress_store = ProgressStore( [ - { - "section_id": "default-section", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["do-you-want-to-skip"], - } + ProgressDict( + section_id="default-section", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["do-you-want-to-skip"], + ) ] ) # When - path_finder = PathFinder(schema, answer_store, list_store, progress_store, {}, {}) - routing_path = path_finder.routing_path(section_id=section_id) + path_finder = PathFinder( + schema, + data_stores=DataStores( + progress_store=progress_store, answer_store=answer_store + ), + ) + routing_path = path_finder.routing_path(SectionKey(section_id)) # Then expected_routing_path = RoutingPath( - ["do-you-want-to-skip"], + block_ids=["do-you-want-to-skip"], section_id="default-section", ) assert routing_path == expected_routing_path -@pytest.mark.parametrize( - "schema", - ( - "test_skip_condition_group", - "test_new_skip_condition_group", - ), -) -def test_routing_path_should_not_skip_group(schema, answer_store, list_store): +def test_routing_path_should_not_skip_group(answer_store): # Given - schema = load_schema_from_name(schema) + schema = load_schema_from_name("test_skip_condition_group") section_id = schema.get_section_id_for_block_id("do-you-want-to-skip") answer_store.add_or_update( @@ -351,22 +337,27 @@ def test_routing_path_should_not_skip_group(schema, answer_store, list_store): ) progress_store = ProgressStore( [ - { - "section_id": "default-section", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["do-you-want-to-skip"], - } + ProgressDict( + section_id="default-section", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["do-you-want-to-skip"], + ) ] ) # When - path_finder = PathFinder(schema, answer_store, list_store, progress_store, {}, {}) - routing_path = path_finder.routing_path(section_id=section_id) + path_finder = PathFinder( + schema, + data_stores=DataStores( + progress_store=progress_store, answer_store=answer_store + ), + ) + routing_path = path_finder.routing_path(SectionKey(section_id)) # Then expected_routing_path = RoutingPath( - ["do-you-want-to-skip", "should-skip"], + block_ids=["do-you-want-to-skip", "should-skip"], section_id="default-section", ) @@ -374,7 +365,7 @@ def test_routing_path_should_not_skip_group(schema, answer_store, list_store): def test_get_routing_path_when_first_block_in_group_skipped( - answer_store, list_store, progress_store + answer_store, ): # Given schema = load_schema_from_name("test_skip_condition_group") @@ -383,7 +374,7 @@ def test_get_routing_path_when_first_block_in_group_skipped( ) # When - path_finder = PathFinder(schema, answer_store, list_store, progress_store, {}, {}) + path_finder = PathFinder(schema, data_stores=DataStores(answer_store=answer_store)) # Then expected_route = RoutingPath( @@ -391,27 +382,29 @@ def test_get_routing_path_when_first_block_in_group_skipped( block_ids=["do-you-want-to-skip"], ) - assert expected_route == path_finder.routing_path(section_id="default-section") + assert expected_route == path_finder.routing_path(SectionKey("default-section")) -def test_build_path_with_group_routing(answer_store, list_store, progress_store): +def test_build_path_with_group_routing( + answer_store, +): # Given i have answered the routing question - schema = load_schema_from_name("test_new_routing_group") + schema = load_schema_from_name("test_routing_group") section_id = schema.get_section_id_for_block_id("group2-block") answer_store.add_or_update(Answer(answer_id="which-group-answer", value="group2")) # When i build the path - path_finder = PathFinder(schema, answer_store, list_store, progress_store, {}, {}) - path = path_finder.routing_path(section_id=section_id) + path_finder = PathFinder(schema, data_stores=DataStores(answer_store=answer_store)) + path = path_finder.routing_path(SectionKey(section_id)) # Then it should route me straight to Group2 and not Group1 assert "group1-block" not in path assert "group2-block" in path -def test_remove_answer_and_block_if_routing_backwards(list_store): - schema = load_schema_from_name("test_confirmation_question") +def test_remove_answer_and_block_if_routing_backwards(): + schema = load_schema_from_name("test_confirmation_question_backwards_routing") section_id = schema.get_section_id_for_block_id("confirm-zero-employees-block") # All blocks completed @@ -421,119 +414,70 @@ def test_remove_answer_and_block_if_routing_backwards(list_store): "section_id": "default-section", "list_item_id": None, "status": CompletionStatus.COMPLETED, - "block_ids": [ - "number-of-employees-total-block", - "confirm-zero-employees-block", - ], - } - ] - ) - - number_of_employees_answer = Answer(answer_id="number-of-employees-total", value=0) - confirm_zero_answer = Answer( - answer_id="confirm-zero-employees-answer", value="No I need to change this" - ) - answer_store = AnswerStore({}) - answer_store.add_or_update(number_of_employees_answer) - answer_store.add_or_update(confirm_zero_answer) - - path_finder = PathFinder(schema, answer_store, list_store, progress_store, {}, {}) - - assert ( - len( - path_finder.progress_store.get_completed_block_ids( - section_id="default-section" - ) - ) - == 2 - ) - assert len(path_finder.answer_store) == 2 - - routing_path = path_finder.routing_path(section_id=section_id) - - expected_path = RoutingPath( - [ - "number-of-employees-total-block", - "confirm-zero-employees-block", - "number-of-employees-total-block", - ], - section_id="default-section", - ) - assert routing_path == expected_path - - assert path_finder.progress_store.get_completed_block_ids( - section_id="default-section" - ) == [progress_store.get_completed_block_ids(section_id="default-section")[0]] - - assert len(path_finder.answer_store) == 1 - assert not path_finder.answer_store.get_answer("confirm-zero-employees-answer") - - assert ( - progress_store.get_section_status(section_id="default-section") - == CompletionStatus.IN_PROGRESS - ) - - -def test_new_remove_answer_and_block_if_routing_backwards(list_store): - schema = load_schema_from_name("test_new_confirmation_question") - section_id = schema.get_section_id_for_block_id("confirm-zero-employees-block") - - # All blocks completed - progress_store = ProgressStore( - [ + "block_ids": ["route-backwards-block"], + }, { - "section_id": "default-section", + "section_id": "section-2", "list_item_id": None, "status": CompletionStatus.COMPLETED, "block_ids": [ - "route-backwards-block", "number-of-employees-total-block", "confirm-zero-employees-block", ], - } + }, ] ) answer_store = AnswerStore() route_backwards_answer = Answer(answer_id="route-backwards-answer", value="Yes") number_of_employees_answer = Answer(answer_id="number-of-employees-total", value=0) - confirm_zero_answer = Answer(answer_id="confirm-zero-employees-answer", value="No") + confirm_zero_answer = Answer( + answer_id="confirm-zero-employees-answer", value="No I need to correct this" + ) answer_store.add_or_update(route_backwards_answer) answer_store.add_or_update(number_of_employees_answer) answer_store.add_or_update(confirm_zero_answer) - path_finder = PathFinder(schema, answer_store, list_store, progress_store, {}, {}) + path_finder = PathFinder( + schema, + data_stores=DataStores( + progress_store=progress_store, answer_store=answer_store + ), + ) assert ( len( - path_finder.progress_store.get_completed_block_ids( - section_id="default-section" + path_finder.data_stores.progress_store.get_completed_block_ids( + SectionKey("section-2") ) ) - == 3 + == 2 ) - assert len(path_finder.answer_store) == 3 + assert len(path_finder.data_stores.answer_store) == 3 - routing_path = path_finder.routing_path(section_id=section_id) + routing_path = path_finder.routing_path(SectionKey(section_id)) expected_path = RoutingPath( - [ - "route-backwards-block", + block_ids=[ "number-of-employees-total-block", "confirm-zero-employees-block", "number-of-employees-total-block", ], - section_id="default-section", + section_id="section-2", ) assert routing_path == expected_path - assert path_finder.progress_store.get_completed_block_ids( - section_id="default-section" - ) == progress_store.get_completed_block_ids(section_id="default-section") + assert path_finder.data_stores.progress_store.get_completed_block_ids( + SectionKey("section-2") + ) == progress_store.get_completed_block_ids(SectionKey("section-2")) - assert len(path_finder.answer_store) == 2 - assert not path_finder.answer_store.get_answer("confirm-zero-employees-answer") + assert len(path_finder.data_stores.answer_store) == 2 + assert not path_finder.data_stores.answer_store.get_answer( + "confirm-zero-employees-answer" + ) assert ( - path_finder.progress_store.get_section_status(section_id="default-section") + path_finder.data_stores.progress_store.get_section_status( + SectionKey("section-2") + ) == CompletionStatus.IN_PROGRESS ) @@ -589,7 +533,8 @@ def test_new_remove_answer_and_block_if_routing_backwards(list_store): "primary-person", ["name-block", "age"], ), - ( # Answering 'Yes' to the skip age question and the skip-confirmation question, but then changing you answer for the skip age question to 'No' + ( + # Answering 'Yes' to the skip age question and the skip-confirmation question, but then changing you answer for the skip age question to 'No' # means because confirmation is not longer on the path in primary-person you will be asked your age, name and why you didn't confirm skipping "No", "Yes", @@ -598,37 +543,26 @@ def test_new_remove_answer_and_block_if_routing_backwards(list_store): ), ), ) -@pytest.mark.parametrize( - "schema_name", - ( - [ - "test_new_routing_and_skipping_section_dependencies", - "test_routing_and_skipping_section_dependencies", - ] - ), -) def test_routing_path_block_ids_dependent_on_other_sections_when_rules( - list_store, skip_age_answer, skip_confirmation_answer, - schema_name, section_id, expected_route, answer_store, ): # Given a schema which has when rules in a section which has dependencies on other sections answers - schema = load_schema_from_name(schema_name) + schema = load_schema_from_name("test_routing_and_skipping_section_dependencies") answer_store.add_or_update( Answer(answer_id="skip-age-answer", value=skip_age_answer) ) progress = [ - { - "section_id": "skip-section", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["skip-age"], - } + ProgressDict( + section_id="skip-section", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["skip-age"], + ) ] if skip_confirmation_answer: @@ -638,12 +572,12 @@ def test_routing_path_block_ids_dependent_on_other_sections_when_rules( answer_store.add_or_update(Answer(answer_id="security-answer", value="Yes")) progress.append( - { - "section_id": "skip-confirmation-section", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["security", "skip-confirmation"], - } + ProgressDict( + section_id="skip-confirmation-section", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["security", "skip-confirmation"], + ) ) progress_store = ProgressStore(progress) @@ -651,17 +585,15 @@ def test_routing_path_block_ids_dependent_on_other_sections_when_rules( # When I build the path path_finder = PathFinder( schema, - answer_store, - list_store, - progress_store, - metadata={}, - response_metadata={}, + data_stores=DataStores( + progress_store=progress_store, answer_store=answer_store + ), ) - routing_path = path_finder.routing_path(section_id=section_id) + routing_path = path_finder.routing_path(SectionKey(section_id)) # Then the path is built correctly expected_routing_path = RoutingPath( - expected_route, + block_ids=expected_route, section_id=section_id, ) assert routing_path == expected_routing_path @@ -674,30 +606,26 @@ def test_routing_path_block_ids_dependent_on_other_sections_when_rules( # Answering 'Yes' to the skip age question # means in all repeating sections you won't be asked their age "Yes", - ["repeating-sex"], + ["repeating-sex", "repeating-is-dependent"], ), ( # Answering 'No' to the skip age question # means in all repeating sections you will be asked their age "No", - ["repeating-sex", "repeating-age"], + [ + "repeating-sex", + "repeating-age", + "repeating-is-dependent", + "repeating-is-smoker", + ], ), ), ) -@pytest.mark.parametrize( - "schema_name", - ( - [ - "test_new_routing_and_skipping_section_dependencies", - "test_routing_and_skipping_section_dependencies", - ] - ), -) def test_routing_path_block_ids_dependent_on_other_sections_when_rules_repeating( - skip_age_answer, schema_name, expected_route, answer_store + skip_age_answer, expected_route, answer_store ): # Given a schema with repeating sections which has when rules dependent on another section - schema = load_schema_from_name(schema_name) + schema = load_schema_from_name("test_routing_and_skipping_section_dependencies") answer_store.add_or_update( Answer(answer_id="skip-age-answer", value=skip_age_answer) ) @@ -714,37 +642,39 @@ def test_routing_path_block_ids_dependent_on_other_sections_when_rules_repeating progress_store = ProgressStore( [ - { - "section_id": "skip-section", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["skip-age"], - }, - { - "section_id": "household-section", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["list-collector"], - }, + ProgressDict( + section_id="skip-section", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["skip-age"], + ), + ProgressDict( + section_id="household-section", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["list-collector"], + ), ] ) # When I build the path path_finder = PathFinder( schema, - answer_store, - list_store, - progress_store, - metadata={}, - response_metadata={}, + data_stores=DataStores( + progress_store=progress_store, + answer_store=answer_store, + list_store=list_store, + ), ) routing_path = path_finder.routing_path( - section_id="household-personal-details-section", list_item_id="lCIZsS" + SectionKey( + section_id="household-personal-details-section", list_item_id="lCIZsS" + ) ) # Then the path is built correctly expected_routing_path = RoutingPath( - expected_route, + block_ids=expected_route, section_id="household-personal-details-section", list_item_id="lCIZsS", list_name="people", diff --git a/tests/app/questionnaire/test_placeholder_parser.py b/tests/app/questionnaire/test_placeholder_parser.py index 22fc2c65fc..32c4d5540f 100644 --- a/tests/app/questionnaire/test_placeholder_parser.py +++ b/tests/app/questionnaire/test_placeholder_parser.py @@ -1,9 +1,19 @@ -from unittest.mock import Mock +from decimal import Decimal +import pytest +from mock import Mock + +from app.data_models import ProgressStore from app.data_models.answer_store import AnswerStore +from app.data_models.data_stores import DataStores from app.data_models.list_store import ListStore +from app.data_models.progress import CompletionStatus, ProgressDict +from app.questionnaire import Location from app.questionnaire.placeholder_parser import PlaceholderParser -from app.questionnaire.questionnaire_schema import QuestionnaireSchema +from app.utilities.schema import load_schema_from_name +from tests.app.questionnaire.conftest import get_metadata + +# pylint: disable=too-many-lines def test_parse_placeholders(placeholder_list, parser): @@ -14,30 +24,35 @@ def test_parse_placeholders(placeholder_list, parser): assert placeholders["first_name"] == "Joe" -def test_metadata_placeholder(mock_renderer): +def test_metadata_placeholder(mock_renderer, mock_schema, mock_location): placeholder_list = [ { "placeholder": "period", - "value": {"source": "metadata", "identifier": "period_str"}, + "value": { + "source": "metadata", + "identifier": "period_str", + }, } ] period_str = "Aug 2018" + + metadata = get_metadata(extra_metadata={"period_str": period_str}) parser = PlaceholderParser( language="en", - answer_store=AnswerStore(), - list_store=ListStore(), - metadata={"period_str": period_str}, - response_metadata={}, - schema=QuestionnaireSchema({}), + data_stores=DataStores(metadata=metadata), + schema=mock_schema, renderer=mock_renderer, + location=mock_location, ) placeholders = parser(placeholder_list) assert period_str == placeholders["period"] -def test_previous_answer_transform_placeholder(mock_renderer): +def test_previous_answer_transform_placeholder(mock_renderer, mock_location): + schema = load_schema_from_name("test_placeholder_transform") + placeholder_list = [ { "placeholder": "total_turnover", @@ -48,7 +63,7 @@ def test_previous_answer_transform_placeholder(mock_renderer): "number": { "source": "answers", "identifier": "total-retail-turnover-answer", - } + }, }, } ], @@ -63,19 +78,17 @@ def test_previous_answer_transform_placeholder(mock_renderer): parser = PlaceholderParser( language="en", - answer_store=answer_store, - list_store=ListStore(), - metadata={}, - response_metadata={}, - schema=QuestionnaireSchema({}), + data_stores=DataStores(answer_store=answer_store), + schema=schema, renderer=mock_renderer, + location=mock_location, ) placeholders = parser(placeholder_list) assert placeholders["total_turnover"] == "ÂŖ1,000.00" -def test_metadata_transform_placeholder(mock_renderer): +def test_metadata_transform_placeholder(mock_renderer, mock_schema, mock_location): placeholder_list = [ { "placeholder": "start_date", @@ -94,21 +107,23 @@ def test_metadata_transform_placeholder(mock_renderer): } ] + metadata = get_metadata(extra_metadata={"ref_p_start_date": "2019-02-11"}) + parser = PlaceholderParser( language="en", - answer_store=AnswerStore(), - list_store=ListStore(), - metadata={"ref_p_start_date": "2019-02-11"}, - response_metadata={}, - schema=QuestionnaireSchema({}), + data_stores=DataStores(metadata=metadata), + schema=mock_schema, renderer=mock_renderer, + location=mock_location, ) placeholders = parser(placeholder_list) assert placeholders["start_date"] == "Monday 11 February 2019" -def test_response_metadata_transform_placeholder(mock_renderer): +def test_response_metadata_transform_placeholder( + mock_renderer, mock_schema, mock_location +): # This test should use ISO format dates when they become supported placeholder_list = [ { @@ -128,21 +143,24 @@ def test_response_metadata_transform_placeholder(mock_renderer): } ] + metadata = get_metadata(extra_metadata={"ref_p_start_date": "2019-02-11"}) + response_metadata = {"started_at": "2019-02-11"} + parser = PlaceholderParser( language="en", - answer_store=AnswerStore(), - list_store=ListStore(), - metadata={"ref_p_start_date": "2019-02-11"}, - response_metadata={"started_at": "2019-02-11"}, - schema=QuestionnaireSchema({}), + data_stores=DataStores(metadata=metadata, response_metadata=response_metadata), + schema=mock_schema, renderer=mock_renderer, + location=mock_location, ) placeholders = parser(placeholder_list) assert placeholders["start_date"] == "Monday 11 February 2019" -def test_multiple_answer_transform_placeholder(mock_renderer): +def test_multiple_answer_transform_placeholder( + mock_renderer, mock_schema, mock_location +): placeholder_list = [ { "placeholder": "persons_name", @@ -161,19 +179,19 @@ def test_multiple_answer_transform_placeholder(mock_renderer): } ] + answer_store = AnswerStore( + [ + {"answer_id": "first-name", "value": "Joe"}, + {"answer_id": "last-name", "value": "Bloggs"}, + ] + ) + parser = PlaceholderParser( language="en", - answer_store=AnswerStore( - [ - {"answer_id": "first-name", "value": "Joe"}, - {"answer_id": "last-name", "value": "Bloggs"}, - ] - ), - list_store=ListStore(), - metadata={}, - response_metadata={}, - schema=QuestionnaireSchema({}), + data_stores=DataStores(answer_store=answer_store), + schema=mock_schema, renderer=mock_renderer, + location=mock_location, ) placeholders = parser(placeholder_list) @@ -181,7 +199,9 @@ def test_multiple_answer_transform_placeholder(mock_renderer): assert placeholders["persons_name"] == "Joe Bloggs" -def test_first_non_empty_item_transform_placeholder(mock_renderer): +def test_first_non_empty_item_transform_placeholder( + mock_renderer, mock_schema, mock_location +): placeholder_list = [ { "placeholder": "company_name", @@ -199,14 +219,14 @@ def test_first_non_empty_item_transform_placeholder(mock_renderer): } ] + metadata = get_metadata(extra_metadata={"trad_as": None, "ru_name": "ru_name"}) + parser = PlaceholderParser( language="en", - answer_store=AnswerStore(), - list_store=ListStore(), - metadata={"ru_name": "ru_name"}, - response_metadata={}, - schema=QuestionnaireSchema({}), + data_stores=DataStores(metadata=metadata), + schema=mock_schema, renderer=mock_renderer, + location=mock_location, ) placeholders = parser(placeholder_list) @@ -214,7 +234,9 @@ def test_first_non_empty_item_transform_placeholder(mock_renderer): assert placeholders["company_name"] == "ru_name" -def test_format_list_answer_transform_placeholder(mock_renderer): +def test_format_list_answer_transform_placeholder( + mock_renderer, mock_schema, mock_location +): placeholder_list = [ { "placeholder": "toppings", @@ -232,16 +254,16 @@ def test_format_list_answer_transform_placeholder(mock_renderer): } ] + answer_store = AnswerStore( + [{"answer_id": "checkbox-answer", "value": ["Ham", "Cheese"]}] + ) + parser = PlaceholderParser( language="en", - answer_store=AnswerStore( - [{"answer_id": "checkbox-answer", "value": ["Ham", "Cheese"]}] - ), - list_store=ListStore(), - metadata={}, - response_metadata={}, - schema=QuestionnaireSchema({}), + data_stores=DataStores(answer_store=answer_store), + schema=mock_schema, renderer=mock_renderer, + location=mock_location, ) placeholders = parser(placeholder_list) @@ -249,7 +271,7 @@ def test_format_list_answer_transform_placeholder(mock_renderer): assert placeholders["toppings"] == "
  • Ham
  • Cheese
" -def test_placeholder_parser_escapes_answers(mock_renderer): +def test_placeholder_parser_escapes_answers(mock_renderer, mock_schema, mock_location): placeholder_list = [ { "placeholder": "crisps", @@ -267,21 +289,21 @@ def test_placeholder_parser_escapes_answers(mock_renderer): } ] + answer_store = AnswerStore( + [ + { + "answer_id": "checkbox-answer", + "value": ["Cheese & Onion", "Salt & Vinegar", "><'"], + } + ] + ) + parser = PlaceholderParser( language="en", - answer_store=AnswerStore( - [ - { - "answer_id": "checkbox-answer", - "value": ["Cheese & Onion", "Salt & Vinegar", "><'"], - } - ] - ), - list_store=ListStore(), - metadata={}, - response_metadata={}, - schema=QuestionnaireSchema({}), + data_stores=DataStores(answer_store=answer_store), + schema=mock_schema, renderer=mock_renderer, + location=mock_location, ) placeholders = parser(placeholder_list) @@ -292,7 +314,9 @@ def test_placeholder_parser_escapes_answers(mock_renderer): ) -def test_multiple_metadata_transform_placeholder(mock_renderer): +def test_multiple_metadata_transform_placeholder( + mock_renderer, mock_schema, mock_location +): placeholder_list = [ { "placeholder": "start_date", @@ -318,14 +342,14 @@ def test_multiple_metadata_transform_placeholder(mock_renderer): } ] + metadata = get_metadata(extra_metadata={"ref_p_start_date": "2019-02-11"}) + parser = PlaceholderParser( language="en", - answer_store=AnswerStore(), - list_store=ListStore(), - metadata={"ref_p_start_date": "2019-02-11"}, - response_metadata={}, - schema=QuestionnaireSchema({}), + data_stores=DataStores(metadata=metadata), + schema=mock_schema, renderer=mock_renderer, + location=mock_location, ) placeholders = parser(placeholder_list) @@ -333,7 +357,9 @@ def test_multiple_metadata_transform_placeholder(mock_renderer): assert placeholders["start_date"] == "11/02/2019" -def test_multiple_metadata_list_transform_placeholder(mock_renderer): +def test_multiple_metadata_list_transform_placeholder( + mock_renderer, mock_schema, mock_location +): placeholder_list = [ { "placeholder": "dates", @@ -352,21 +378,26 @@ def test_multiple_metadata_list_transform_placeholder(mock_renderer): } ] + metadata = get_metadata( + extra_metadata={ + "ref_p_start_date": "2019-02-11", + "ref_p_end_date": "2019-10-11", + } + ) + parser = PlaceholderParser( language="en", - answer_store=AnswerStore(), - list_store=ListStore(), - metadata={"ref_p_start_date": "2019-02-11", "ref_p_end_date": "2019-10-11"}, - response_metadata={}, - schema=QuestionnaireSchema({}), + data_stores=DataStores(metadata=metadata), + schema=mock_schema, renderer=mock_renderer, + location=mock_location, ) placeholders = parser(placeholder_list) assert placeholders["dates"] == "2019-02-11 2019-10-11" -def test_checkbox_transform_placeholder(mock_renderer): +def test_checkbox_transform_placeholder(mock_renderer, mock_schema, mock_location): placeholder_list = [ { "placeholder": "toppings", @@ -384,18 +415,18 @@ def test_checkbox_transform_placeholder(mock_renderer): } ] + answer_store = AnswerStore( + [ + {"answer_id": "checkbox-answer", "value": ["Ham", "Cheese"]}, + ] + ) + parser = PlaceholderParser( language="en", - answer_store=AnswerStore( - [ - {"answer_id": "checkbox-answer", "value": ["Ham", "Cheese"]}, - ] - ), - list_store=ListStore(), - metadata={}, - response_metadata={}, - schema=QuestionnaireSchema({}), + data_stores=DataStores(answer_store=answer_store), + schema=mock_schema, renderer=mock_renderer, + location=mock_location, ) placeholders = parser(placeholder_list) @@ -403,7 +434,7 @@ def test_checkbox_transform_placeholder(mock_renderer): assert placeholders["toppings"] == "Ham, Cheese" -def test_mixed_transform_placeholder(mock_renderer): +def test_mixed_transform_placeholder(mock_renderer, mock_schema, mock_location): placeholder_list = [ { "placeholder": "age", @@ -425,23 +456,24 @@ def test_mixed_transform_placeholder(mock_renderer): } ] + answer_store = AnswerStore( + [{"answer_id": "date-of-birth-answer", "value": "1999-01-01"}] + ) + metadata = get_metadata(extra_metadata={"second-date": "2019-02-02"}) + parser = PlaceholderParser( language="en", - answer_store=AnswerStore( - [{"answer_id": "date-of-birth-answer", "value": "1999-01-01"}] - ), - list_store=ListStore(), - metadata={"second-date": "2019-02-02"}, - response_metadata={}, - schema=QuestionnaireSchema({}), + data_stores=DataStores(answer_store=answer_store, metadata=metadata), + schema=mock_schema, renderer=mock_renderer, + location=mock_location, ) placeholders = parser(placeholder_list) assert placeholders["age"] == "20 years" -def test_mixed_transform_placeholder_value(mock_renderer): +def test_mixed_transform_placeholder_value(mock_renderer, mock_schema, mock_location): placeholder_list = [ { "placeholder": "age", @@ -460,23 +492,23 @@ def test_mixed_transform_placeholder_value(mock_renderer): } ] + answer_store = AnswerStore( + [{"answer_id": "date-of-birth-answer", "value": "1999-01-01"}] + ) + parser = PlaceholderParser( language="en", - answer_store=AnswerStore( - [{"answer_id": "date-of-birth-answer", "value": "1999-01-01"}] - ), - list_store=ListStore(), - metadata={}, - response_metadata={}, - schema=QuestionnaireSchema({}), + data_stores=DataStores(answer_store=answer_store), + schema=mock_schema, renderer=mock_renderer, + location=mock_location, ) placeholders = parser(placeholder_list) assert placeholders["age"] == "20 years" -def test_list_source_count(mock_renderer): +def test_list_source_count(mock_renderer, mock_schema, mock_location): placeholder_list = [ { "placeholder": "number_of_people", @@ -490,19 +522,17 @@ def test_list_source_count(mock_renderer): parser = PlaceholderParser( language="en", - answer_store=AnswerStore(), - list_store=list_store, - metadata={}, - response_metadata={}, - schema=QuestionnaireSchema({}), + data_stores=DataStores(list_store=list_store), + schema=mock_schema, renderer=mock_renderer, + location=mock_location, ) placeholders = parser(placeholder_list) assert placeholders["number_of_people"] == 2 -def test_list_source_count_in_transform(mock_renderer): +def test_list_source_count_in_transform(mock_renderer, mock_schema, mock_location): placeholder_list = [ { "placeholder": "number_of_people", @@ -527,19 +557,17 @@ def test_list_source_count_in_transform(mock_renderer): parser = PlaceholderParser( language="en", - answer_store=AnswerStore(), - list_store=list_store, - metadata={}, - response_metadata={}, - schema=QuestionnaireSchema({}), + data_stores=DataStores(list_store=list_store), + schema=mock_schema, renderer=mock_renderer, + location=mock_location, ) placeholders = parser(placeholder_list) assert placeholders["number_of_people"] == 2 -def test_chain_transform_placeholder(mock_renderer): +def test_chain_transform_placeholder(mock_renderer, mock_schema, mock_location): placeholder_list = [ { "placeholder": "persons_name", @@ -562,26 +590,28 @@ def test_chain_transform_placeholder(mock_renderer): } ] + answer_store = AnswerStore( + [ + {"answer_id": "first-name", "value": "Joe"}, + {"answer_id": "last-name", "value": "Bloggs"}, + ] + ) + parser = PlaceholderParser( language="en", - answer_store=AnswerStore( - [ - {"answer_id": "first-name", "value": "Joe"}, - {"answer_id": "last-name", "value": "Bloggs"}, - ] - ), - list_store=ListStore(), - metadata={}, - response_metadata={}, - schema=QuestionnaireSchema({}), + data_stores=DataStores(answer_store=answer_store), + schema=mock_schema, renderer=mock_renderer, + location=mock_location, ) placeholders = parser(placeholder_list) assert placeholders["persons_name"] == "Joe Bloggs’" -def test_placeholder_resolves_answer_value_based_on_first_item_in_list(mock_renderer): +def test_placeholder_resolves_answer_value_based_on_first_item_in_list( + mock_renderer, mock_schema, mock_location +): placeholder_list = [ { "placeholder": "answer", @@ -611,12 +641,10 @@ def test_placeholder_resolves_answer_value_based_on_first_item_in_list(mock_rend parser = PlaceholderParser( language="en", - answer_store=answer_store, - list_store=list_store, - metadata={}, - response_metadata={}, - schema=QuestionnaireSchema({}), + data_stores=DataStores(list_store=list_store, answer_store=answer_store), + schema=mock_schema, renderer=mock_renderer, + location=mock_location, ) placeholders = parser(placeholder_list) @@ -624,7 +652,7 @@ def test_placeholder_resolves_answer_value_based_on_first_item_in_list(mock_rend def test_placeholder_resolves_list_item_value_based_on_first_item_in_list( - mock_renderer, + mock_renderer, mock_schema, mock_location ): placeholder_list = [ { @@ -641,12 +669,10 @@ def test_placeholder_resolves_list_item_value_based_on_first_item_in_list( parser = PlaceholderParser( language="en", - answer_store=AnswerStore(), - list_store=list_store, - metadata={}, - response_metadata={}, - schema=QuestionnaireSchema({}), + data_stores=DataStores(list_store=list_store), + schema=mock_schema, renderer=mock_renderer, + location=mock_location, ) placeholders = parser(placeholder_list) @@ -654,7 +680,9 @@ def test_placeholder_resolves_list_item_value_based_on_first_item_in_list( assert str(placeholders["first_person_list_item_id"]) == list_store["people"].first -def test_placeholder_resolves_same_name_items(mock_renderer): +def test_placeholder_resolves_same_name_items( + mock_renderer, mock_schema, mock_location +): list_store = ListStore( [ { @@ -677,13 +705,11 @@ def test_placeholder_resolves_same_name_items(mock_renderer): parser = PlaceholderParser( language="en", - answer_store=AnswerStore(), - list_store=list_store, - metadata={}, - response_metadata={}, - schema=QuestionnaireSchema({}), + data_stores=DataStores(list_store=list_store), + schema=mock_schema, renderer=mock_renderer, list_item_id="abc123", + location=mock_location, ) placeholders = parser(placeholder_list) @@ -691,7 +717,9 @@ def test_placeholder_resolves_same_name_items(mock_renderer): assert placeholders["answer"] == ["abc123", "fgh789"] -def test_placeholder_resolves_name_is_duplicate_chain(mock_schema, mock_renderer): +def test_placeholder_resolves_name_is_duplicate_chain( + mock_schema, mock_renderer, mock_location +): list_store = ListStore( [ { @@ -775,17 +803,15 @@ def test_placeholder_resolves_name_is_duplicate_chain(mock_schema, mock_renderer } ] - mock_schema.is_repeating_answer = Mock(return_value=True) + mock_schema.get_list_name_for_answer_id = Mock(return_value="people") parser = PlaceholderParser( language="en", - answer_store=answer_store, - list_store=list_store, - metadata={}, - response_metadata={}, + data_stores=DataStores(list_store=list_store, answer_store=answer_store), schema=mock_schema, list_item_id="abc123", renderer=mock_renderer, + location=mock_location, ) placeholders = parser(placeholder_transforms) @@ -794,13 +820,11 @@ def test_placeholder_resolves_name_is_duplicate_chain(mock_schema, mock_renderer parser = PlaceholderParser( language="en", - answer_store=answer_store, - list_store=list_store, - metadata={}, - response_metadata={}, + data_stores=DataStores(list_store=list_store, answer_store=answer_store), schema=mock_schema, list_item_id="cde456", renderer=mock_renderer, + location=mock_location, ) placeholders = parser(placeholder_transforms) @@ -808,7 +832,9 @@ def test_placeholder_resolves_name_is_duplicate_chain(mock_schema, mock_renderer assert placeholders["persons_name"] == "Marie Smith" -def test_placeholder_resolves_list_has_items_chain(mock_schema, mock_renderer): +def test_placeholder_resolves_list_has_items_chain( + mock_schema, mock_renderer, mock_location +): list_store = ListStore( [ { @@ -888,17 +914,15 @@ def test_placeholder_resolves_list_has_items_chain(mock_schema, mock_renderer): } ] - mock_schema.is_repeating_answer = Mock(return_value=True) + mock_schema.get_list_name_for_answer_id = Mock(return_value="people") parser = PlaceholderParser( language="en", - answer_store=answer_store, - list_store=list_store, - metadata={}, - response_metadata={}, + data_stores=DataStores(answer_store=answer_store, list_store=list_store), schema=mock_schema, list_item_id="abc123", renderer=mock_renderer, + location=mock_location, ) placeholders = parser(placeholder_transforms) @@ -907,13 +931,11 @@ def test_placeholder_resolves_list_has_items_chain(mock_schema, mock_renderer): parser = PlaceholderParser( language="en", - answer_store=answer_store, - list_store=list_store, - metadata={}, - response_metadata={}, + data_stores=DataStores(answer_store=answer_store, list_store=list_store), schema=mock_schema, list_item_id="cde456", renderer=mock_renderer, + location=mock_location, ) placeholders = parser(placeholder_transforms) @@ -935,15 +957,259 @@ def test_placeholder_default_value(default_placeholder_value_schema, mock_render ], } ] + + location = Location(section_id="default-section") + parser = PlaceholderParser( language="en", - answer_store=AnswerStore(), - list_store=ListStore(), - metadata={}, - response_metadata={}, + data_stores=DataStores(), schema=default_placeholder_value_schema, renderer=mock_renderer, + location=location, ) placeholders = parser(placeholder_list) assert placeholders["answer_employee"] == "0" + + +def test_placeholder_parser_calculated_summary_dependencies_cache( + mocker, mock_renderer +): + """ + Tests Calculated Summaries fetches the dependencies using the routing path cache + Mocker patch the routing_path function in the Path Finder class to check the number of calls + Both placeholders lists use the calculated summary placeholder that requires the Path. + The first and second placeholder list is from the same section so when we call the second list, it should use the cache from the first call. + Set Location to the BlockId where the transform is required and the values have already been set + Set Answer Store with values to check if the transform is working as expected in the Schema. + With calculated summaries we check the two values in the answer source sum to the expected number + """ + schema = load_schema_from_name("test_calculated_summary") + + path_finder = mocker.patch("app.questionnaire.path_finder.PathFinder.routing_path") + + placeholder_list_1 = [ + { + "placeholder": "percentage-total-playback", + "value": { + "source": "calculated_summary", + "identifier": "percentage-total-playback", + }, + }, + ] + + placeholder_list_2 = [ + { + "placeholder": "unit-total-playback", + "value": { + "source": "calculated_summary", + "identifier": "unit-total-playback", + }, + }, + ] + + progress_store = ProgressStore( + [ + ProgressDict( + section_id="default-section", + block_ids=[ + "second-number-answer-unit-total", + "third-and-a-half-number-answer-unit-total", + "unit-total-playback", + "fifth-percent-answer", + "sixth-percent-answer", + "percentage-total-playback", + ], + status="COMPLETED", + ), + ] + ) + + answer_store = AnswerStore( + [ + {"answer_id": "second-number-answer-unit-total", "value": 1}, + {"answer_id": "third-and-a-half-number-answer-unit-total", "value": 10}, + {"answer_id": "fifth-percent-answer", "value": 2}, + {"answer_id": "sixth-percent-answer", "value": 20}, + ] + ) + + location = Location( + section_id="default-section", + block_id="calculated-summary-total-confirmation", + ) + + placeholder_parser = PlaceholderParser( + language="en", + data_stores=DataStores( + answer_store=answer_store, progress_store=progress_store + ), + schema=schema, + renderer=mock_renderer, + location=location, + ) + + placeholder_1 = placeholder_parser(placeholder_list=placeholder_list_1) + assert placeholder_1["percentage-total-playback"] == 22 + assert path_finder.called == 1 + + placeholder_2 = placeholder_parser(placeholder_list=placeholder_list_2) + assert placeholder_2["unit-total-playback"] == 11 + assert path_finder.called == 1 + + +def test_placeholder_dependencies_cache(mocker, mock_renderer): + """ + Tests Placeholder Parser fetches the placeholder dependencies using the routing path cache + Mocker patch the routing_path function in the Path Finder class to check the number of calls + Both placeholders lists use the first_non_empty_item transform that requires the Path. + The first and second placeholder list is from the same section so when we call the second list, it should use the cache from the first call. + Set Location to the BlockId where the transform is required and the values have already been set + Set Answer Store with values to check if the transform is working as expected in the Schema. + """ + schema = load_schema_from_name("test_placeholder_first_non_empty_item") + path_finder = mocker.patch("app.questionnaire.path_finder.PathFinder.routing_path") + placeholder_list_1 = [ + { + "placeholder": "date_entry_answer_from", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + { + "source": "answers", + "identifier": "date-entry-answer-from", + }, + {"source": "metadata", "identifier": "ref_p_start_date"}, + ] + }, + }, + { + "transform": "format_date", + "arguments": { + "date_to_format": {"source": "previous_transform"}, + "date_format": "d MMMM yyyy", + }, + }, + ], + } + ] + + placeholder_list_2 = [ + { + "placeholder": "date_entry_answer_to", + "transforms": [ + { + "transform": "first_non_empty_item", + "arguments": { + "items": [ + {"source": "answers", "identifier": "date-entry-answer-to"}, + {"source": "metadata", "identifier": "ref_p_end_date"}, + ] + }, + }, + { + "transform": "format_date", + "arguments": { + "date_to_format": {"source": "previous_transform"}, + "date_format": "d MMMM yyyy", + }, + }, + ], + }, + { + "placeholder": "ru_name", + "value": {"source": "metadata", "identifier": "ru_name"}, + }, + ] + + location = Location( + section_id="default-section", + block_id="total-turnover-block", + ) + + progress_store = ProgressStore( + [ + ProgressDict( + section_id="default-section", + block_ids=["date-question-block", "date-entry-block"], + status=CompletionStatus.COMPLETED, + ) + ] + ) + answer_store = AnswerStore( + [ + {"answer_id": "date-entry-answer-from", "value": "2016-04-16"}, + {"answer_id": "date-entry-answer-to", "value": "2016-04-28"}, + ] + ) + placeholder_parser = PlaceholderParser( + language="en", + data_stores=DataStores( + answer_store=answer_store, + progress_store=progress_store, + metadata=get_metadata(), + ), + schema=schema, + renderer=mock_renderer, + location=location, + ) + + placeholder_1 = placeholder_parser(placeholder_list=placeholder_list_1) + assert placeholder_1["date_entry_answer_from"] == "16 April 2016" + assert path_finder.called == 1 + + placeholder_2 = placeholder_parser(placeholder_list=placeholder_list_2) + assert placeholder_2["date_entry_answer_to"] == "28 April 2016" + assert path_finder.called == 1 + + +@pytest.mark.parametrize( + "first_number, second_number, expected_result", + ( + ("1.2", "1", "ÂŖ2.20"), + ("1", "2", "ÂŖ3"), + ("1.123", "1.2", "ÂŖ2.323"), + ), +) +def test_format_currency_placeholder_total_with_previous_transform( + mock_renderer, + mock_schema, + mock_location, + first_number, + second_number, + expected_result, +): + placeholder_list = [ + { + "placeholder": "total", + "transforms": [ + { + "transform": "add", + "arguments": { + "lhs": Decimal(first_number), + "rhs": Decimal(second_number), + }, + }, + { + "transform": "format_currency", + "arguments": {"number": {"source": "previous_transform"}}, + }, + ], + } + ] + + metadata = get_metadata() + + parser = PlaceholderParser( + language="en", + data_stores=DataStores(metadata=metadata), + schema=mock_schema, + renderer=mock_renderer, + location=mock_location, + ) + + placeholders = parser(placeholder_list) + + assert placeholders["total"] == expected_result diff --git a/tests/app/questionnaire/test_placeholder_renderer.py b/tests/app/questionnaire/test_placeholder_renderer.py index 1fc86caaed..6786e41212 100644 --- a/tests/app/questionnaire/test_placeholder_renderer.py +++ b/tests/app/questionnaire/test_placeholder_renderer.py @@ -1,15 +1,22 @@ import pytest +from mock import MagicMock from app.data_models.answer_store import AnswerStore +from app.data_models.data_stores import DataStores from app.data_models.list_store import ListStore +from app.questionnaire import Location +from app.questionnaire.placeholder_parser import PlaceholderParser from app.questionnaire.placeholder_renderer import PlaceholderRenderer +from app.utilities.schema import load_schema_from_name def test_correct_pointers(placholder_transform_pointers): assert placholder_transform_pointers[0] == "/answers/0/options/0/label" -def test_renders_pointer(placholder_transform_question_json, mocker): +def test_renders_pointer( + placholder_transform_question_json, mock_schema, mock_renderer, mock_location +): mock_transform = { "transform": "calculate_date_difference", "arguments": { @@ -33,17 +40,25 @@ def test_renders_pointer(placholder_transform_question_json, mocker): {"answer_id": "date-of-birth-answer", "value": "1991-01-01"}, ] ) - renderer = get_placeholder_render(mocker=mocker, answer_store=answer_store) + + renderer = get_placeholder_render(data_stores=DataStores(answer_store=answer_store)) rendered = renderer.render_pointer( - placholder_transform_question_json, - "/answers/0/options/0/label", + dict_to_render=placholder_transform_question_json, + pointer_to_render="/answers/0/options/0/label", list_item_id=None, + placeholder_parser=PlaceholderParser( + language="en", + data_stores=DataStores(answer_store=answer_store), + schema=mock_schema, + location=mock_location, + renderer=mock_renderer, + ), ) assert rendered == "Hal Abelson’s age is 28 years. Is this correct?" -def test_renders_json(placholder_transform_question_json, mocker): +def test_renders_json(placholder_transform_question_json): mock_transform = { "transform": "calculate_date_difference", "arguments": { @@ -67,14 +82,14 @@ def test_renders_json(placholder_transform_question_json, mocker): ] ) - renderer = get_placeholder_render(mocker=mocker, answer_store=answer_store) - rendered_schema = renderer.render(json_to_render, list_item_id=None) + renderer = get_placeholder_render(data_stores=DataStores(answer_store=answer_store)) + rendered_schema = renderer.render(data_to_render=json_to_render, list_item_id=None) rendered_label = rendered_schema["answers"][0]["options"][0]["label"] assert rendered_label == "Alfred Aho’s age is 33 years. Is this correct?" -def test_renders_json_uses_language(placholder_transform_question_json, mocker): +def test_renders_json_uses_language(placholder_transform_question_json): mock_transform = { "transform": "calculate_date_difference", "arguments": { @@ -97,34 +112,56 @@ def test_renders_json_uses_language(placholder_transform_question_json, mocker): {"answer_id": "date-of-birth-answer", "value": "1986-01-01"}, ] ) + renderer = get_placeholder_render( - mocker=mocker, language="cy", answer_store=answer_store + language="cy", data_stores=DataStores(answer_store=answer_store) ) - rendered_schema = renderer.render(json_to_render, list_item_id=None) + rendered_schema = renderer.render(data_to_render=json_to_render, list_item_id=None) rendered_label = rendered_schema["answers"][0]["options"][0]["label"] assert rendered_label == "Alfred Aho age is 33 years. Is this correct?" -def test_errors_on_invalid_pointer(placholder_transform_question_json, mocker): - renderer = get_placeholder_render(mocker=mocker) +def test_errors_on_invalid_pointer(placholder_transform_question_json, mock_schema): + renderer = get_placeholder_render() with pytest.raises(ValueError): renderer.render_pointer( - placholder_transform_question_json, "/title", list_item_id=None + dict_to_render=placholder_transform_question_json, + pointer_to_render="/title", + list_item_id=None, + placeholder_parser=PlaceholderParser( + language="en", + data_stores=DataStores(), + schema=mock_schema, + location=None, + renderer=renderer, + ), ) -def test_errors_on_invalid_json(mocker): - renderer = get_placeholder_render(mocker=mocker) +def test_errors_on_invalid_json(mock_schema): + renderer = get_placeholder_render() with pytest.raises(ValueError): dict_to_render = {"invalid": {"no": "placeholders", "in": "this"}} - renderer.render_pointer(dict_to_render, "/invalid", list_item_id=None) + renderer.render_pointer( + dict_to_render=dict_to_render, + pointer_to_render="/invalid", + list_item_id=None, + placeholder_parser=PlaceholderParser( + language="en", + data_stores=DataStores(), + schema=mock_schema, + location=None, + renderer=renderer, + ), + ) -def test_renders_text_plural_from_answers(mocker): +def test_renders_text_plural_from_answers(): answer_store = AnswerStore([{"answer_id": "number-of-people", "value": 1}]) - renderer = get_placeholder_render(mocker=mocker, answer_store=answer_store) + + renderer = get_placeholder_render(data_stores=DataStores(answer_store=answer_store)) rendered_text = renderer.render_placeholder( { "text_plural": { @@ -147,8 +184,8 @@ def test_renders_text_plural_from_answers(mocker): assert rendered_text == "Yes, 1 person lives here" -def test_renders_text_plural_from_list(mocker): - renderer = get_placeholder_render(mocker=mocker) +def test_renders_text_plural_from_list(): + renderer = get_placeholder_render() rendered_text = renderer.render_placeholder( { @@ -176,9 +213,9 @@ def test_renders_text_plural_from_list(mocker): assert rendered_text == "Yes, 0 people live here" -def test_renders_text_plural_from_metadata(mocker): +def test_renders_text_plural_from_metadata(): metadata = {"some_value": 100} - renderer = get_placeholder_render(mocker=mocker, metadata=metadata) + renderer = get_placeholder_render(data_stores=DataStores(metadata=metadata)) rendered_text = renderer.render_placeholder( { @@ -202,21 +239,103 @@ def test_renders_text_plural_from_metadata(mocker): assert rendered_text == "Yes, 100 people live here" -def get_placeholder_render( - *, - language="en", - answer_store=AnswerStore(), - list_store=ListStore(), - metadata=None, - response_metadata=None, - mocker, +def test_renders_json_dynamic_answers( + placeholder_transform_question_dynamic_answers_json, +): + json_to_render = placeholder_transform_question_dynamic_answers_json + + answer_store = AnswerStore( + [ + {"answer_id": "any-supermarket-answer", "value": "Yes"}, + { + "answer_id": "supermarket-name", + "value": "Tesco", + "list_item_id": "yWKpNY", + }, + { + "answer_id": "supermarket-name", + "value": "Aldi", + "list_item_id": "vtbSnC", + }, + {"answer_id": "list-collector-answer", "value": "No"}, + ] + ) + + list_store = ListStore([{"items": ["yWKpNY", "vtbSnC"], "name": "supermarkets"}]) + + renderer = get_placeholder_render_dynamic_answers( + data_stores=DataStores(answer_store=answer_store, list_store=list_store) + ) + rendered_schema = renderer.render(data_to_render=json_to_render, list_item_id=None) + rendered_label_first = rendered_schema["answers"][0]["label"] + rendered_id_first = rendered_schema["answers"][0]["id"] + rendered_label_second = rendered_schema["answers"][1]["label"] + rendered_id_second = rendered_schema["answers"][1]["id"] + + assert rendered_label_first == "Percentage of shopping at Tesco" + assert rendered_id_first == "percentage-of-shopping-yWKpNY" + assert rendered_label_second == "Percentage of shopping at Aldi" + assert rendered_id_second == "percentage-of-shopping-vtbSnC" + + +def test_renders_json_dynamic_answers_pointer( + placeholder_transform_question_dynamic_answers_pointer_json, ): + json_to_render = placeholder_transform_question_dynamic_answers_pointer_json + + answer_store = AnswerStore( + [ + {"answer_id": "any-supermarket-answer", "value": "Yes"}, + { + "answer_id": "supermarket-name", + "value": "Tesco", + "list_item_id": "yWKpNY", + }, + { + "answer_id": "supermarket-name", + "value": "Aldi", + "list_item_id": "vtbSnC", + }, + {"answer_id": "list-collector-answer", "value": "No"}, + ] + ) + + list_store = ListStore([{"items": ["yWKpNY", "vtbSnC"], "name": "supermarkets"}]) + + renderer = get_placeholder_render_dynamic_answers( + data_stores=DataStores(answer_store=answer_store, list_store=list_store) + ) + rendered_schema = renderer.render(data_to_render=json_to_render, list_item_id=None) + rendered_label_first = rendered_schema["question"]["answers"][0]["label"] + rendered_id_first = rendered_schema["question"]["answers"][0]["id"] + rendered_label_second = rendered_schema["question"]["answers"][1]["label"] + rendered_id_second = rendered_schema["question"]["answers"][1]["id"] + + assert rendered_label_first == "Percentage of shopping at Tesco" + assert rendered_id_first == "percentage-of-shopping-yWKpNY" + assert rendered_label_second == "Percentage of shopping at Aldi" + assert rendered_id_second == "percentage-of-shopping-vtbSnC" + + +def get_placeholder_render_dynamic_answers(*, language="en", data_stores=None): + data_stores = data_stores or DataStores() + schema = load_schema_from_name("test_dynamic_answers_list_source") + return PlaceholderRenderer( + language=language, + data_stores=data_stores, + schema=schema, + ) + + +def get_placeholder_render(*, language="en", data_stores=None): + data_stores = data_stores or DataStores() + schema = MagicMock() + schema.is_answer_dynamic = MagicMock(return_value=False) + schema.is_answer_in_list_collector_repeating_block = MagicMock(return_value=False) renderer = PlaceholderRenderer( language=language, - answer_store=answer_store, - list_store=list_store, - metadata=metadata or {}, - response_metadata=response_metadata or {}, - schema=mocker.Mock(), + data_stores=data_stores, + schema=schema, + location=Location(section_id="default-section"), ) return renderer diff --git a/tests/app/questionnaire/test_placeholder_transforms.py b/tests/app/questionnaire/test_placeholder_transforms.py index 467d36aeb5..1af30ecdab 100644 --- a/tests/app/questionnaire/test_placeholder_transforms.py +++ b/tests/app/questionnaire/test_placeholder_transforms.py @@ -1,41 +1,79 @@ +import unicodedata from decimal import Decimal import pytest from app.questionnaire.placeholder_transforms import PlaceholderTransforms +from app.questionnaire.questionnaire_schema import QuestionnaireSchema +from tests.app.test_jinja_filters import ( + TEST_FORMAT_CURRENCY_PARAMS, + TEST_FORMAT_NUMBER_PARAMS, + TEST_FORMAT_UNIT_PARAMS, +) -@pytest.mark.parametrize( - "number, currency, expected", - ( - ("11", "GBP", "ÂŖ11.00"), - ("11.99", "GBP", "ÂŖ11.99"), - ("11000", "USD", "US$11,000.00"), - (0, None, "ÂŖ0.00"), - (0.00, None, "ÂŖ0.00"), - ), -) -def test_format_currency(number, currency, expected, transformer): - assert transformer().format_currency(number, currency or "GBP") == expected +@pytest.mark.parametrize(*TEST_FORMAT_CURRENCY_PARAMS) +def test_format_currency( + mocker, + transformer, + value, + currency, + locale_string, + decimal_limit, + expected_result, + app, +): + with app.app_context(): + mocker.patch( + "app.questionnaire.placeholder_transforms.PlaceholderTransforms._get_decimal_limit", + return_value=decimal_limit, + ) + result = transformer(language=locale_string).format_currency( + number=value, currency=currency, unresolved_arguments={} + ) + assert unicodedata.normalize("NFKD", result) == expected_result + + +@pytest.mark.parametrize(*TEST_FORMAT_NUMBER_PARAMS) +def test_format_number(value, locale_string, expected_result, transformer, app): + with app.app_context(): + result = transformer(language=locale_string).format_number(value) + assert unicodedata.normalize("NFKD", result) == expected_result @pytest.mark.parametrize( - "number, expected", + "value, expected", ( - (123, "123"), - ("123.4", "123.4"), - ("123.40", "123.4"), - ("1000", "1,000"), - ("10000", "10,000"), - ("100000000", "100,000,000"), - (0, "0"), - (0.00, "0"), - ("", ""), - (None, ""), + (123, "123%"), + ("123.4", "123.4%"), + ("123.40", "123.40%"), + ("1000", "1000%"), + (0, "0%"), + (0.00, "0.0%"), + (Decimal("0.123"), "0.123%"), ), ) -def test_format_number(number, expected, transformer): - assert transformer().format_number(number) == expected +def test_format_percentage(value, expected, transformer): + assert transformer().format_percentage(value) == expected + + +@pytest.mark.parametrize(*TEST_FORMAT_UNIT_PARAMS) +def test_format_unit( + value, + measurement_unit, + locale_string, + length, + expected_result, + transformer, + app, +): + with app.app_context(): + assert ( + transformer(language=locale_string).format_unit( + measurement_unit, value, length + ) + == expected_result + ) def test_format_list(transformer): @@ -55,6 +93,12 @@ def test_format_list(transformer): assert expected_result == format_value +@pytest.mark.parametrize("list_to_format", ([], None, [[], (), ""])) +def test_format_list_empty_or_none(transformer, list_to_format): + transform = transformer() + assert transform.format_list(list_to_format) == "" + + @pytest.mark.parametrize( "name, expected", ( @@ -65,7 +109,7 @@ def test_format_list(transformer): ), ) def test_format_possessive(name, expected, transformer): - assert transformer().format_possessive(name) == expected + assert transformer(language="en").format_possessive(name) == expected @pytest.mark.parametrize( @@ -459,6 +503,7 @@ def test_option_label_from_value_with_placeholder_label( option_label_from_value_schema, ): label_renderer = placeholder_renderer + placeholder_transform = PlaceholderTransforms( language="en", schema=option_label_from_value_schema, renderer=label_renderer ) @@ -478,3 +523,18 @@ def test_option_label_from_value_with_placeholder_label( def test_conditional_trad_as(transformer, trad_as, expected): actual = transformer().conditional_trad_as(trad_as) assert actual == expected + + +@pytest.mark.parametrize( + "invalid_number_to_format", + [ + (100), + (200), + ], +) +def test_get_ordinal_indicator_raises_NotImplementedError(invalid_number_to_format): + placeholder_transform = PlaceholderTransforms( + "invalid_language", QuestionnaireSchema, "PlaceholderRenderer" + ) + with pytest.raises(NotImplementedError): + placeholder_transform.get_ordinal_indicator(invalid_number_to_format) diff --git a/tests/app/questionnaire/test_questionnaire_schema.py b/tests/app/questionnaire/test_questionnaire_schema.py index fdf9d20917..a31804804e 100644 --- a/tests/app/questionnaire/test_questionnaire_schema.py +++ b/tests/app/questionnaire/test_questionnaire_schema.py @@ -1,9 +1,11 @@ from collections import abc import pytest +from ordered_set import OrderedSet from werkzeug.datastructures import ImmutableDict -from app.questionnaire.questionnaire_schema import AnswerDependent, QuestionnaireSchema +from app.questionnaire.questionnaire_schema import Dependent, QuestionnaireSchema +from app.utilities.schema import load_schema_from_name def assert_all_dict_values_are_hashable(data): @@ -26,6 +28,25 @@ def test_schema_json_is_immutable_and_hashable(question_schema): assert_all_dict_values_are_hashable(json) +def test_schema_min_max_populate(): + schema = load_schema_from_name("test_numbers") + assert schema.min_and_max_map == { + "set-minimum": {"maximum": 4, "minimum": 5}, + "set-maximum": {"maximum": 5, "minimum": 4}, + "test-range": {"maximum": 5, "minimum": 5}, + "test-range-exclusive": {"maximum": 5, "minimum": 5}, + "test-min": {"maximum": 15, "minimum": 4}, + "test-max": {"maximum": 4, "minimum": 1}, + "test-min-exclusive": {"maximum": 15, "minimum": 3}, + "test-max-exclusive": {"maximum": 4, "minimum": 1}, + "test-percent": {"maximum": 3, "minimum": 1}, + "test-decimal": {"maximum": 5, "minimum": 5}, + "other-answer": {"maximum": 5, "minimum": 1}, + "first-number-answer": {"maximum": 4, "minimum": 2}, + "second-number-answer": {"maximum": 5, "minimum": 3}, + } + + def test_schema_attributes_returns_hashable_values(question_schema): schema = QuestionnaireSchema(question_schema) for section in schema.get_sections(): @@ -200,14 +221,6 @@ def test_get_all_questions_for_block_question(): assert all_questions[0]["answers"][0]["id"] == "answer1" -def test_get_section_ids_by_list_name(sections_dependent_on_list_schema): - schema = QuestionnaireSchema(sections_dependent_on_list_schema) - when_blocks = schema.get_section_ids_dependent_on_list("list") - - assert len(when_blocks) == 2 - assert ["section2", "section4"] == when_blocks - - def test_get_all_questions_for_block_question_variants(): block = { "id": "block1", @@ -220,7 +233,7 @@ def test_get_all_questions_for_block_question_variants(): "title": "Question 1", "answers": [{"id": "answer1", "label": "Variant 1"}], }, - "when": [], + "when": {}, }, { "question": { @@ -228,7 +241,7 @@ def test_get_all_questions_for_block_question_variants(): "title": "Question 1", "answers": [{"id": "answer1", "label": "Variant 2"}], }, - "when": [], + "when": {}, }, ], } @@ -331,24 +344,15 @@ def test_is_repeating_answer_within_list_collector( def test_get_list_collector_for_list(list_collector_variant_schema): schema = QuestionnaireSchema(list_collector_variant_schema) - section = schema.get_section("section") - result = QuestionnaireSchema.get_list_collector_for_list(section, for_list="people") + result = schema.get_list_collectors_for_list(for_list="people") - assert result["id"] == "block1" + assert result[0]["id"] == "block1" - filtered_result = QuestionnaireSchema.get_list_collector_for_list( - section, for_list="people" - ) + filtered_result = schema.get_list_collectors_for_list(for_list="people") assert filtered_result == result - no_result = QuestionnaireSchema.get_list_collector_for_list( - section, for_list="not-valid" - ) - - assert no_result is None - def test_has_address_lookup_answer(): question = { @@ -397,7 +401,7 @@ def test_answer_dependencies_for_calculated_question_non_repeating( assert schema.answer_dependencies == { "total-employees-answer": { - AnswerDependent( + Dependent( section_id="breakdown-section", block_id="employees-breakdown-block", for_list=None, @@ -405,7 +409,7 @@ def test_answer_dependencies_for_calculated_question_non_repeating( ) }, "total-turnover-answer": { - AnswerDependent( + Dependent( section_id="breakdown-section", block_id="turnover-breakdown-block", for_list=None, @@ -421,17 +425,100 @@ def test_answer_dependencies_for_calculated_question_repeating( schema = calculated_question_with_dependent_sections_schema_repeating assert schema.answer_dependencies == { + "entertainment-spending-answer": { + Dependent( + section_id="breakdown-section", + block_id="second-spending-breakdown-block", + for_list="people", + answer_id=None, + ) + }, "total-spending-answer": { - AnswerDependent( + Dependent( section_id="breakdown-section", block_id="spending-breakdown-block", for_list="people", answer_id=None, ) - } + }, } +def test_answer_dependencies_for_calculated_question_value_source( + calculated_question_with_dependent_sections_schema, +): + schema = calculated_question_with_dependent_sections_schema + + assert schema.answer_dependencies == ImmutableDict( + { + "total-answer": { + Dependent( + section_id="default-section", + block_id="breakdown-block", + for_list=None, + answer_id=None, + ) + }, + "breakdown-1": { + Dependent( + section_id="default-section", + block_id="number-total-playback", + for_list=None, + answer_id=None, + ), + Dependent( + section_id="default-section", + block_id="second-breakdown-block", + for_list=None, + answer_id=None, + ), + Dependent( + section_id="default-section", + block_id="another-number-total-playback", + for_list=None, + answer_id=None, + ), + }, + "breakdown-2": { + Dependent( + section_id="default-section", + block_id="number-total-playback", + for_list=None, + answer_id=None, + ), + Dependent( + section_id="default-section", + block_id="second-breakdown-block", + for_list=None, + answer_id=None, + ), + Dependent( + section_id="default-section", + block_id="another-number-total-playback", + for_list=None, + answer_id=None, + ), + }, + "breakdown-3": { + Dependent( + section_id="default-section", + block_id="another-number-total-playback", + for_list=None, + answer_id=None, + ) + }, + "breakdown-4": { + Dependent( + section_id="default-section", + block_id="another-number-total-playback", + for_list=None, + answer_id=None, + ) + }, + } + ) + + def test_answer_dependencies_for_calculated_summary( calculated_summary_schema, ): @@ -439,79 +526,91 @@ def test_answer_dependencies_for_calculated_summary( expected_dependencies = { "first-number-answer": { - AnswerDependent( + Dependent( section_id="default-section", - block_id="currency-total-playback-skipped-fourth", + block_id="currency-total-playback", for_list=None, answer_id=None, ), - AnswerDependent( + Dependent( section_id="default-section", - block_id="currency-total-playback-with-fourth", + block_id="set-min-max-block", for_list=None, answer_id=None, ), }, "second-number-answer": { - AnswerDependent( + Dependent( section_id="default-section", - block_id="currency-total-playback-skipped-fourth", + block_id="currency-total-playback", for_list=None, answer_id=None, ), - AnswerDependent( + Dependent( section_id="default-section", - block_id="currency-total-playback-with-fourth", + block_id="set-min-max-block", for_list=None, answer_id=None, ), }, "second-number-answer-also-in-total": { - AnswerDependent( + Dependent( section_id="default-section", - block_id="currency-total-playback-skipped-fourth", + block_id="currency-total-playback", for_list=None, answer_id=None, ), - AnswerDependent( + Dependent( section_id="default-section", - block_id="currency-total-playback-with-fourth", + block_id="set-min-max-block", for_list=None, answer_id=None, ), }, "third-number-answer": { - AnswerDependent( + Dependent( section_id="default-section", - block_id="currency-total-playback-skipped-fourth", + block_id="currency-total-playback", for_list=None, answer_id=None, ), - AnswerDependent( + Dependent( section_id="default-section", - block_id="currency-total-playback-with-fourth", + block_id="set-min-max-block", for_list=None, answer_id=None, ), }, "fourth-number-answer": { - AnswerDependent( + Dependent( section_id="default-section", - block_id="currency-total-playback-with-fourth", + block_id="currency-total-playback", for_list=None, answer_id=None, - ) + ), + Dependent( + section_id="default-section", + block_id="set-min-max-block", + for_list=None, + answer_id=None, + ), }, "fourth-and-a-half-number-answer-also-in-total": { - AnswerDependent( + Dependent( section_id="default-section", - block_id="currency-total-playback-with-fourth", + block_id="currency-total-playback", for_list=None, answer_id=None, - ) + ), + Dependent( + section_id="default-section", + block_id="set-min-max-block", + for_list=None, + answer_id=None, + ), }, "second-number-answer-unit-total": { - AnswerDependent( + Dependent( section_id="default-section", block_id="unit-total-playback", for_list=None, @@ -519,7 +618,7 @@ def test_answer_dependencies_for_calculated_summary( ) }, "third-and-a-half-number-answer-unit-total": { - AnswerDependent( + Dependent( section_id="default-section", block_id="unit-total-playback", for_list=None, @@ -527,7 +626,7 @@ def test_answer_dependencies_for_calculated_summary( ) }, "fifth-percent-answer": { - AnswerDependent( + Dependent( section_id="default-section", block_id="percentage-total-playback", for_list=None, @@ -535,7 +634,7 @@ def test_answer_dependencies_for_calculated_summary( ) }, "sixth-percent-answer": { - AnswerDependent( + Dependent( section_id="default-section", block_id="percentage-total-playback", for_list=None, @@ -543,7 +642,7 @@ def test_answer_dependencies_for_calculated_summary( ) }, "fifth-number-answer": { - AnswerDependent( + Dependent( section_id="default-section", block_id="number-total-playback", for_list=None, @@ -551,7 +650,7 @@ def test_answer_dependencies_for_calculated_summary( ) }, "sixth-number-answer": { - AnswerDependent( + Dependent( section_id="default-section", block_id="number-total-playback", for_list=None, @@ -568,7 +667,7 @@ def test_answer_dependencies_for_min_max(numbers_schema): assert schema.answer_dependencies == { "set-minimum": { - AnswerDependent( + Dependent( section_id="default-section", block_id="test-min-max-block", for_list=None, @@ -576,19 +675,35 @@ def test_answer_dependencies_for_min_max(numbers_schema): ) }, "set-maximum": { - AnswerDependent( - section_id="default-section", - block_id="detail-answer-block", + Dependent( + section_id="currency-section", + block_id="second-number-block", for_list=None, answer_id=None, ), - AnswerDependent( + Dependent( section_id="default-section", block_id="test-min-max-block", for_list=None, answer_id=None, ), }, + "test-range": { + Dependent( + section_id="default-section", + block_id="detail-answer-block", + for_list=None, + answer_id=None, + ) + }, + "first-number-answer": { + Dependent( + section_id="currency-section", + block_id="second-number-block", + for_list=None, + answer_id=None, + ) + }, } @@ -599,13 +714,13 @@ def test_answer_dependencies_for_dynamic_options( assert schema.answer_dependencies == { "injury-sustained-answer": { - AnswerDependent( + Dependent( section_id="injury-sustained-section", block_id="most-serious-injury", for_list=None, answer_id="most-serious-injury-answer", ), - AnswerDependent( + Dependent( section_id="injury-sustained-section", block_id="healed-the-quickest", for_list=None, @@ -622,25 +737,25 @@ def test_answer_dependencies_for_dynamic_options_function_driven( assert schema.answer_dependencies == { "reference-date-answer": { - AnswerDependent( + Dependent( section_id="default-section", block_id="dynamic-mutually-exclusive", for_list=None, answer_id="dynamic-mutually-exclusive-dynamic-answer", ), - AnswerDependent( + Dependent( section_id="default-section", block_id="dynamic-checkbox", for_list=None, answer_id="dynamic-checkbox-answer", ), - AnswerDependent( + Dependent( section_id="default-section", block_id="dynamic-dropdown", for_list=None, answer_id="dynamic-dropdown-answer", ), - AnswerDependent( + Dependent( section_id="default-section", block_id="dynamic-radio", for_list=None, @@ -648,3 +763,345 @@ def test_answer_dependencies_for_dynamic_options_function_driven( ), } } + + +def test_list_dependencies_for_calculated_summary_with_repeating_answers(): + """ + Tests list dependencies for list value sources, calculated summaries involving a repeat + and calculated summary value sources where the calculated summary includes a repeating answer. + """ + schema = load_schema_from_name( + "test_new_calculated_summary_repeating_and_static_answers" + ) + + assert schema.list_dependencies == { + "supermarkets": { + Dependent(section_id="section-1", block_id="dynamic-answer"), + Dependent(section_id="section-1", block_id="calculated-summary-spending"), + Dependent(section_id="section-1", block_id="calculated-summary-visits"), + Dependent(section_id="section-2", block_id="supermarket-transport"), + } + } + + +def test_when_rules_section_dependencies_by_section( + skipping_section_dependencies_schema, +): + schema = skipping_section_dependencies_schema + assert { + "household-personal-details-section": { + "skip-confirmation-section", + "skip-section", + }, + "household-section": {"skip-section"}, + "primary-person": {"skip-confirmation-section", "skip-section"}, + "skip-confirmation-section": {"skip-section"}, + } == schema.when_rules_section_dependencies_by_section + + +def test_when_rules_section_dependencies_by_answer( + skipping_section_dependencies_schema, +): + schema = skipping_section_dependencies_schema + assert { + "enable-section-answer": {"household-section"}, + "skip-age-answer": { + "household-personal-details-section", + "primary-person", + "skip-confirmation-section", + }, + "skip-confirmation-answer": { + "household-personal-details-section", + "primary-person", + }, + } == schema.when_rules_section_dependencies_by_answer + + +def test_when_rules_section_dependencies_calculated_summary( + section_dependencies_calculated_summary_schema, +): + schema = section_dependencies_calculated_summary_schema + + assert { + "milk-answer": {"dependent-enabled-section", "dependent-question-section"}, + "eggs-answer": {"dependent-enabled-section", "dependent-question-section"}, + "bread-answer": {"dependent-enabled-section", "dependent-question-section"}, + "cheese-answer": {"dependent-enabled-section", "dependent-question-section"}, + "butter-answer": {"dependent-enabled-section", "dependent-question-section"}, + } == schema.when_rules_section_dependencies_by_answer + + +def test_when_rules_section_dependencies_new_calculated_summary( + section_dependencies_new_calculated_summary_schema, +): + schema = section_dependencies_new_calculated_summary_schema + + assert { + "milk-answer": {"dependent-enabled-section", "dependent-question-section"}, + "eggs-answer": {"dependent-enabled-section", "dependent-question-section"}, + "bread-answer": {"dependent-enabled-section", "dependent-question-section"}, + "cheese-answer": {"dependent-enabled-section", "dependent-question-section"}, + "butter-answer": {"dependent-enabled-section", "dependent-question-section"}, + } == schema.when_rules_section_dependencies_by_answer + + +def test_when_rule_section_dependencies_for_list(sections_dependent_on_list_schema): + """Tests when rule dependencies for lists when used in a section, a block, and nested in a conditional question""" + schema = QuestionnaireSchema(sections_dependent_on_list_schema) + + assert schema.get_when_rule_section_dependencies_for_list("list") == { + "section2", + "section4", + "section6", + } + + +def test_progress_block_dependencies( + progress_block_dependencies_schema, +): + schema = progress_block_dependencies_schema + + assert { + "section-1": {"calculated-summary-block": {"section-2", "section-3"}} + } == schema.when_rules_block_dependencies_by_section_for_progress_value_source + + +def test_progress_section_dependencies( + progress_section_dependencies_schema, +): + schema = progress_section_dependencies_schema + + assert { + "section-1": {"section-2"}, + "section-2": {"section-4"}, + } == schema.when_rules_section_dependencies_by_section_for_progress_value_source + + +def test_progress_block_and_section_dependencies_are_ordered( + progress_dependencies_schema, +): + schema = progress_dependencies_schema + + assert ( + ImmutableDict( + { + "section-1": OrderedSet(["section-4"]), + "section-2": OrderedSet( + ["section-7", "section-8", "section-9", "section-10"] + ), + "section-4": OrderedSet(["section-6"]), + "section-5": OrderedSet(["section-7"]), + "section-7": OrderedSet(["section-8"]), + "section-9": OrderedSet(["section-12"]), + "section-10": OrderedSet(["section-11"]), + } + ) + == schema.when_rules_section_dependencies_by_section_for_progress_value_source + ) + + assert ( + ImmutableDict( + { + "section-1": { + "calculated-summary-block": OrderedSet( + [ + "section-2", + "section-3", + "section-5", + ] + ) + } + } + ) + == schema.when_rules_block_dependencies_by_section_for_progress_value_source + ) + + +@pytest.mark.parametrize( + "rule, expected_result", + ( + ([], False), + ("This is a string", False), + ({"key": "value"}, False), + ( + {"invalid-operator": ({"source": "answers", "identifier": "answer"}, 123)}, + False, + ), + ({"==": ({"source": "answers", "identifier": "answer"}, 123)}, True), + ({">": ({"source": "answers", "identifier": "answer"}, 123)}, True), + ( + { + "or": ( + {"source": "answers", "identifier": "answer"}, + "No I need to correct this", + ) + }, + True, + ), + ), +) +def test_has_operator_returns_correct_value(rule, expected_result): + result = QuestionnaireSchema.has_operator(rule) + assert result == expected_result + + +def test_progress_dependencies_for_when_rules( + progress_dependencies_schema, +): + """ + Asserts that the dependencies captured by + schema.when_rules_section_dependencies_by_section_for_progress_value_source and + schema.when_rules_section_dependencies_by_block_for_progress_value_source are flipped + correctly so that progress dependencies can be evaluated with our normal when rules + """ + schema = progress_dependencies_schema + + assert { + "section-10": {"section-2"}, + "section-11": {"section-10"}, + "section-12": {"section-9"}, + "section-2": {"section-1"}, + "section-3": {"section-1"}, + "section-4": {"section-1"}, + "section-5": {"section-1"}, + "section-6": {"section-4"}, + "section-7": {"section-5", "section-2"}, + "section-8": {"section-7", "section-2"}, + "section-9": {"section-2"}, + } == schema.when_rules_section_dependencies_for_progress + + +def test_get_blocks_with_repeating_blocks(): + schema = load_schema_from_name( + "test_list_collector_repeating_blocks_section_summary" + ) + assert len(schema.get_blocks()) == 9 + + +def test_get_block_with_repeating_blocks(): + schema = load_schema_from_name( + "test_list_collector_repeating_blocks_section_summary" + ) + block1 = schema.get_block("companies-repeating-block-1") + block2 = schema.get_block("companies-repeating-block-2") + + assert block1["id"] == "companies-repeating-block-1" + assert block2["id"] == "companies-repeating-block-2" + + +def test_get_block_for_answer_id_returns_repeating_block_for_repeating_block_answer_id(): + schema = load_schema_from_name( + "test_list_collector_repeating_blocks_section_summary" + ) + + block1 = schema.get_block_for_answer_id("registration-number") + block2 = schema.get_block_for_answer_id("authorised-trader-eu-radio") + + assert block1["id"] == "companies-repeating-block-1" + assert block2["id"] == "companies-repeating-block-2" + + +def test_when_rule_dependencies_dont_include_variants(list_collector_variant_schema): + """ + Since question variants and content variants still have the same question ids, answer ids, etc. + A change to the variant does not affect progress, + therefore section progress does not need to be re-evaluated when a when rule reference is updated + This test ensures that when rule dependencies don't include keys within variants + """ + schema = QuestionnaireSchema(list_collector_variant_schema) + assert not schema.when_rules_section_dependencies_by_answer + assert not schema.when_rules_section_dependencies_by_section + assert not schema.get_when_rule_section_dependencies_for_list("people") + + +def test_grand_calculated_summary_dependencies(): + """ + Ensures that both the grand calculated summary, and the breakdown depending on it appear in + the list dependencies of the two lists involved in the grand calculated summary, and each answer id + """ + schema = load_schema_from_name( + "test_grand_calculated_summary_inside_repeating_section" + ) + + gcs_dependent = Dependent( + section_id="vehicle-details-section", + block_id="gcs-breakdown-block", + for_list="vehicles", + ) + + assert gcs_dependent in schema.list_dependencies["costs"] + assert gcs_dependent in schema.list_dependencies["vehicles"] + gcs_answers = [ + "dynamic-answer-cost-extra", + "finance-cost-answer", + "vehicle-maintenance-cost", + "vehicle-fuel-cost", + ] + for answer in gcs_answers: + assert gcs_dependent in schema.answer_dependencies[answer] + + +def test_grand_calculated_summary_path_dependencies(): + """ + Ensures that the calculation block dependencies are correct for the grand calculated summary + These are used to look up the routing path block ids and include only answers on the path in the total value + + For this schema, the grand calculated summary, breakdown depending on it, and page it is piped into, + all need the routing path block ids from both sections to display the correct total + """ + schema = load_schema_from_name( + "test_grand_calculated_summary_inside_repeating_section" + ) + calculation_deps = schema.calculation_summary_section_dependencies_by_block[ + "vehicle-details-section" + ] + for block_id in [ + "grand-calculated-summary-vehicle", + "gcs-breakdown-block", + "gcs-piping", + ]: + assert calculation_deps[block_id] == { + "base-costs-section", + "vehicle-details-section", + } + + +def test_grand_calculated_summary_when_rule_dependencies(): + """ + Tests that for a section enabled only when a grand calculated summary is a specific value, the schema when rules + have the section depending on each answer making up the GCS and depending on the sections containing the answers and GCS + """ + schema = load_schema_from_name("test_grand_calculated_summary_overlapping_answers") + assert schema.when_rules_section_dependencies_by_section["section-4"] == { + "section-1", + "section-3", + } + assert schema.when_rules_section_dependencies_by_answer == ImmutableDict( + { + "q1-a2": {"section-4"}, + "q2-a1": {"section-4"}, + "q1-a1": {"section-4"}, + "q2-a2": {"section-4"}, + } + ) + + +def test_placeholder_transform_section_dependencies_by_block_for_calculation_summaries(): + """ + Ensures that dependencies are captured correctly for calculation summary blocks using transforms. + In this schema the calculation summaries use placeholder transforms based on other blocks that have dependencies in the + reporting-period-section + """ + schema = load_schema_from_name( + "test_placeholder_dependencies_with_calculation_summaries" + ) + + assert schema.placeholder_transform_section_dependencies_by_block == { + "questions-section": { + "calc-summary-1": {"reporting-period-section"}, + "calc-summary-2": {"reporting-period-section"}, + "how-much-rnd": {"reporting-period-section"}, + "how-much-rnd-2": {"reporting-period-section"}, + "rnd-grand-calculated-summary": {"reporting-period-section"}, + } + } diff --git a/tests/app/questionnaire/test_questionnaire_store_updater.py b/tests/app/questionnaire/test_questionnaire_store_updater.py index d42e2dbace..cbe1d3e4b3 100644 --- a/tests/app/questionnaire/test_questionnaire_store_updater.py +++ b/tests/app/questionnaire/test_questionnaire_store_updater.py @@ -1,32 +1,47 @@ +from collections import defaultdict + import pytest from mock import MagicMock, Mock +from mock.mock import call +from ordered_set import OrderedSet from werkzeug.datastructures import MultiDict -from app.data_models import QuestionnaireStore +from app.data_models import CompletionStatus, QuestionnaireStore, SupplementaryDataStore from app.data_models.answer_store import AnswerDict, AnswerStore +from app.data_models.data_stores import DataStores from app.data_models.list_store import ListStore -from app.data_models.progress_store import CompletionStatus, ProgressStore +from app.data_models.progress import ProgressDict +from app.data_models.progress_store import ProgressStore from app.questionnaire.location import Location -from app.questionnaire.questionnaire_schema import AnswerDependent, QuestionnaireSchema +from app.questionnaire.questionnaire_schema import Dependent, QuestionnaireSchema from app.questionnaire.questionnaire_store_updater import QuestionnaireStoreUpdater +from app.utilities.schema import load_schema_from_name +from app.utilities.types import DependentSection, SectionKey +# pylint: disable=too-many-locals, too-many-lines def test_save_answers_with_form_data( mock_location, mock_empty_schema, mock_empty_answer_store, mock_questionnaire_store, + mock_router, ): answer_id = "answer" answer_value = "1000" mock_empty_schema.get_answer_ids_for_question.return_value = [answer_id] + mock_empty_schema.get_answers_for_question_by_id.return_value = {answer_id: {}} form_data = {answer_id: answer_value} current_question = mock_empty_schema.get_block(mock_location.block_id)["question"] questionnaire_store_updater = QuestionnaireStoreUpdater( - mock_location, mock_empty_schema, mock_questionnaire_store, current_question + mock_location, + mock_empty_schema, + mock_questionnaire_store, + mock_router, + current_question, ) questionnaire_store_updater.update_answers(form_data) @@ -40,10 +55,98 @@ def test_save_answers_with_form_data( } -def test_save_empty_answer_removes_existing_answer( +def test_update_dynamic_answers( + mock_location, mock_empty_schema, - mock_empty_answer_store, mock_questionnaire_store, + mock_router, +): + mock_empty_schema.get_answer_ids_for_question.return_value = [ + "percentage-of-shopping-vhECeh" + ] + mock_empty_schema.answer_dependencies = { + "supermarket-name": { + Dependent( + section_id="section", + block_id="dynamic-answer", + for_list="supermarkets", + answer_id="percentage-of-shopping", + ) + } + } + + mock_empty_schema.get_answers_for_question_by_id.return_value = { + "percentage-of-shopping": {} + } + + mock_questionnaire_store.data_stores.answer_store = AnswerStore( + [ + {"answer_id": "any-supermarket-answer", "value": "Yes"}, + { + "answer_id": "supermarket-name", + "value": "Tesco", + "list_item_id": "tUJzGV", + }, + { + "answer_id": "supermarket-name", + "value": "Aldi", + "list_item_id": "vhECeh", + }, + {"answer_id": "list-collector-answer", "value": "No"}, + { + "answer_id": "percentage-of-shopping", + "value": 12, + "list_item_id": "tUJzGV", + }, + ] + ) + + form_data = {"percentage-of-shopping": 21} + + current_question = mock_empty_schema.get_block(mock_location.block_id)["question"] + + questionnaire_store_updater = QuestionnaireStoreUpdater( + mock_location, + mock_empty_schema, + mock_questionnaire_store, + mock_router, + current_question, + ) + questionnaire_store_updater._list_store = ( # pylint: disable=protected-access + ListStore([{"items": ["tUJzGV", "vhECeh"], "name": "supermarkets"}]) + ) + questionnaire_store_updater.update_answers(form_data, list_item_id="vhECeh") + + assert mock_questionnaire_store.data_stores.answer_store == AnswerStore( + [ + {"answer_id": "any-supermarket-answer", "value": "Yes"}, + { + "answer_id": "supermarket-name", + "value": "Tesco", + "list_item_id": "tUJzGV", + }, + { + "answer_id": "supermarket-name", + "value": "Aldi", + "list_item_id": "vhECeh", + }, + {"answer_id": "list-collector-answer", "value": "No"}, + { + "answer_id": "percentage-of-shopping", + "value": 12, + "list_item_id": "tUJzGV", + }, + { + "answer_id": "percentage-of-shopping", + "value": 21, + "list_item_id": "vhECeh", + }, + ] + ) + + +def test_save_empty_answer_removes_existing_answer( + mock_empty_schema, mock_empty_answer_store, mock_questionnaire_store, mock_router ): answer_id = "answer" answer_value = "1000" @@ -58,11 +161,17 @@ def test_save_empty_answer_removes_existing_answer( mock_empty_schema.get_answer_ids_for_question.return_value = [answer_id] + mock_empty_schema.get_answers_for_question_by_id.return_value = {answer_id: {}} + form_data = MultiDict({answer_id: answer_value}) current_question = mock_empty_schema.get_block(location.block_id)["question"] questionnaire_store_updater = QuestionnaireStoreUpdater( - location, mock_empty_schema, mock_questionnaire_store, current_question + location, + mock_empty_schema, + mock_questionnaire_store, + mock_router, + current_question, ) questionnaire_store_updater.update_answers(form_data) @@ -91,6 +200,7 @@ def test_default_answers_are_not_saved( mock_empty_schema, mock_empty_answer_store, mock_questionnaire_store, + mock_router, ): answer_id = "answer" default_value = 0 @@ -99,13 +209,18 @@ def test_default_answers_are_not_saved( mock_empty_schema.get_answers_by_answer_id.return_value = [ {"default": default_value} ] + mock_empty_schema.get_answers_for_question_by_id.return_value = {answer_id: {}} # No answer given so will use schema defined default form_data = MultiDict({answer_id: None}) current_question = {"answers": [{"id": "answer", "default": default_value}]} questionnaire_store_updater = QuestionnaireStoreUpdater( - mock_location, mock_empty_schema, mock_questionnaire_store, current_question + mock_location, + mock_empty_schema, + mock_questionnaire_store, + mock_router, + current_question, ) questionnaire_store_updater.update_answers(form_data) @@ -117,6 +232,7 @@ def test_empty_answers( mock_empty_schema, mock_empty_answer_store, mock_questionnaire_store, + mock_router, ): string_answer_id = "string-answer" checkbox_answer_id = "checkbox-answer" @@ -127,6 +243,11 @@ def test_empty_answers( checkbox_answer_id, radio_answer_id, ] + mock_empty_schema.get_answers_for_question_by_id.return_value = { + string_answer_id: {}, + checkbox_answer_id: {}, + radio_answer_id: {}, + } form_data = { string_answer_id: "", @@ -142,7 +263,11 @@ def test_empty_answers( ] } questionnaire_store_updater = QuestionnaireStoreUpdater( - mock_location, mock_empty_schema, mock_questionnaire_store, current_question + mock_location, + mock_empty_schema, + mock_questionnaire_store, + mock_router, + current_question, ) questionnaire_store_updater.update_answers(form_data) @@ -152,12 +277,11 @@ def test_empty_answers( def test_remove_all_answers_with_list_item_id( mock_location, mock_empty_schema, - mock_empty_answer_store, - mock_questionnaire_store, + mock_router, mocker, ): mock_empty_answer_store = AnswerStore( - existing_answers=[ + answers=[ {"answer_id": "test1", "value": 1, "list_item_id": "abcdef"}, {"answer_id": "test2", "value": 2, "list_item_id": "abcdef"}, {"answer_id": "test3", "value": 3, "list_item_id": "uvwxyz"}, @@ -167,15 +291,18 @@ def test_remove_all_answers_with_list_item_id( mock_questionnaire_store = mocker.MagicMock( spec=QuestionnaireStore, completed_blocks=[], - answer_store=mock_empty_answer_store, - list_store=mocker.MagicMock(spec=ListStore), - progress_store=ProgressStore(), + data_stores=DataStores( + answer_store=mock_empty_answer_store, + list_store=mocker.MagicMock(spec=ListStore), + progress_store=ProgressStore(), + supplementary_data_store=SupplementaryDataStore(), + ), ) questionnaire_store_updater = QuestionnaireStoreUpdater( - mock_location, mock_empty_schema, mock_questionnaire_store, None + mock_location, mock_empty_schema, mock_questionnaire_store, mock_router, None ) - questionnaire_store_updater.remove_list_item_and_answers("abc", "abcdef") + questionnaire_store_updater.remove_list_item_data("abc", "abcdef") assert len(mock_empty_answer_store) == 1 assert mock_empty_answer_store.get_answer("test3", "uvwxyz") @@ -184,13 +311,12 @@ def test_remove_all_answers_with_list_item_id( def test_remove_primary_person( mock_location, mock_empty_schema, - mock_empty_answer_store, - mock_questionnaire_store, + mock_router, populated_list_store, mocker, ): mock_empty_answer_store = AnswerStore( - existing_answers=[ + answers=[ {"answer_id": "test1", "value": 1, "list_item_id": "abcdef"}, {"answer_id": "test2", "value": 2, "list_item_id": "abcdef"}, {"answer_id": "test3", "value": 3, "list_item_id": "xyzabc"}, @@ -200,13 +326,16 @@ def test_remove_primary_person( mock_questionnaire_store = mocker.MagicMock( spec=QuestionnaireStore, completed_blocks=[], - answer_store=mock_empty_answer_store, - list_store=populated_list_store, - progress_store=ProgressStore(), + data_stores=DataStores( + answer_store=mock_empty_answer_store, + list_store=populated_list_store, + progress_store=ProgressStore(), + supplementary_data_store=SupplementaryDataStore(), + ), ) questionnaire_store_updater = QuestionnaireStoreUpdater( - mock_location, mock_empty_schema, mock_questionnaire_store, None + mock_location, mock_empty_schema, mock_questionnaire_store, mock_router, None ) questionnaire_store_updater.remove_primary_person("people") @@ -216,21 +345,23 @@ def test_add_primary_person( mock_location, mock_empty_schema, mock_empty_answer_store, - mock_questionnaire_store, + mock_router, populated_list_store, mocker, ): - mock_questionnaire_store = mocker.MagicMock( spec=QuestionnaireStore, completed_blocks=[], - answer_store=mock_empty_answer_store, - list_store=populated_list_store, - progress_store=ProgressStore(), + data_stores=DataStores( + answer_store=mock_empty_answer_store, + list_store=populated_list_store, + progress_store=ProgressStore(), + supplementary_data_store=SupplementaryDataStore(), + ), ) questionnaire_store_updater = QuestionnaireStoreUpdater( - mock_location, mock_empty_schema, mock_questionnaire_store, None + mock_location, mock_empty_schema, mock_questionnaire_store, mock_router, None ) questionnaire_store_updater.add_primary_person("people") @@ -240,7 +371,8 @@ def test_remove_completed_relationship_locations_for_list_name( mock_empty_schema, mock_empty_answer_store, mock_empty_progress_store, - mock_questionnaire_store, + mock_empty_supplementary_data_store, + mock_router, populated_list_store, mocker, ): @@ -251,12 +383,15 @@ def test_remove_completed_relationship_locations_for_list_name( mock_questionnaire_store = mocker.MagicMock( spec=QuestionnaireStore, completed_blocks=[], - answer_store=mock_empty_answer_store, - list_store=populated_list_store, - progress_store=mock_empty_progress_store, + data_stores=DataStores( + answer_store=mock_empty_answer_store, + list_store=populated_list_store, + progress_store=mock_empty_progress_store, + supplementary_data_store=mock_empty_supplementary_data_store, + ), ) questionnaire_store_updater = QuestionnaireStoreUpdater( - mock_location, mock_empty_schema, mock_questionnaire_store, None + mock_location, mock_empty_schema, mock_questionnaire_store, mock_router, None ) patch_method = "app.questionnaire.questionnaire_store_updater.QuestionnaireStoreUpdater._get_relationship_collectors_by_list_name" @@ -278,7 +413,8 @@ def test_remove_completed_relationship_locations_for_list_name_no_locations( mock_empty_schema, mock_empty_answer_store, mock_empty_progress_store, - mock_questionnaire_store, + mock_empty_supplementary_data_store, + mock_router, populated_list_store, mocker, ): @@ -290,12 +426,15 @@ def test_remove_completed_relationship_locations_for_list_name_no_locations( mock_questionnaire_store = mocker.MagicMock( spec=QuestionnaireStore, completed_blocks=[], - answer_store=mock_empty_answer_store, - list_store=populated_list_store, - progress_store=mock_empty_progress_store, + data_stores=DataStores( + answer_store=mock_empty_answer_store, + list_store=populated_list_store, + progress_store=mock_empty_progress_store, + supplementary_data_store=mock_empty_supplementary_data_store, + ), ) questionnaire_store_updater = QuestionnaireStoreUpdater( - mock_location, mock_empty_schema, mock_questionnaire_store, None + mock_location, mock_empty_schema, mock_questionnaire_store, mock_router, None ) questionnaire_store_updater.remove_completed_relationship_locations_for_list_name( @@ -313,23 +452,27 @@ def test_update_relationship_question_completeness_no_relationship_collectors( mock_empty_schema, mock_empty_answer_store, mock_empty_progress_store, - mock_questionnaire_store, + mock_empty_supplementary_data_store, + mock_router, populated_list_store, mocker, ): mock_questionnaire_store = mocker.MagicMock( spec=QuestionnaireStore, completed_blocks=[], - answer_store=mock_empty_answer_store, - list_store=populated_list_store, - progress_store=mock_empty_progress_store, + data_stores=DataStores( + answer_store=mock_empty_answer_store, + list_store=populated_list_store, + progress_store=mock_empty_progress_store, + supplementary_data_store=mock_empty_supplementary_data_store, + ), ) questionnaire_store_updater = QuestionnaireStoreUpdater( - mock_location, mock_empty_schema, mock_questionnaire_store, None + mock_location, mock_empty_schema, mock_questionnaire_store, mock_router, None ) assert ( - questionnaire_store_updater.update_relationship_question_completeness( + questionnaire_store_updater._update_relationship_question_completeness( # pylint: disable=protected-access "test-relationship-collector" ) is None @@ -339,13 +482,12 @@ def test_update_relationship_question_completeness_no_relationship_collectors( def test_update_same_name_items( mock_location, mock_empty_schema, - mock_empty_answer_store, - mock_questionnaire_store, + mock_router, populated_list_store, mocker, ): mock_empty_answer_store = AnswerStore( - existing_answers=[ + answers=[ {"answer_id": "first-name", "value": "Joe", "list_item_id": "abcdef"}, { "answer_id": "middle-name", @@ -372,13 +514,16 @@ def test_update_same_name_items( mock_questionnaire_store = mocker.MagicMock( spec=QuestionnaireStore, completed_blocks=[], - answer_store=mock_empty_answer_store, - list_store=populated_list_store, - progress_store=ProgressStore(), + data_stores=DataStores( + answer_store=mock_empty_answer_store, + list_store=populated_list_store, + progress_store=ProgressStore(), + supplementary_data_store=SupplementaryDataStore(), + ), ) questionnaire_store_updater = QuestionnaireStoreUpdater( - mock_location, mock_empty_schema, mock_questionnaire_store, None + mock_location, mock_empty_schema, mock_questionnaire_store, mock_router, None ) questionnaire_store_updater.update_same_name_items( @@ -392,7 +537,7 @@ def test_update_same_name_items( def get_answer_dependencies(for_list=None): return { "total-employees-answer": { - AnswerDependent( + Dependent( section_id="breakdown-section", block_id="employees-breakdown-block", for_list=for_list, @@ -400,7 +545,7 @@ def get_answer_dependencies(for_list=None): ) }, "total-turnover-answer": { - AnswerDependent( + Dependent( section_id="breakdown-section", block_id="turnover-breakdown-block", for_list=for_list, @@ -411,24 +556,41 @@ def get_answer_dependencies(for_list=None): @pytest.mark.parametrize( - "answer_id, answer_updated, answer_dependencies, expected_output", + "answer_id, answer_updated, answer_dependencies, is_repeating, expected_output", [ ( "total-employees-answer", True, get_answer_dependencies(), + False, {("breakdown-section", None): {"employees-breakdown-block"}}, ), ( "total-turnover-answer", True, get_answer_dependencies(), + False, {("breakdown-section", None): {"turnover-breakdown-block"}}, ), ( "total-employees-answer", True, get_answer_dependencies(for_list="people"), + True, + {("breakdown-section", "person-1"): {"employees-breakdown-block"}}, + ), + ( + "total-turnover-answer", + True, + get_answer_dependencies(for_list="people"), + True, + {("breakdown-section", "person-1"): {"turnover-breakdown-block"}}, + ), + ( + "total-employees-answer", + True, + get_answer_dependencies(for_list="people"), + False, { ("breakdown-section", "person-1"): {"employees-breakdown-block"}, ("breakdown-section", "person-2"): {"employees-breakdown-block"}, @@ -439,6 +601,7 @@ def get_answer_dependencies(for_list=None): "total-turnover-answer", True, get_answer_dependencies(for_list="people"), + False, { ("breakdown-section", "person-1"): {"turnover-breakdown-block"}, ("breakdown-section", "person-2"): {"turnover-breakdown-block"}, @@ -449,21 +612,23 @@ def get_answer_dependencies(for_list=None): "total-employees-answer", False, get_answer_dependencies(), + False, {}, ), ], ) def test_update_answers_captures_answer_dependencies( mock_empty_answer_store, + mock_router, answer_id, answer_updated, answer_dependencies, + is_repeating, expected_output, mock_schema, ): location = Location( - section_id="default-section", - block_id="default-block", + section_id="default-section", block_id="default-block", list_item_id="person-1" ) list_store = ListStore( @@ -477,6 +642,11 @@ def test_update_answers_captures_answer_dependencies( mock_schema.get_answer_ids_for_question.return_value = [answer_id] mock_schema.answer_dependencies = answer_dependencies + mock_schema.is_answer_in_repeating_section.return_value = is_repeating + mock_schema.get_answers_for_question_by_id.return_value = { + "total-employees-answer": {}, + "total-turnover-answer": {}, + } mock_empty_answer_store.add_or_update.return_value = answer_updated form_data = MultiDict({answer_id: "some-value"}) @@ -486,6 +656,7 @@ def test_update_answers_captures_answer_dependencies( schema=mock_schema, answer_store=mock_empty_answer_store, list_store=list_store, + router=mock_router, current_location=location, current_question=current_question, ) @@ -500,7 +671,8 @@ def test_update_answers_captures_answer_dependencies( @pytest.mark.parametrize( "answer_dependent_answer_id, updated_answer_value, expected_output", [ - ( # when the answer dependent has an answer_id, then the dependent answer should be removed from the answer store + ( + # when the answer dependent has an answer_id, then the dependent answer should be removed from the answer store "second-answer", "answer updated", AnswerStore( @@ -519,7 +691,8 @@ def test_update_answers_captures_answer_dependencies( ] ), ), - ( # When the answer dependent has an answer_id, but the answer dependency value is not changed, then the answer store should not change + ( + # When the answer dependent has an answer_id, but the answer dependency value is not changed, then the answer store should not change "second-answer", "original answer", AnswerStore( @@ -532,9 +705,12 @@ def test_update_answers_captures_answer_dependencies( ], ) def test_update_answers_with_answer_dependents( - mock_schema, answer_dependent_answer_id, updated_answer_value, expected_output + mock_schema, + mock_router, + answer_dependent_answer_id, + updated_answer_value, + expected_output, ): - answer_store = AnswerStore( [ AnswerDict(answer_id="first-answer", value="original answer"), @@ -548,7 +724,7 @@ def test_update_answers_with_answer_dependents( mock_schema.get_answer_ids_for_question.return_value = ["first-answer"] mock_schema.answer_dependencies = { "first-answer": { - AnswerDependent( + Dependent( section_id="section", block_id="second-block", for_list=None, @@ -556,6 +732,10 @@ def test_update_answers_with_answer_dependents( ) }, } + mock_schema.get_answers_for_question_by_id.return_value = { + "first-answer": {}, + "second-answer": {}, + } form_data = MultiDict({"first-answer": updated_answer_value}) @@ -568,6 +748,7 @@ def test_update_answers_with_answer_dependents( schema=mock_schema, answer_store=answer_store, list_store=ListStore(), + router=mock_router, current_location=location, current_question=current_question, ) @@ -576,11 +757,49 @@ def test_update_answers_with_answer_dependents( assert answer_store == expected_output -def test_update_repeating_answers_with_answer_dependents(mock_schema): +@pytest.mark.parametrize( + "is_repeating, expected_output", + [ + ( + False, + AnswerStore( + [ + AnswerDict( + answer_id="first-answer", + value="answer updated", + list_item_id="abc123", + ), + ] + ), + ), + ( + True, + AnswerStore( + [ + AnswerDict( + answer_id="first-answer", + value="answer updated", + list_item_id="abc123", + ), + AnswerDict( + answer_id="second-answer", + value="second answer", + list_item_id="xyz456", + ), + ] + ), + ), + ], +) +def test_update_repeating_answers_with_answer_dependents( + mock_schema, mock_router, is_repeating, expected_output +): # Given repeating dependent answers answer_store = AnswerStore( [ - AnswerDict(answer_id="first-answer", value="original-answer"), + AnswerDict( + answer_id="first-answer", value="original-answer", list_item_id="abc123" + ), AnswerDict( answer_id="second-answer", value="second answer", list_item_id="abc123" ), @@ -594,7 +813,7 @@ def test_update_repeating_answers_with_answer_dependents(mock_schema): mock_schema.get_answer_ids_for_question.return_value = ["first-answer"] mock_schema.answer_dependencies = { "first-answer": { - AnswerDependent( + Dependent( section_id="section", block_id="second-block", for_list="list-name", @@ -602,18 +821,24 @@ def test_update_repeating_answers_with_answer_dependents(mock_schema): ) }, } + mock_schema.get_answers_for_question_by_id.return_value = { + "first-answer": {}, + "second-answer": {}, + } form_data = MultiDict({"first-answer": "answer updated"}) location = Location( - section_id="section", - block_id="first-block", + section_id="section", block_id="first-block", list_item_id="abc123" ) current_question = mock_schema.get_block(location.block_id)["question"] + + mock_schema.is_answer_in_repeating_section.return_value = is_repeating questionnaire_store_updater = get_questionnaire_store_updater( schema=mock_schema, answer_store=answer_store, list_store=list_store, + router=mock_router, current_location=location, current_question=current_question, ) @@ -622,8 +847,232 @@ def test_update_repeating_answers_with_answer_dependents(mock_schema): questionnaire_store_updater.update_answers(form_data) # Then all repeating dependent answers should be removed from the answer store - assert answer_store == AnswerStore( - [AnswerDict(answer_id="first-answer", value="answer updated")] + assert answer_store == expected_output + + +@pytest.mark.parametrize( + "section_status, updated_answer_value, is_path_complete, expected_status", + [ + ( + # When an answer is changed which causes the path of a dependent section to be incomplete, Then that sections is update to IN_PROGRESS + CompletionStatus.COMPLETED, + "answer updated", + False, + CompletionStatus.IN_PROGRESS, + ), + ( + # When an answer is changed which causes the path of a dependent section to be complete, Then that sections is update to COMPLETED + CompletionStatus.IN_PROGRESS, + "answer updated", + True, + CompletionStatus.COMPLETED, + ), + ( # When an answer is not changed, Then a dependent section status should not change + CompletionStatus.IN_PROGRESS, + "original answer", + False, + CompletionStatus.IN_PROGRESS, + ), + ( # When an answer is not changed, Then a dependent section status should not change + CompletionStatus.COMPLETED, + "original answer", + True, + CompletionStatus.COMPLETED, + ), + ], +) +def test_answer_id_section_dependents( + section_status, + updated_answer_value, + is_path_complete, + expected_status, + mock_schema, + mock_router, +): + mock_schema.get_answer_ids_for_question.return_value = ["first-answer"] + mock_schema.get_repeating_list_for_section.return_value = None + mock_schema.when_rules_section_dependencies_by_answer = { + "first-answer": {"section-2"} + } + mock_schema.get_section_ids.return_value = ["section-1", "section-2"] + mock_router.is_path_complete.return_value = is_path_complete + mock_schema.get_answers_for_question_by_id.return_value = { + "first-answer": {}, + "second-answer": {}, + } + + answer_store = AnswerStore( + [ + AnswerDict(answer_id="first-answer", value="original answer"), + AnswerDict(answer_id="second-answer", value="second answer"), + ] + ) + form_data = MultiDict({"first-answer": updated_answer_value}) + location = Location( + section_id="section", + block_id="first-block", + ) + progress_store = ProgressStore( + [ + ProgressDict( + section_id="section-2", + block_ids=["second-block"], + status=section_status, + ) + ], + ) + current_question = mock_schema.get_block(location.block_id)["question"] + questionnaire_store_updater = get_questionnaire_store_updater( + schema=mock_schema, + answer_store=answer_store, + progress_store=progress_store, + router=mock_router, + current_location=location, + current_question=current_question, + ) + questionnaire_store_updater.update_answers(form_data) + questionnaire_store_updater.update_progress_for_dependent_sections() + + assert progress_store.get_section_status(SectionKey("section-2")) is expected_status + + +@pytest.mark.parametrize( + "list_item_1_section_status, list_item_2_section_status, updated_answer_value, " + "is_list_item_1_path_complete, is_list_item_2_path_complete, expected_list_item_1_status, expected_list_item_2_status", + [ + ( + # When an answer is changed which causes repeating dependent section to be incomplete, Then those repeating sections are updated to IN_PROGRESS + CompletionStatus.COMPLETED, + CompletionStatus.COMPLETED, + "answer updated", + False, + False, + CompletionStatus.IN_PROGRESS, + CompletionStatus.IN_PROGRESS, + ), + ( + # When an answer is changed which causes repeating dependent section to be complete, Then those repeating sections are updated to COMPLETED + CompletionStatus.IN_PROGRESS, + CompletionStatus.IN_PROGRESS, + "answer updated", + True, + True, + CompletionStatus.COMPLETED, + CompletionStatus.COMPLETED, + ), + ( + # When an answer is changed which causes repeating section paths to change, Then those repeating sections statuses are updated correctly + CompletionStatus.COMPLETED, + CompletionStatus.IN_PROGRESS, + "answer updated", + False, + True, + CompletionStatus.IN_PROGRESS, + CompletionStatus.COMPLETED, + ), + ( # When an answer is not changed, Then a repeating dependent section status should not change + CompletionStatus.COMPLETED, + CompletionStatus.IN_PROGRESS, + "original answer", + True, + False, + CompletionStatus.COMPLETED, + CompletionStatus.IN_PROGRESS, + ), + ], +) +def test_answer_id_section_dependents_repeating( + list_item_1_section_status, + list_item_2_section_status, + updated_answer_value, + is_list_item_1_path_complete, + is_list_item_2_path_complete, + expected_list_item_1_status, + expected_list_item_2_status, + mock_schema, + mock_router, +): + mock_schema.get_repeating_list_for_section.return_value = "list-name" + mock_schema.get_answer_ids_for_question.return_value = ["first-answer"] + mock_schema.when_rules_section_dependencies_by_answer = { + "first-answer": {"section-2"} + } + mock_schema.get_section_ids.return_value = ["section-1", "section-2"] + mock_schema.get_answers_for_question_by_id.return_value = { + "first-answer": {}, + "second-answer": {}, + } + + answer_store = AnswerStore( + [ + AnswerDict(answer_id="first-answer", value="original answer"), + AnswerDict( + answer_id="second-answer", + value="second answer", + list_item_id="list-item-id-1", + ), + AnswerDict( + answer_id="second-answer", + value="second answer", + list_item_id="list-item-id-2", + ), + ] + ) + list_store = ListStore( + [{"items": ["list-item-id-1", "list-item-id-2"], "name": "list-name"}] + ) + form_data = MultiDict({"first-answer": updated_answer_value}) + location = Location( + section_id="section", + block_id="first-block", + ) + + progress_store = ProgressStore( + [ + ProgressDict( + section_id="section-2", + block_ids=["second-block"], + status=list_item_1_section_status, + list_item_id="list-item-id-1", + ), + ProgressDict( + section_id="section-2", + block_ids=["second-block"], + status=list_item_2_section_status, + list_item_id="list-item-id-2", + ), + ], + ) + current_question = mock_schema.get_block(location.block_id)["question"] + questionnaire_store_updater = get_questionnaire_store_updater( + schema=mock_schema, + answer_store=answer_store, + list_store=list_store, + progress_store=progress_store, + router=mock_router, + current_location=location, + current_question=current_question, + ) + questionnaire_store_updater.update_answers(form_data) + + # This test case is dependent on the order that the dependent_sections set is iterated over, + # however as python sets are unordered we need to check that the first item is equal to our expected + # list_item_id so that we can set the correct side effect as per the test case + first_item = next(iter(questionnaire_store_updater.dependent_sections), None) + effects = [is_list_item_1_path_complete, is_list_item_2_path_complete] + if first_item and first_item.list_item_id != "list-item-id-1": + effects = [is_list_item_2_path_complete, is_list_item_1_path_complete] + mock_router.is_path_complete.side_effect = effects + + questionnaire_store_updater.update_progress_for_dependent_sections() + + assert ( + progress_store.get_section_status(SectionKey("section-2", "list-item-id-1")) + is expected_list_item_1_status + ) + assert ( + progress_store.get_section_status(SectionKey("section-2", "list-item-id-2")) + is expected_list_item_2_status ) @@ -634,11 +1083,15 @@ def get_questionnaire_store_updater( answer_store=None, list_store=None, progress_store=None, + router=None, current_question=None, + supplementary_data_store=None, ): answer_store = AnswerStore() if answer_store is None else answer_store list_store = ListStore() if list_store is None else list_store progress_store = ProgressStore() if progress_store is None else progress_store + supplementary_data_store = supplementary_data_store or SupplementaryDataStore() + mock_schema = ( MagicMock( QuestionnaireSchema({"questionnaire_flow": {"type": "Hub", "options": {}}}) @@ -652,14 +1105,21 @@ def get_questionnaire_store_updater( mock_questionnaire_store = MagicMock( spec=QuestionnaireStore, - answer_store=answer_store, - list_store=list_store, - progress_store=progress_store, + data_stores=DataStores( + answer_store=answer_store, + list_store=list_store, + progress_store=progress_store, + supplementary_data_store=supplementary_data_store, + ), ) current_question = current_question or {} return QuestionnaireStoreUpdater( - current_location, mock_schema, mock_questionnaire_store, current_question + current_location, + mock_schema, + mock_questionnaire_store, + router, + current_question, ) @@ -668,32 +1128,35 @@ def get_questionnaire_store_updater( [CompletionStatus.IN_PROGRESS, CompletionStatus.COMPLETED], ) def test_dependent_sections_completed_dependant_blocks_removed_and_status_updated( - dependent_section_status, + mocker, dependent_section_status, mock_router ): # Given current_location = Location( - section_id="company-summary-section", block_id="total-turnover-block" + section_id="company-summary-section", block_id="breakdown-section" ) progress_store = ProgressStore( [ - { - "section_id": "company-summary-section", - "block_ids": ["total-turnover-block", "total-employees-block"], - "status": "COMPLETED", - }, - { - "section_id": "breakdown-section", - "block_ids": [ + ProgressDict( + section_id="company-summary-section", + block_ids=["total-turnover-block", "total-employees-block"], + status=CompletionStatus.COMPLETED, + ), + ProgressDict( + section_id="breakdown-section", + block_ids=[ "turnover-breakdown-block", ], - "status": dependent_section_status, - }, + status=dependent_section_status, + ), ], ) + questionnaire_store_updater = get_questionnaire_store_updater( - current_location=current_location, progress_store=progress_store + current_location=current_location, + progress_store=progress_store, + router=mock_router, ) - dependent_section_key = ("breakdown-section", None) + dependent_section_key = SectionKey("breakdown-section", None) dependent_block_id = "turnover-breakdown-block" questionnaire_store_updater.dependent_block_id_by_section_key = { @@ -701,18 +1164,29 @@ def test_dependent_sections_completed_dependant_blocks_removed_and_status_update } assert dependent_block_id in progress_store.get_completed_block_ids( - *dependent_section_key + section_key=SectionKey(*dependent_section_key) ) + mocker.patch( + "app.questionnaire.questionnaire_store_updater.QuestionnaireStoreUpdater._get_chronological_section_dependents", + return_value=[ + DependentSection( + section_id="breakdown-section", list_item_id=None, is_complete=False + ) + ], + ) # When - questionnaire_store_updater.update_progress_for_dependant_sections() + questionnaire_store_updater.remove_dependent_blocks_and_capture_dependent_sections() + questionnaire_store_updater.update_progress_for_dependent_sections() # Then assert dependent_block_id not in progress_store.get_completed_block_ids( - *dependent_section_key + section_key=SectionKey(*dependent_section_key) ) assert ( - progress_store.get_section_status(*dependent_section_key) + progress_store.get_section_status( + section_key=SectionKey(*dependent_section_key) + ) == CompletionStatus.IN_PROGRESS ) @@ -724,14 +1198,14 @@ def test_dependent_sections_current_section_status_not_updated(mocker): ) progress_store = ProgressStore( [ - { - "section_id": "breakdown-section", - "block_ids": [ + ProgressDict( + section_id="breakdown-section", + block_ids=[ "total-turnover-block", "turnover-breakdown-block", ], - "status": CompletionStatus.COMPLETED, - }, + status=CompletionStatus.COMPLETED, + ), ], ) questionnaire_store_updater = get_questionnaire_store_updater( @@ -746,36 +1220,43 @@ def test_dependent_sections_current_section_status_not_updated(mocker): questionnaire_store_updater.update_section_status = mocker.Mock() assert dependent_block_id in progress_store.get_completed_block_ids( - *dependent_section_key + section_key=SectionKey(*dependent_section_key) ) # When - questionnaire_store_updater.update_progress_for_dependant_sections() + questionnaire_store_updater.remove_dependent_blocks_and_capture_dependent_sections() + questionnaire_store_updater.update_progress_for_dependent_sections() # Then assert dependent_block_id not in progress_store.get_completed_block_ids( - *dependent_section_key + section_key=SectionKey(*dependent_section_key) ) # Status for current section is handled separately by handle post. assert questionnaire_store_updater.update_section_status.call_count == 0 -def test_dependent_sections_not_started_skipped(mocker): +def test_dependent_sections_not_started_skipped(mock_router, mocker): # Given + schema = load_schema_from_name( + "test_validation_sum_against_total_hub_with_dependent_section" + ) current_location = Location( section_id="company-summary-section", block_id="total-turnover-block" ) progress_store = ProgressStore( [ - { - "section_id": "company-summary-section", - "block_ids": ["total-turnover-block", "total-employees-block"], - "status": "COMPLETED", - } + ProgressDict( + section_id="company-summary-section", + block_ids=["total-turnover-block", "total-employees-block"], + status=CompletionStatus.COMPLETED, + ) ], ) questionnaire_store_updater = get_questionnaire_store_updater( - current_location=current_location, progress_store=progress_store + current_location=current_location, + progress_store=progress_store, + router=mock_router, + schema=schema, ) dependent_section_key = ("breakdown-section", None) @@ -789,36 +1270,39 @@ def test_dependent_sections_not_started_skipped(mocker): questionnaire_store_updater.update_section_status = mocker.Mock() # When - questionnaire_store_updater.update_progress_for_dependant_sections() + questionnaire_store_updater.remove_dependent_blocks_and_capture_dependent_sections() + questionnaire_store_updater.update_progress_for_dependent_sections() # Then assert questionnaire_store_updater.remove_completed_location.call_count == 0 assert questionnaire_store_updater.update_section_status.call_count == 0 -def test_dependent_sections_started_but_blocks_incomplete(mocker): +def test_dependent_sections_started_but_blocks_incomplete(mock_router, mocker): # Given current_location = Location( section_id="company-summary-section", block_id="total-employees-block" ) progress_store = ProgressStore( [ - { - "section_id": "company-summary-section", - "block_ids": ["total-turnover-block", "total-employees-block"], - "status": "COMPLETED", - }, - { - "section_id": "breakdown-section", - "block_ids": [ + ProgressDict( + section_id="company-summary-section", + block_ids=["total-turnover-block", "total-employees-block"], + status=CompletionStatus.COMPLETED, + ), + ProgressDict( + section_id="breakdown-section", + block_ids=[ "turnover-breakdown-block", ], - "status": "IN_PROGRESS", - }, + status=CompletionStatus.IN_PROGRESS, + ), ], ) questionnaire_store_updater = get_questionnaire_store_updater( - current_location=current_location, progress_store=progress_store + current_location=current_location, + progress_store=progress_store, + router=mock_router, ) dependent_section_key = ("breakdown-section", None) @@ -830,11 +1314,12 @@ def test_dependent_sections_started_but_blocks_incomplete(mocker): questionnaire_store_updater.update_section_status = mocker.Mock() assert dependent_block_id not in progress_store.get_completed_block_ids( - *dependent_section_key + section_key=SectionKey(*dependent_section_key) ) # When - questionnaire_store_updater.update_progress_for_dependant_sections() + questionnaire_store_updater.remove_dependent_blocks_and_capture_dependent_sections() + questionnaire_store_updater.update_progress_for_dependent_sections() # Then assert questionnaire_store_updater.update_section_status.call_count == 0 @@ -845,9 +1330,11 @@ def test_dependent_sections_started_but_blocks_incomplete(mocker): [CompletionStatus.IN_PROGRESS, CompletionStatus.COMPLETED], ) def test_repeating_dependent_sections_completed_dependant_blocks_removed_and_status_updated( - dependent_section_status, + mocker, dependent_section_status, mock_router ): - # Given + schema = load_schema_from_name( + "test_validation_sum_against_total_hub_with_dependent_section" + ) current_location = Location( section_id="company-summary-section", block_id="total-turnover-block" ) @@ -861,11 +1348,11 @@ def test_repeating_dependent_sections_completed_dependant_blocks_removed_and_sta ) progress_store = ProgressStore( [ - { - "section_id": "company-summary-section", - "block_ids": ["total-turnover-block", "total-employees-block"], - "status": "COMPLETED", - }, + ProgressDict( + section_id="company-summary-section", + block_ids=["total-turnover-block", "total-employees-block"], + status=CompletionStatus.COMPLETED, + ), { "section_id": "breakdown-section", "list_item_id": "item-1", @@ -888,23 +1375,207 @@ def test_repeating_dependent_sections_completed_dependant_blocks_removed_and_sta current_location=current_location, progress_store=progress_store, list_store=list_store, + router=mock_router, + schema=schema, ) questionnaire_store_updater.dependent_block_id_by_section_key = { - ("breakdown-section", list_item): {"turnover-breakdown-block"} + SectionKey("breakdown-section", list_item): {"turnover-breakdown-block"} for list_item in list_store["some-list"] } + questionnaire_store_updater.dependent_sections.add( + DependentSection( + section_id="breakdown-section", list_item_id="item-1", is_complete=None + ) + ) + mocker.patch( + "app.questionnaire.questionnaire_store_updater.QuestionnaireStoreUpdater._get_chronological_section_dependents", + return_value=[ + DependentSection( + section_id="breakdown-section", list_item_id=None, is_complete=None + ) + ], + ) # When - questionnaire_store_updater.update_progress_for_dependant_sections() + questionnaire_store_updater.remove_dependent_blocks_and_capture_dependent_sections() + questionnaire_store_updater.update_progress_for_dependent_sections() # Then for list_item in list_store["some-list"]: section_id, list_item_id = "breakdown-section", list_item assert "turnover-breakdown-block" not in progress_store.get_completed_block_ids( - section_id, list_item_id + SectionKey(section_id, list_item_id) ) assert ( - progress_store.get_section_status(section_id, list_item_id) + progress_store.get_section_status(SectionKey(section_id, list_item_id)) == CompletionStatus.IN_PROGRESS ) + assert questionnaire_store_updater.dependent_sections == { + DependentSection( + section_id=section_id, list_item_id="item-1", is_complete=False + ), + DependentSection( + section_id=section_id, list_item_id="item-2", is_complete=False + ), + } + + +@pytest.mark.parametrize( + "dependent_section_status", + [CompletionStatus.IN_PROGRESS, CompletionStatus.COMPLETED], +) +def test_dependent_sections_added_dependant_block_removed( + dependent_section_status, mock_router +): + # Given + current_location = Location( + section_id="company-summary-section", block_id="total-turnover-block" + ) + progress_store = ProgressStore( + [ + ProgressDict( + section_id="company-summary-section", + block_ids=["total-turnover-block", "total-employees-block"], + status=CompletionStatus.COMPLETED, + ), + ProgressDict( + section_id="breakdown-section", + block_ids=[ + "turnover-breakdown-block", + ], + status=dependent_section_status, + ), + ], + ) + questionnaire_store_updater = get_questionnaire_store_updater( + current_location=current_location, + progress_store=progress_store, + router=mock_router, + ) + dependent_section_key = SectionKey("breakdown-section", None) + dependent_block_id = "turnover-breakdown-block" + + questionnaire_store_updater.dependent_block_id_by_section_key = { + dependent_section_key: {dependent_block_id} + } + + assert dependent_block_id in progress_store.get_completed_block_ids( + section_key=SectionKey(*dependent_section_key) + ) + assert questionnaire_store_updater.dependent_sections == set() + + # When + questionnaire_store_updater.remove_dependent_blocks_and_capture_dependent_sections() + + # Then + assert dependent_block_id not in progress_store.get_completed_block_ids( + section_key=SectionKey(*dependent_section_key) + ) + assert questionnaire_store_updater.dependent_sections == { + DependentSection( + section_id="breakdown-section", list_item_id=None, is_complete=False + ) + } + + +@pytest.mark.parametrize( + "status_unchanged_section_ids, expected_routing_path_calls", + [ + ( + # s1.s1 -> s1.s2 -> s2.s3 -> s3.s4 -> s3.s5 -> s3.s7 -> s2.s6 + [], + [ + call(SectionKey("section-1")), + call(SectionKey("section-2")), + call(SectionKey("section-3")), + call(SectionKey("section-4")), + call(SectionKey("section-5")), + call(SectionKey("section-7")), + call(SectionKey("section-6")), + ], + ), + ( + # s1 -> s1.s2 -> s1.s3 -> s3.s4 -> s3.s5 -> s3.s7 + ["section-2"], + [ + call(SectionKey("section-1")), + call(SectionKey("section-2")), + call(SectionKey("section-3")), + call(SectionKey("section-4")), + call(SectionKey("section-5")), + call(SectionKey("section-7")), + ], + ), + ( + # s1 -> s1.s2 -> s2.s3 -> s2.s4 -> s2.s5 -> s2.s6 + ["section-3"], + [ + call(SectionKey("section-1")), + call(SectionKey("section-2")), + call(SectionKey("section-3")), + call(SectionKey("section-4")), + call(SectionKey("section-5")), + call(SectionKey("section-6")), + ], + ), + ], +) +def test_questionnaire_store_updater_dependency_capture( + mocker, + mock_router, + mock_schema, + status_unchanged_section_ids, + expected_routing_path_calls, +): + """ + This test is intended to ensure that the order in which dependencies are captured and evaluated is correct. + We should only call the routing path for a given section once and need to ensure that only the necessary paths are evaluated + i.e. only sections in which the status has changed should be evaluated. + """ + current_location = Location(section_id="section-1", block_id="block-2") + mock_dependencies = defaultdict(OrderedSet) | { + "section-1": OrderedSet(["section-2", "section-3"]), + "section-2": OrderedSet(["section-3", "section-4", "section-5", "section-6"]), + "section-3": OrderedSet(["section-4", "section-5", "section-7"]), + } + mock_schema.when_rules_section_dependencies_by_section_for_progress_value_source = ( + mock_dependencies + ) + mock_schema.get_repeating_list_for_section.return_value = False + mock_router.is_path_complete.return_value = ( + False # This will result in new status being IN=PROGRESS + ) + progress_store = ProgressStore( + [ + { + "section_id": f"section-{idx}", + "block_ids": ["block-1", "block-2"], + "status": ( + CompletionStatus.IN_PROGRESS + if f"section-{idx}" in status_unchanged_section_ids + else CompletionStatus.COMPLETED + ), + } + for idx in range(1, 8) + ], + ) + questionnaire_store_updater = get_questionnaire_store_updater( + current_location=current_location, + progress_store=progress_store, + router=mock_router, + schema=mock_schema, + ) + mocker.patch( + "app.questionnaire.questionnaire_store_updater.QuestionnaireStoreUpdater._get_chronological_section_dependents", + return_value=[ + DependentSection( + section_id="section-1", list_item_id=None, is_complete=None + ), + DependentSection( + section_id="section-2", list_item_id=None, is_complete=None + ), + ], + ) + questionnaire_store_updater.update_progress_for_dependent_sections() + assert mock_router.routing_path.call_args_list == expected_routing_path_calls diff --git a/tests/app/questionnaire/test_questionnaire_store_updater_base.py b/tests/app/questionnaire/test_questionnaire_store_updater_base.py new file mode 100644 index 0000000000..e7acf7f92e --- /dev/null +++ b/tests/app/questionnaire/test_questionnaire_store_updater_base.py @@ -0,0 +1,458 @@ +# pylint: disable=redefined-outer-name +import pytest +from mock import MagicMock, Mock + +from app.data_models import AnswerStore, ListStore, ProgressStore, QuestionnaireStore +from app.data_models.progress import CompletionStatus, ProgressDict +from app.questionnaire import QuestionnaireSchema +from app.questionnaire.questionnaire_store_updater import QuestionnaireStoreUpdaterBase +from app.utilities.make_immutable import make_immutable +from app.utilities.types import DependentSection, SectionKey + + +@pytest.fixture +def questionnaire_store_with_supplementary_data( + fake_questionnaire_store, supplementary_data_store_with_data +): + fake_questionnaire_store.data_stores.supplementary_data_store = ( + supplementary_data_store_with_data + ) + fake_questionnaire_store.data_stores.list_store = ListStore( + [{"items": ["item-1", "item-2"], "name": "products"}] + ) + # Mock the identifier generation in list store so the ids are item-1, item-2, ... + # pylint: disable=protected-access + fake_questionnaire_store.data_stores.list_store._generate_identifier = Mock( + side_effect=(f"item-{i}" for i in range(3, 100)) + ) + return fake_questionnaire_store + + +@pytest.fixture +def questionnaire_store_with_employee_supplementary_data( + fake_questionnaire_store, + supplementary_data_store_with_employees, +): + """Mock questionnaire store with supplementary data of two products and partial progress""" + fake_questionnaire_store.data_stores.supplementary_data_store = ( + supplementary_data_store_with_employees + ) + fake_questionnaire_store.data_stores.list_store = ListStore( + [ + {"items": ["item-1", "item-2"], "name": "products"}, + { + "items": ["employee-1", "employee-2", "employee-3", "employee-4"], + "name": "employees", + }, + ], + ) + fake_questionnaire_store.data_stores.progress_store = ProgressStore( + [ + ProgressDict( + section_id="introduction-section", + block_ids=["loaded-successfully-block", "introduction-block"], + status=CompletionStatus.COMPLETED, + ), + ProgressDict( + section_id="section-2", + block_ids=["list-collector-employees"], + status=CompletionStatus.COMPLETED, + ), + ProgressDict( + section_id="section-3", + block_ids=["any-additional-employees"], + status=CompletionStatus.COMPLETED, + ), + ProgressDict( + section_id="section-4", + block_ids=["length-of-employment"], + list_item_id="employee-1", + status=CompletionStatus.COMPLETED, + ), + ProgressDict( + section_id="section-4", + block_ids=["length-of-employment"], + list_item_id="employee-2", + status=CompletionStatus.COMPLETED, + ), + ProgressDict( + section_id="section-6", + block_ids=["product-repeating-block-1"], + status=CompletionStatus.COMPLETED, + list_item_id="item-1", + ), + ProgressDict( + section_id="section-6", + block_ids=["product-repeating-block-1"], + status=CompletionStatus.COMPLETED, + list_item_id="item-2", + ), + ProgressDict( + section_id="section-6", + block_ids=[ + "list-collector-products", + "calculated-summary-volume-sales", + "calculated-summary-volume-total", + "dynamic-products", + "calculated-summary-value-sales", + ], + status=CompletionStatus.COMPLETED, + ), + ProgressDict( + section_id="section-8", + block_ids=["product-volume-interstitial"], + status=CompletionStatus.COMPLETED, + ), + ] + ) + # Mock the identifier generation in list store so the ids are item-1, item-2, ... + # pylint: disable=protected-access + fake_questionnaire_store.data_stores.list_store._generate_identifier = Mock( + side_effect=(f"item-{i}" for i in range(3, 100)) + ) + return fake_questionnaire_store + + +def test_removing_list_item_data( + supplementary_data_schema, questionnaire_store_with_employee_supplementary_data +): + """ + Tests that if you remove list item data with the base updater + it removes the list item from the list store and removes the progress for any dependents of the list item + """ + base_questionnaire_store_updater = QuestionnaireStoreUpdaterBase( + schema=supplementary_data_schema, + questionnaire_store=questionnaire_store_with_employee_supplementary_data, + router=MagicMock(), + ) + base_questionnaire_store_updater.remove_list_item_data("products", "item-2") + base_questionnaire_store_updater.capture_dependencies_for_list_change("products") + + assert questionnaire_store_with_employee_supplementary_data.data_stores.list_store[ + "products" + ].items == ["item-1"] + # should not contain item-1 since this should be unaffected + assert base_questionnaire_store_updater.dependent_sections == { + DependentSection(section_id="section-6", list_item_id=None), + DependentSection(section_id="section-8", list_item_id=None), + } + # this should affect all blocks in section-6 + assert base_questionnaire_store_updater.dependent_block_id_by_section_key == { + SectionKey(section_id="section-6", list_item_id=None): { + "list-collector-products", + "calculated-summary-value-sales", + "dynamic-products", + "calculated-summary-volume-sales", + "calculated-summary-volume-total", + }, + } + + +@pytest.mark.parametrize( + "section_id,list_item_id,dependent_blocks,expected_blocks", + [ + ( + "section-6", + None, + { + "calculated-summary-volume-sales", + "calculated-summary-volume-total", + }, + [ + "list-collector-products", + "dynamic-products", + "calculated-summary-value-sales", + ], + ), + ( + "section-6", + None, + { + "list-collector-products", + "calculated-summary-value-sales", + "dynamic-products", + "calculated-summary-volume-sales", + "calculated-summary-volume-total", + }, + [], + ), + ( + "section-6", + "item-1", + {"product-repeating-block-1"}, + [], + ), + ( + "section-8", + None, + {"product-volume-interstitial"}, + [], + ), + ], +) +def test_remove_dependent_blocks_and_capture_dependent_sections( + supplementary_data_schema, + questionnaire_store_with_employee_supplementary_data, + section_id, + list_item_id, + dependent_blocks, + expected_blocks, +): + """ + Tests that the progress store is successfully updated when removing captured dependencies + and that the dependent sections are captured correctly + """ + base_questionnaire_store_updater = QuestionnaireStoreUpdaterBase( + schema=supplementary_data_schema, + questionnaire_store=questionnaire_store_with_employee_supplementary_data, + router=MagicMock(), + ) + section_key = SectionKey(section_id=section_id, list_item_id=list_item_id) + base_questionnaire_store_updater.dependent_block_id_by_section_key = { + section_key: dependent_blocks, + } + base_questionnaire_store_updater.remove_dependent_blocks_and_capture_dependent_sections() + assert ( + base_questionnaire_store_updater._progress_store._progress[ # pylint: disable=protected-access + section_key + ].block_ids + == expected_blocks + ) + assert base_questionnaire_store_updater.dependent_sections == { + DependentSection( + section_id=section_id, list_item_id=list_item_id, is_complete=False + ) + } + + +@pytest.mark.parametrize( + "section_id, list_item_id", + [("section-6", None), ("section-6", "item-1"), ("section-8", None)], +) +def test_update_progress_for_dependent_sections( + supplementary_data_schema, + questionnaire_store_with_employee_supplementary_data, + section_id, + list_item_id, +): + """ + Tests that captured dependent sections get set back to in progress + """ + base_questionnaire_store_updater = QuestionnaireStoreUpdaterBase( + schema=supplementary_data_schema, + questionnaire_store=questionnaire_store_with_employee_supplementary_data, + router=MagicMock(), + ) + base_questionnaire_store_updater.dependent_sections = { + dependent_section := DependentSection( + section_id=section_id, list_item_id=list_item_id, is_complete=False + ) + } + base_questionnaire_store_updater.update_progress_for_dependent_sections() + assert ( + base_questionnaire_store_updater._progress_store._progress[ # pylint: disable=protected-access + dependent_section.section_key + ].status + == CompletionStatus.IN_PROGRESS + ) + + +def test_update_progress_of_repeating_dependent( + supplementary_data_schema, + questionnaire_store_with_employee_supplementary_data, +): + """ + Tests that when a data change unlocks a new question within a repeating section + Each repeating section which has been started is captured + """ + base_questionnaire_store_updater = QuestionnaireStoreUpdaterBase( + schema=supplementary_data_schema, + questionnaire_store=questionnaire_store_with_employee_supplementary_data, + router=MagicMock(), + ) + base_questionnaire_store_updater.remove_list_item_data("employees", "employee-4") + base_questionnaire_store_updater.capture_dependencies_for_list_change("employees") + # employee-3 not affected as section had not yet been started + assert base_questionnaire_store_updater.dependent_sections == { + DependentSection(section_id="section-2"), + DependentSection(section_id="section-4", list_item_id="employee-1"), + DependentSection(section_id="section-4", list_item_id="employee-2"), + } + + +def test_update_supplementary_data_list_in_schema_with_no_list_collector( + question_variant_schema, + fake_questionnaire_store, + supplementary_data_with_employees, +): + """ + Tests that updating supplementary data for a schema with no list collectors is safe + """ + base_questionnaire_store_updater = QuestionnaireStoreUpdaterBase( + schema=QuestionnaireSchema(questionnaire_json=question_variant_schema), + questionnaire_store=fake_questionnaire_store, + router=MagicMock(), + ) + base_questionnaire_store_updater.set_supplementary_data( + supplementary_data_with_employees + ) + base_questionnaire_store_updater.remove_list_item_data("products", "product-1") + base_questionnaire_store_updater.capture_dependencies_for_list_change("products") + # supplementary data has nothing to do with given schema + assert base_questionnaire_store_updater.dependent_sections == set() + + +class TestSettingSupplementaryData: + store: QuestionnaireStore + + def get_questionnaire_store_updater_base(self): + return QuestionnaireStoreUpdaterBase( + schema=MagicMock(), + questionnaire_store=self.store, + router=MagicMock(), + ) + + def assert_list_store_data(self, list_name: str, list_item_ids: list[str]): + """Helper function to check that ListStore contains the given list with matching list_item_ids""" + lists = [list_model.name for list_model in self.store.data_stores.list_store] + assert list_name in lists + assert self.store.data_stores.list_store[list_name].items == list_item_ids + + def test_adding_new_supplementary_data( + self, fake_questionnaire_store, supplementary_data + ): + """Tests that adding supplementary data adds supplementary list items to the list store + this test doesn't mock list item ids, and checks that they match those in list_mappings + """ + self.store = fake_questionnaire_store + questionnaire_store_updater_base = self.get_questionnaire_store_updater_base() + questionnaire_store_updater_base.set_supplementary_data(supplementary_data) + assert "products" in self.store.data_stores.supplementary_data_store.list_lookup + supplementary_list_item_ids = list( + self.store.data_stores.supplementary_data_store.list_lookup[ + "products" + ].values() + ) + # check list mapping ids match list store ids + self.assert_list_store_data("products", supplementary_list_item_ids) + + def test_updating_supplementary_data( + self, questionnaire_store_with_supplementary_data, supplementary_data + ): + """Test that overwriting supplementary data with additional lists/items adds them to the list store + without duplicating any existing data""" + self.store = questionnaire_store_with_supplementary_data + questionnaire_store_updater_base = self.get_questionnaire_store_updater_base() + + supplementary_data["items"]["supermarkets"] = [{"identifier": "54321"}] + supplementary_data["items"]["products"].append({"identifier": "12345"}) + questionnaire_store_updater_base.set_supplementary_data(supplementary_data) + + assert ( + self.store.data_stores.supplementary_data_store.list_mappings + == make_immutable( + { + "products": [ + {"identifier": 89929001, "list_item_id": "item-1"}, + {"identifier": "201630601", "list_item_id": "item-2"}, + {"identifier": "12345", "list_item_id": "item-3"}, + ], + "supermarkets": [ + {"identifier": "54321", "list_item_id": "item-4"}, + ], + } + ) + ) + + self.assert_list_store_data("products", ["item-1", "item-2", "item-3"]) + self.assert_list_store_data("supermarkets", ["item-4"]) + + def test_removing_some_supplementary_data( + self, questionnaire_store_with_supplementary_data, supplementary_data + ): + """Tests that if you overwrite existing supplementary data with data that is missing list item ids + or lists, that the list store is updated to remove that data""" + self.store = questionnaire_store_with_supplementary_data + questionnaire_store_updater_base = self.get_questionnaire_store_updater_base() + + del supplementary_data["items"]["products"][0] + questionnaire_store_updater_base.set_supplementary_data(supplementary_data) + + # products item-1 should be gone + self.assert_list_store_data("products", ["item-2"]) + + def test_removing_all_supplementary_data( + self, questionnaire_store_with_supplementary_data + ): + """Checks that removing all supplementary data clears out the list store""" + self.store = questionnaire_store_with_supplementary_data + questionnaire_store_updater_base = self.get_questionnaire_store_updater_base() + questionnaire_store_updater_base.set_supplementary_data(to_set={}) + assert len(list(self.store.data_stores.list_store)) == 0 + + def test_removing_supplementary_lists_with_answers( + self, questionnaire_store_with_supplementary_data, supplementary_data + ): + """Tests that if you overwrite supplementary data, + related answers for old list/list_item_ids are removed from the answer store""" + self.store = questionnaire_store_with_supplementary_data + # add some answers for the supplementary list items + self.store.data_stores.answer_store = AnswerStore( + [ + { + "answer_id": "product-sales-answer", + "value": "100", + "list_item_id": "item-1", + }, + { + "answer_id": "product-sales-answer", + "value": "200", + "list_item_id": "item-2", + }, + ] + ) + questionnaire_store_updater_base = self.get_questionnaire_store_updater_base() + + # delete the first product and update supplementary data + del supplementary_data["items"]["products"][0] + questionnaire_store_updater_base.set_supplementary_data(supplementary_data) + + # item-1 should be gone + self.assert_list_store_data("products", ["item-2"]) + # the answer for it should be too + answers = list(self.store.data_stores.answer_store.answer_map.keys()) + assert len(answers) == 1 + assert answers[0] == ("product-sales-answer", "item-2") + + # remove all answers + questionnaire_store_updater_base.set_supplementary_data({}) + assert not self.store.data_stores.answer_store.answer_map + + def test_removing_supplementary_data_ignores_non_supplementary_data( + self, questionnaire_store_with_supplementary_data + ): + """Tests that removing supplementary data does not affect other lists and answers""" + self.store = questionnaire_store_with_supplementary_data + questionnaire_store_updater_base = self.get_questionnaire_store_updater_base() + # unrelated + self.store.data_stores.answer_store = AnswerStore( + [ + { + "answer_id": "unrelated-answer", + "value": "100", + "list_item_id": "JxSW21", + }, + { + "answer_id": "sales", + "value": "200", + }, + ] + ) + self.store.data_stores.list_store.add_list_item("supermarkets") + self.assert_list_store_data("products", ["item-1", "item-2"]) + self.assert_list_store_data("supermarkets", ["item-3"]) + + questionnaire_store_updater_base.set_supplementary_data({}) + self.assert_list_store_data("supermarkets", ["item-3"]) + answers = list(self.store.data_stores.answer_store.answer_map.keys()) + assert answers == [("unrelated-answer", "JxSW21"), ("sales", None)] diff --git a/tests/app/questionnaire/test_relationship_location.py b/tests/app/questionnaire/test_relationship_location.py index 77af655622..9ed402e590 100644 --- a/tests/app/questionnaire/test_relationship_location.py +++ b/tests/app/questionnaire/test_relationship_location.py @@ -1,5 +1,6 @@ import pytest +from app.questionnaire.location import SectionKey from app.questionnaire.relationship_location import RelationshipLocation @@ -14,20 +15,15 @@ def test_location_url(): ) location_url = location.url() - assert ( - location_url - == "http://test.localdomain/questionnaire/relationships/household/id1/to/id2/", - ) - assert ( - location.for_json() - == { - "section_id": "household", - "block_id": "relationships", - "list_item_id": "id1", - "to_list_item_id": "id2", - "list_name": "household", - }, - ) + assert location_url == "/questionnaire/relationships/household/id1/to/id2/" + + assert location.for_json() == { + "section_id": "household", + "block_id": "relationships", + "list_item_id": "id1", + "to_list_item_id": "id2", + "list_name": "household", + } def test_create_location_from_dict(): @@ -45,3 +41,6 @@ def test_create_location_from_dict(): assert location.block_id == "relationships" assert location.list_item_id == "id1" assert location.to_list_item_id == "id2" + assert location.section_key == SectionKey( + section_id=location.section_id, list_item_id=location.list_item_id + ) diff --git a/tests/app/questionnaire/test_return_location.py b/tests/app/questionnaire/test_return_location.py new file mode 100644 index 0000000000..352f6fd905 --- /dev/null +++ b/tests/app/questionnaire/test_return_location.py @@ -0,0 +1,26 @@ +from app.questionnaire.return_location import ReturnLocation + +TEST_RETURN_LOCATION_OBJECT = ReturnLocation( + return_to="test-return-to", + return_to_block_id="test-return-to-block-id", + return_to_answer_id="test-return-to-answer-id", + return_to_list_item_id="return-to-list-item-id", +) + + +def test_return_location_to_dict(): + assert TEST_RETURN_LOCATION_OBJECT.to_dict() == { + "return_to": "test-return-to", + "return_to_block_id": "test-return-to-block-id", + "return_to_answer_id": "test-return-to-answer-id", + "return_to_list_item_id": "return-to-list-item-id", + } + + +def test_return_location_to_dict_with_anchor_return_to_answer_id(): + assert TEST_RETURN_LOCATION_OBJECT.to_dict(answer_id_is_anchor=True) == { + "return_to": "test-return-to", + "return_to_block_id": "test-return-to-block-id", + "_anchor": "test-return-to-answer-id", + "return_to_list_item_id": "return-to-list-item-id", + } diff --git a/tests/app/questionnaire/test_router.py b/tests/app/questionnaire/test_router.py index b6a62786ce..7034e6d272 100644 --- a/tests/app/questionnaire/test_router.py +++ b/tests/app/questionnaire/test_router.py @@ -1,13 +1,19 @@ +# pylint: disable=too-many-lines + from functools import cached_property -from unittest.mock import Mock import pytest from flask import url_for +from mock import Mock +from app.data_models import CompletionStatus from app.data_models.answer_store import AnswerStore +from app.data_models.data_stores import DataStores from app.data_models.list_store import ListStore -from app.data_models.progress_store import CompletionStatus, ProgressStore -from app.questionnaire.location import Location +from app.data_models.progress import ProgressDict +from app.data_models.progress_store import ProgressStore +from app.questionnaire.location import Location, SectionKey +from app.questionnaire.return_location import ReturnLocation from app.questionnaire.router import Router from app.questionnaire.routing_path import RoutingPath from app.utilities.schema import load_schema_from_name @@ -15,37 +21,30 @@ class RouterTestCase: schema = None - answer_store = AnswerStore() - list_store = ListStore() - progress_store = ProgressStore() - metadata = {} - response_metadata = {} + data_stores: DataStores + + @pytest.fixture(autouse=True) + def setup(self): + self.data_stores = DataStores() @cached_property def router(self): - return Router( - self.schema, - self.answer_store, - self.list_store, - self.progress_store, - self.metadata, - self.response_metadata, - ) + return Router(self.schema, self.data_stores) class TestRouter(RouterTestCase): def test_enabled_section_ids(self): self.schema = load_schema_from_name("test_section_enabled_checkbox") - self.progress_store = ProgressStore( + self.data_stores.progress_store = ProgressStore( [ - { - "section_id": "section-1", - "block_ids": ["section-1-block"], - "status": CompletionStatus.COMPLETED, - } + ProgressDict( + section_id="section-1", + block_ids=["section-1-block"], + status=CompletionStatus.COMPLETED, + ) ] ) - self.answer_store = AnswerStore( + self.data_stores.answer_store = AnswerStore( [{"answer_id": "section-1-answer", "value": ["Section 2"]}] ) @@ -53,17 +52,13 @@ def test_enabled_section_ids(self): assert expected_section_ids == self.router.enabled_section_ids - self.schema = load_schema_from_name("test_new_section_enabled_checkbox") - - assert expected_section_ids == self.router.enabled_section_ids - def test_full_routing_path_without_repeating_sections(self): self.schema = load_schema_from_name("test_checkbox") routing_path = self.router.full_routing_path() expected_path = [ RoutingPath( - [ + block_ids=[ "mandatory-checkbox", "non-mandatory-checkbox", "single-checkbox", @@ -78,7 +73,7 @@ def test_full_routing_path_with_repeating_sections(self): self.schema = load_schema_from_name( "test_repeating_sections_with_hub_and_spoke" ) - self.list_store = ListStore( + self.data_stores.list_store = ListStore( [ { "items": ["abc123", "123abc"], @@ -92,7 +87,7 @@ def test_full_routing_path_with_repeating_sections(self): expected_path = [ RoutingPath( - [ + block_ids=[ "primary-person-list-collector", "list-collector", "next-interstitial", @@ -100,17 +95,15 @@ def test_full_routing_path_with_repeating_sections(self): "visitors-block", ], section_id="section", - list_name=None, - list_item_id=None, ), RoutingPath( - ["proxy", "date-of-birth", "confirm-dob", "sex"], + block_ids=["proxy", "date-of-birth", "confirm-dob", "sex"], section_id="personal-details-section", list_name="people", list_item_id="abc123", ), RoutingPath( - ["proxy", "date-of-birth", "confirm-dob", "sex"], + block_ids=["proxy", "date-of-birth", "confirm-dob", "sex"], section_id="personal-details-section", list_name="people", list_item_id="123abc", @@ -119,22 +112,142 @@ def test_full_routing_path_with_repeating_sections(self): assert expected_path == routing_path + def test_can_access_hub(self): + self.schema = load_schema_from_name( + "test_repeating_sections_with_hub_and_spoke" + ) + + assert self.router.can_access_hub() + + def test_can_access_hub_with_required_sections_enabled_and_sections_complete(self): + self.schema = load_schema_from_name("test_hub_section_required_and_enabled") + + self.data_stores.progress_store = ProgressStore( + [ + ProgressDict( + section_id="household-section", + block_ids=["household-relationships-block"], + status=CompletionStatus.COMPLETED, + ), + ProgressDict( + section_id="relationships-section", + block_ids=["relationships-count"], + status=CompletionStatus.COMPLETED, + ), + ], + ) + + assert self.router.can_access_hub() + + def test_can_access_hub_with_required_sections_enabled_and_section_incomplete(self): + self.schema = load_schema_from_name("test_hub_section_required_and_enabled") + + self.data_stores.progress_store = ProgressStore( + [ + ProgressDict( + section_id="household-section", + block_ids=["household-relationships-block"], + status=CompletionStatus.IN_PROGRESS, + ), + ], + ) + + assert not self.router.can_access_hub() + + def test_can_access_hub_with_required_sections_enabled_and_repeating_sections_complete( + self, + ): + self.schema = load_schema_from_name("test_hub_section_required_with_repeat") + + self.data_stores.progress_store = ProgressStore( + [ + ProgressDict( + section_id="list-collector-section", + block_ids=["primary-person-list-collector", "list-collector"], + status=CompletionStatus.COMPLETED, + ), + ProgressDict( + section_id="personal-details-section", + block_ids=["proxy", "date-of-birth"], + status=CompletionStatus.COMPLETED, + list_item_id="XLvVvS", + ), + ProgressDict( + section_id="personal-details-section", + block_ids=["proxy", "date-of-birth"], + status=CompletionStatus.COMPLETED, + list_item_id="mJPRpW", + ), + ], + ) + + self.data_stores.list_store = ListStore( + [ + { + "items": ["XLvVvS", "mJPRpW"], + "name": "people", + "primary_person": "XLvVvS", + } + ] + ) + + assert self.router.can_access_hub() + + def test_can_access_hub_with_required_sections_enabled_and_repeating_sections_incomplete( + self, + ): + self.schema = load_schema_from_name("test_hub_section_required_with_repeat") + + self.data_stores.progress_store = ProgressStore( + [ + ProgressDict( + section_id="list-collector-section", + block_ids=["primary-person-list-collector", "list-collector"], + status=CompletionStatus.COMPLETED, + ), + ProgressDict( + section_id="personal-details-section", + block_ids=["proxy", "date-of-birth"], + status=CompletionStatus.COMPLETED, + list_item_id="XLvVvS", + ), + ProgressDict( + section_id="personal-details-section", + block_ids=["proxy"], + status=CompletionStatus.IN_PROGRESS, + list_item_id="mJPRpW", + ), + ], + ) + + self.data_stores.list_store = ListStore( + [ + { + "items": ["XLvVvS", "mJPRpW"], + "name": "people", + "primary_person": "XLvVvS", + } + ] + ) + + assert not self.router.can_access_hub() + class TestRouterPathCompletion(RouterTestCase): def test_is_complete(self): self.schema = load_schema_from_name("test_textfield") - self.progress_store = ProgressStore( + self.data_stores.progress_store = ProgressStore( [ - { - "section_id": "default-section", - "list_item_id": None, - "status": CompletionStatus.IN_PROGRESS, - "block_ids": ["name-block"], - } + ProgressDict( + section_id="default-section", + list_item_id=None, + status=CompletionStatus.IN_PROGRESS, + block_ids=["name-block"], + ) ] ) - routing_path = self.router.routing_path(section_id="default-section") + routing_path = self.router.routing_path(SectionKey("default-section")) is_path_complete = self.router.is_path_complete(routing_path) assert is_path_complete @@ -142,7 +255,7 @@ def test_is_complete(self): def test_is_not_complete(self): self.schema = load_schema_from_name("test_textfield") - routing_path = self.router.routing_path(section_id="default-section") + routing_path = self.router.routing_path(SectionKey("default-section")) is_path_complete = self.router.is_path_complete(routing_path) assert not is_path_complete @@ -151,14 +264,14 @@ def test_is_not_complete(self): class TestRouterQuestionnaireCompletion(RouterTestCase): def test_is_complete(self): self.schema = load_schema_from_name("test_textfield") - self.progress_store = ProgressStore( + self.data_stores.progress_store = ProgressStore( [ - { - "section_id": "default-section", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["name-block"], - } + ProgressDict( + section_id="default-section", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["name-block"], + ) ] ) @@ -177,7 +290,7 @@ def test_is_complete_with_repeating_sections(self): self.schema = load_schema_from_name( "test_repeating_sections_with_hub_and_spoke" ) - self.progress_store = ProgressStore( + self.data_stores.progress_store = ProgressStore( [ { "section_id": "section", @@ -197,7 +310,7 @@ def test_is_complete_with_repeating_sections(self): }, ] ) - self.list_store = ListStore( + self.data_stores.list_store = ListStore( [{"items": ["abc123"], "name": "people", "primary_person": "abc123"}] ) @@ -209,7 +322,7 @@ def test_is_not_complete_with_repeating_sections(self): self.schema = load_schema_from_name( "test_repeating_sections_with_hub_and_spoke" ) - self.progress_store = ProgressStore( + self.data_stores.progress_store = ProgressStore( [ { "section_id": "default-section", @@ -218,7 +331,7 @@ def test_is_not_complete_with_repeating_sections(self): } ] ) - self.list_store = ListStore( + self.data_stores.list_store = ListStore( [ { "items": ["abc123", "123abc"], @@ -238,7 +351,9 @@ def test_can_access(self): self.schema = load_schema_from_name("test_textfield") current_location = Location(section_id="default-section", block_id="name-block") - routing_path = RoutingPath(["name-block"], section_id="default-section") + routing_path = RoutingPath( + block_ids=["name-block"], section_id="default-section" + ) can_access_location = self.router.can_access_location( current_location, routing_path ) @@ -254,7 +369,9 @@ def test_can_access_with_list_name_and_list_name_id(self): list_name="default-list", list_item_id="default-list-id", ) - routing_path = RoutingPath(["name-block"], section_id="default-section") + routing_path = RoutingPath( + block_ids=["name-block"], section_id="default-section" + ) can_access_location = self.router.can_access_location( current_location, routing_path ) @@ -266,7 +383,7 @@ def test_cant_access(self): "test_repeating_sections_with_hub_and_spoke" ) - self.list_store = ListStore( + self.data_stores.list_store = ListStore( [ { "items": ["abc123", "123abc"], @@ -288,7 +405,7 @@ def test_cant_access(self): assert not can_access_location def test_cant_access_section_disabled(self): - self.schema = load_schema_from_name("test_new_section_enabled_checkbox") + self.schema = load_schema_from_name("test_section_enabled_checkbox") current_location = Location( section_id="section-2", block_id="section-2-block", list_item_id=None @@ -316,7 +433,7 @@ def test_cant_access_not_on_allowable_path(self): section_id="default-section", block_id="set-duration-units-block" ) routing_path = RoutingPath( - [ + block_ids=[ "set-length-units-block", "set-duration-units-block", "set-area-units-block", @@ -335,14 +452,14 @@ class TestRouterNextLocation(RouterTestCase): @pytest.mark.usefixtures("app") def test_within_section(self): self.schema = load_schema_from_name("test_checkbox") - self.progress_store = ProgressStore( + self.data_stores.progress_store = ProgressStore( [ - { - "section_id": "default-section", - "list_item_id": None, - "status": CompletionStatus.IN_PROGRESS, - "block_ids": ["mandatory-checkbox"], - } + ProgressDict( + section_id="default-section", + list_item_id=None, + status=CompletionStatus.IN_PROGRESS, + block_ids=["mandatory-checkbox"], + ) ] ) @@ -350,11 +467,19 @@ def test_within_section(self): section_id="default-section", block_id="mandatory-checkbox" ) routing_path = RoutingPath( - ["mandatory-checkbox", "non-mandatory-checkbox", "single-checkbox"], + block_ids=[ + "mandatory-checkbox", + "non-mandatory-checkbox", + "single-checkbox", + ], section_id="default-section", ) + return_location = ReturnLocation() + next_location = self.router.get_next_location_url( - current_location, routing_path + current_location, + routing_path, + return_location, ) expected_location = Location( @@ -369,23 +494,27 @@ def test_last_block_in_section_but_section_is_not_complete_when_routing_backward ): self.schema = Mock() self.schema.get_block.return_value = {"type": "Question"} - self.progress_store = ProgressStore( + self.data_stores.progress_store = ProgressStore( [ - { - "section_id": "section-1", - "list_item_id": None, - "status": CompletionStatus.IN_PROGRESS, - "block_ids": ["block-1"], - } + ProgressDict( + section_id="section-1", + list_item_id=None, + status=CompletionStatus.IN_PROGRESS, + block_ids=["block-1"], + ) ] ) current_location = Location(section_id="section-1", block_id="block-1") # Simulates routing backwards. Last block in section does not mean section is complete. routing_path = RoutingPath( - ["block-1", "block-2", "block-1"], section_id="section-1" + block_ids=["block-1", "block-2", "block-1"], section_id="section-1" ) + return_location = ReturnLocation() + next_location = self.router.get_next_location_url( - current_location, routing_path + current_location, + routing_path, + return_location, ) assert "questionnaire/block-2/" in next_location @@ -393,7 +522,7 @@ def test_last_block_in_section_but_section_is_not_complete_when_routing_backward @pytest.mark.usefixtures("app") def test_return_to_section_summary_section_is_complete(self): self.schema = load_schema_from_name("test_section_summary") - self.progress_store = ProgressStore( + self.data_stores.progress_store = ProgressStore( [ { "section_id": "property-details-section", @@ -412,11 +541,13 @@ def test_return_to_section_summary_section_is_complete(self): section_id="property-details-section", block_id="insurance-type" ) routing_path = RoutingPath( - ["insurance-type", "insurance-address", "listed"], + block_ids=["insurance-type", "insurance-address", "listed"], section_id="property-details-section", ) + return_location = ReturnLocation(return_to="section-summary") + next_location = self.router.get_next_location_url( - current_location, routing_path, return_to="section-summary" + current_location, routing_path, return_location ) assert "/questionnaire/sections/property-details-section/" in next_location @@ -424,32 +555,39 @@ def test_return_to_section_summary_section_is_complete(self): @pytest.mark.usefixtures("app") def test_return_to_section_summary_section_is_in_progress(self): self.schema = load_schema_from_name("test_section_summary") - self.answer_store = AnswerStore( + self.data_stores.answer_store = AnswerStore( [ {"answer_id": "insurance-type-answer", "value": "Both"}, {"answer_id": "insurance-address-answer", "value": "Address"}, {"answer_id": "listed-answer", "value": "No"}, ] ) - self.progress_store = ProgressStore( + self.data_stores.progress_store = ProgressStore( [ - { - "section_id": "property-details-section", - "list_item_id": None, - "status": CompletionStatus.IN_PROGRESS, - "block_ids": ["insurance-type", "insurance-address", "listed"], - } + ProgressDict( + section_id="property-details-section", + list_item_id=None, + status=CompletionStatus.IN_PROGRESS, + block_ids=["insurance-type", "insurance-address", "listed"], + ) ] ) current_location = Location( section_id="property-details-section", block_id="insurance-address" ) routing_path = RoutingPath( - ["insurance-type", "insurance-address", "address-duration", "listed"], + block_ids=[ + "insurance-type", + "insurance-address", + "address-duration", + "listed", + ], section_id="property-details-section", ) + return_location = ReturnLocation(return_to="section-summary") + next_location = self.router.get_next_location_url( - current_location, routing_path, return_to="section-summary" + current_location, routing_path, return_location ) assert ( @@ -460,22 +598,25 @@ def test_return_to_section_summary_section_is_in_progress(self): @pytest.mark.usefixtures("app") def test_section_summary_on_completion_true(self): self.schema = load_schema_from_name("test_show_section_summary_on_completion") - self.progress_store = ProgressStore( + self.data_stores.progress_store = ProgressStore( [ - { - "section_id": "accommodation-section", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["proxy"], - } + ProgressDict( + section_id="accommodation-section", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["proxy"], + ) ] ) current_location = Location( section_id="accommodation-section", block_id="proxy" ) - routing_path = RoutingPath(["proxy"], section_id="default-section") + routing_path = RoutingPath(block_ids=["proxy"], section_id="default-section") + + return_location = ReturnLocation() + next_location = self.router.get_next_location_url( - current_location, routing_path + current_location, routing_path, return_location ) assert "questionnaire/sections/accommodation-section/" in next_location @@ -483,227 +624,1273 @@ def test_section_summary_on_completion_true(self): @pytest.mark.usefixtures("app") def test_section_summary_on_completion_false(self): self.schema = load_schema_from_name("test_show_section_summary_on_completion") - self.progress_store = ProgressStore( + self.data_stores.progress_store = ProgressStore( [ - { - "section_id": "employment-section", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["employment-status"], - } + ProgressDict( + section_id="employment-section", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["employment-status"], + ) ] ) current_location = Location( section_id="employment-section", block_id="employment-type" ) routing_path = RoutingPath( - ["employment-status", "employment-type"], section_id="employment-section" + block_ids=["employment-status", "employment-type"], + section_id="employment-section", ) + return_location = ReturnLocation() + next_location = self.router.get_next_location_url( - current_location, routing_path + current_location, routing_path, return_location ) expected_location_url = url_for("questionnaire.get_questionnaire") assert expected_location_url == next_location - -class TestRouterNextLocationLinearFlow(RouterTestCase): @pytest.mark.usefixtures("app") - def test_redirects_to_submit_page_when_questionnaire_complete( - self, - ): - self.schema = load_schema_from_name("test_textfield") - self.progress_store = ProgressStore( + @pytest.mark.parametrize( + "schema", + ("test_calculated_summary",), + ) + def test_return_to_calculated_summary(self, schema): + """ + This tests that when you hit continue on an edited answer for a calculated summary and all other dependent answers are complete + you are routed to the calculated summary, anchored to the answer that you edited + """ + self.schema = load_schema_from_name(schema) + # for the purposes of this test, assume the routing path consists only of the first two blocks and the calculated summary + # and that those two blocks are complete - this will be a sufficient condition to return to the calculated summary + self.data_stores.progress_store = ProgressStore( [ - { - "section_id": "default-section", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["name-block"], - } + ProgressDict( + section_id="default-section", + block_ids=[ + "first-number-block", + "second-number-block", + ], + status=CompletionStatus.IN_PROGRESS, + ) ] ) - current_location = Location(section_id="default-section", block_id="name-block") - routing_path = RoutingPath(["name-block"], section_id="default-section") - next_location = self.router.get_next_location_url( - current_location, routing_path + current_location = Location( + section_id="default-section", block_id="second-number-block" ) - assert url_for("questionnaire.submit_questionnaire") == next_location + routing_path = RoutingPath( + block_ids=[ + "first-number-block", + "second-number-block", + "currency-total-playback", + ], + section_id="default-section", + ) - @pytest.mark.usefixtures("app") - def test_return_to_final_summary_questionnaire_and_section_is_complete(self): - self.schema = load_schema_from_name( - "test_new_routing_to_questionnaire_end_single_section" + return_location = ReturnLocation( + return_to_answer_id="first-number-answer", + return_to="calculated-summary", + return_to_block_id="currency-total-playback", ) - self.progress_store = ProgressStore( - [ - { - "section_id": "test-section", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["test-forced"], - } - ] + + next_location_url = self.router.get_next_location_url( + current_location, routing_path, return_location ) - current_location = Location(section_id="test-section", block_id="test-forced") - routing_path = RoutingPath(["test-forced"], section_id="test-section") - next_location = self.router.get_next_location_url( - current_location, routing_path, return_to="final-summary" + + expected_location = Location( + section_id="default-section", + block_id="currency-total-playback", ) - assert url_for("questionnaire.submit_questionnaire") == next_location + expected_location_url = url_for( + "questionnaire.block", + list_item_id=expected_location.list_item_id, + block_id=expected_location.block_id, + _anchor="first-number-answer", + ) + + assert expected_location_url == next_location_url @pytest.mark.usefixtures("app") - def test_return_to_final_summary_section_is_in_progress(self): - self.schema = load_schema_from_name("test_submit_with_summary") - self.progress_store = ProgressStore( + @pytest.mark.parametrize( + "schema", + ( + "test_calculated_summary_dependent_questions", + "test_new_calculated_summary_dependent_questions", + ), + ) + def test_return_to_calculated_summary_not_on_allowable_path(self, schema): + """ + This tests that if you try to return to a calculated summary before all its dependencies have been answered + then you are instead routed to the first incomplete block of the section + """ + self.schema = load_schema_from_name(schema) + self.data_stores.progress_store = ProgressStore( [ - { - "section_id": "default-section", - "list_item_id": None, - "status": CompletionStatus.IN_PROGRESS, - "block_ids": ["radio", "dessert", "dessert-confirmation"], - } + ProgressDict( + section_id="default-section", + block_ids=["block-3"], + status=CompletionStatus.IN_PROGRESS, + ) ] ) - current_location = Location( - section_id="default-section", block_id="dessert-confirmation" - ) + + current_location = Location(section_id="default-section", block_id="block-3") + + # block 3 is complete, and block 4 is not, so block 4 should be routed to before the calculated summary routing_path = RoutingPath( - ["radio", "dessert", "dessert-confirmation", "numbers"], + block_ids=[ + "block-3", + "block-4", + "calculated-summary-block", + ], section_id="default-section", ) - next_location = self.router.get_next_location_url( - current_location, routing_path, return_to="final-summary" - ) - - assert "/questionnaire/numbers/?return_to=final-summary" in next_location - @pytest.mark.usefixtures("app") - def test_return_to_final_summary_questionnaire_is_not_complete(self): - self.schema = load_schema_from_name( - "test_new_routing_to_questionnaire_end_multiple_sections" - ) - self.answer_store = AnswerStore([{"answer_id": "test-answer", "value": "Yes"}]) - self.progress_store = ProgressStore( - [ - { - "section_id": "test-section", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["test-forced"], - } - ] + return_location = ReturnLocation( + return_to_answer_id="answer-3", + return_to="calculated-summary", + return_to_block_id="calculated-summary-block", ) - current_location = Location(section_id="test-section", block_id="test-forced") - routing_path = RoutingPath(["test-forced"], section_id="test-section") - next_location = self.router.get_next_location_url( - current_location, routing_path, return_to="final-summary" + next_location_url = self.router.get_next_location_url( + current_location, + routing_path, + return_location, ) + expected_location = Location( - section_id="test-section-2", - block_id="test-optional", - list_item_id=None, + section_id="default-section", + block_id="block-4", ) - assert expected_location.url() == next_location + expected_location_url = url_for( + "questionnaire.block", + list_item_id=expected_location.list_item_id, + block_id=expected_location.block_id, + return_to=return_location.return_to, + return_to_block_id=return_location.return_to_block_id, + return_to_answer_id=return_location.return_to_answer_id, + ) + assert expected_location_url == next_location_url -class TestRouterPreviousLocation(RouterTestCase): @pytest.mark.usefixtures("app") - def test_within_section(self): - self.schema = load_schema_from_name("test_checkbox") + @pytest.mark.parametrize( + "schema, return_to_block_id, expected_url", + [ + ( + "test_calculated_summary", + "non-valid-block", + "/questionnaire/sixth-number-block/?return_to=calculated-summary&return_to_block_id=non-valid-block", + ), + ( + "test_calculated_summary", + None, + "/questionnaire/sixth-number-block/?return_to=calculated-summary", + ), + ], + ) + def test_return_to_calculated_summary_invalid_return_to_block_id( + self, schema, return_to_block_id, expected_url + ): + self.schema = load_schema_from_name(schema) + self.data_stores.progress_store = ProgressStore( + [ + ProgressDict( + section_id="default-section", + block_ids=["fifth-number-block"], + status=CompletionStatus.IN_PROGRESS, + ) + ] + ) current_location = Location( - section_id="default-section", block_id="non-mandatory-checkbox" + section_id="default-section", block_id="fifth-number-block" ) routing_path = RoutingPath( - ["mandatory-checkbox", "non-mandatory-checkbox"], + block_ids=["fifth-number-block", "sixth-number-block"], section_id="default-section", ) - previous_location_url = self.router.get_previous_location_url( - current_location, routing_path + + return_location = ReturnLocation( + return_to="calculated-summary", + return_to_block_id=return_to_block_id, + ) + next_location_url = self.router.get_next_location_url( + current_location, + routing_path, + return_location, ) - expected_location_url = Location( - section_id="default-section", block_id="mandatory-checkbox" - ).url() - assert expected_location_url == previous_location_url + assert expected_url == next_location_url @pytest.mark.usefixtures("app") - def test_return_to_section_summary_section_is_complete(self): - self.schema = load_schema_from_name("test_section_summary") - self.progress_store = ProgressStore( + @pytest.mark.parametrize( + "schema", + ("test_calculated_summary",), + ) + def test_return_to_calculated_summary_return_to_block_id_not_on_path(self, schema): + self.schema = load_schema_from_name(schema) + self.data_stores.progress_store = ProgressStore( [ - { - "section_id": "property-details-section", - "list_item_id": None, - "status": CompletionStatus.COMPLETED, - "block_ids": ["insurance-type", "insurance-address", "listed"], - } + ProgressDict( + section_id="default-section", + block_ids=["fifth-number-block"], + status=CompletionStatus.IN_PROGRESS, + ) ] ) current_location = Location( - section_id="property-details-section", block_id="insurance-type" + section_id="default-section", block_id="fifth-number-block" ) + routing_path = RoutingPath( - ["insurance-type", "insurance-address", "listed"], + block_ids=["fifth-number-block", "sixth-number-block"], section_id="default-section", ) - previous_location_url = self.router.get_previous_location_url( - current_location, - routing_path, - return_to="section-summary", - return_to_answer_id="insurance-address-answer", + + return_location = ReturnLocation( + return_to="calculated-summary", + return_to_block_id="fourth-number-block", + ) + + next_location_url = self.router.get_next_location_url( + current_location, routing_path, return_location ) + # return_to_block_id is still passed here as although it is not currently on the path it may be in future once incomplete questions are + # answered so needs to be preserved assert ( - "/questionnaire/sections/property-details-section/#insurance-address-answer" - in previous_location_url + "/questionnaire/sixth-number-block/?return_to=calculated-summary&return_to_block_id=fourth-number-block" + == next_location_url ) @pytest.mark.usefixtures("app") - def test_return_to_section_summary_section_is_in_progress(self): - self.schema = load_schema_from_name("test_section_summary") - self.progress_store = ProgressStore( - [ - { - "section_id": "property-details-section", - "list_item_id": None, - "status": CompletionStatus.IN_PROGRESS, - "block_ids": ["insurance-type", "insurance-address", "listed"], - } - ] - ) + def test_return_to_grand_calculated_summary_from_answer( + self, grand_calculated_summary_progress_store, grand_calculated_summary_schema + ): + """ + If going from GCS -> CS -> answer -> CS -> GCS this tests going from CS -> GCS having just come from an answer + """ + self.schema = grand_calculated_summary_schema + self.data_stores.progress_store = grand_calculated_summary_progress_store current_location = Location( - section_id="property-details-section", block_id="insurance-address" + section_id="section-1", block_id="first-number-block" ) + routing_path = RoutingPath( - ["insurance-type", "insurance-address", "listed"], - section_id="default-section", + block_ids=["distance-calculated-summary-1"], + section_id="section-1", ) - previous_location_url = self.router.get_previous_location_url( - current_location, - routing_path, - return_to="section-summary", - return_to_answer_id="insurance-address-answer", + + return_location = ReturnLocation( + return_to="calculated-summary,grand-calculated-summary", + return_to_answer_id="q1-a1,distance-calculated-summary-1", + return_to_block_id="distance-calculated-summary-1,distance-grand-calculated-summary", ) - assert ( - "/questionnaire/insurance-type/?return_to=section-summary#insurance-address-answer" - in previous_location_url + next_location_url = self.router.get_next_location_url( + current_location, routing_path, return_location ) + expected_previous_url = url_for( + "questionnaire.block", + return_to="grand-calculated-summary", + block_id="distance-calculated-summary-1", + return_to_block_id="distance-grand-calculated-summary", + return_to_answer_id="distance-calculated-summary-1", + _anchor="q1-a1", + ) + + assert expected_previous_url == next_location_url + @pytest.mark.usefixtures("app") - def test_return_to_final_summary_section_is_complete(self): + def test_return_to_calculated_summary_from_answer_when_multiple_answers(self): + """ + If going from GCS -> CS -> answer -> CS -> GCS this tests going from CS -> GCS having just come from an answer + """ + self.schema = load_schema_from_name( + "test_grand_calculated_summary_overlapping_answers" + ) + self.data_stores.progress_store = ProgressStore( + [ + ProgressDict( + section_id="introduction-section", + block_ids=[ + "introduction-block", + ], + status=CompletionStatus.COMPLETED, + ), + ProgressDict( + section_id="section-1", + block_ids=[ + "block-1", + "block-2", + "calculated-summary-1", + "calculated-summary-2", + "block-3", + "calculated-summary-3", + ], + status=CompletionStatus.COMPLETED, + ), + ] + ) + + current_location = Location(section_id="section-1", block_id="block-1") + + routing_path = RoutingPath( + block_ids=[ + "block-1", + "block-2", + "calculated-summary-1", + "calculated-summary-2", + "block-3", + "calculated-summary-3", + ], + section_id="section-1", + ) + + return_location = ReturnLocation( + return_to="calculated-summary,grand-calculated-summary", + return_to_answer_id="q1-a1,calculated-summary-1", + return_to_block_id="calculated-summary-1,grand-calculated-summary-shopping", + ) + + next_location_url = self.router.get_next_location_url( + current_location, routing_path, return_location + ) + + assert ( + next_location_url + == "/questionnaire/calculated-summary-1/?return_to=grand-calculated-summary&return_to_block_id=grand-calculated-summary-shopping&" + "return_to_answer_id=calculated-summary-1#q1-a1" + ) + + @pytest.mark.usefixtures("app") + def test_return_to_grand_calculated_summary_from_answer_when_multiple_answers(self): + """ + If going from GCS -> CS -> answer -> CS -> GCS this tests going from CS -> GCS having just come from an answer + """ + self.schema = load_schema_from_name( + "test_grand_calculated_summary_overlapping_answers" + ) + self.data_stores.progress_store = ProgressStore( + [ + ProgressDict( + section_id="introduction-section", + block_ids=[ + "introduction-block", + ], + status=CompletionStatus.COMPLETED, + ), + ProgressDict( + section_id="section-1", + block_ids=[ + "block-1", + "block-2", + "calculated-summary-1", + "calculated-summary-2", + "block-3", + "calculated-summary-3", + ], + status=CompletionStatus.COMPLETED, + ), + ] + ) + + current_location = Location( + section_id="section-1", block_id="calculated-summary-1" + ) + + routing_path = RoutingPath( + block_ids=[ + "block-1", + "block-2", + "calculated-summary-1", + "calculated-summary-2", + "block-3", + "calculated-summary-3", + ], + section_id="section-1", + ) + + return_location = ReturnLocation( + return_to="grand-calculated-summary", + return_to_answer_id="calculated-summary-1", + return_to_block_id="grand-calculated-summary-shopping", + ) + + next_location_url = self.router.get_next_location_url( + current_location, routing_path, return_location + ) + + assert ( + next_location_url + == "/questionnaire/grand-calculated-summary-shopping/#calculated-summary-1" + ) + + @pytest.mark.usefixtures("app") + def test_return_to_grand_calculated_summary_from_calculated_summary( + self, grand_calculated_summary_progress_store, grand_calculated_summary_schema + ): + """ + If going from GCS -> CS -> GCS this tests going from CS -> GCS having just come from the grand calculated summary + """ + self.schema = grand_calculated_summary_schema + self.data_stores.progress_store = grand_calculated_summary_progress_store + + current_location = Location( + section_id="section-1", block_id="distance-calculated-summary-1" + ) + + routing_path = RoutingPath( + block_ids=["distance-calculated-summary-1"], + section_id="section-1", + ) + + return_location = ReturnLocation( + return_to="grand-calculated-summary", + return_to_answer_id="distance-calculated-summary-1", + return_to_block_id="distance-grand-calculated-summary", + ) + next_location_url = self.router.get_next_location_url( + current_location, + routing_path, + return_location, + ) + + expected_previous_url = url_for( + "questionnaire.block", + block_id="distance-grand-calculated-summary", + _anchor="distance-calculated-summary-1", + ) + + assert expected_previous_url == next_location_url + + @pytest.mark.parametrize( + "section_id,block_id,list_name,list_item_id,return_to_list_item_id", + [ + ( + "base-costs-section", + "calculated-summary-base-cost", + None, + None, + "ZIrqqR", + ), + ( + "vehicle-details-section", + "calculated-summary-running-costs", + "vehicles", + "ZIrqqR", + None, + ), + ], + ) + @pytest.mark.usefixtures("app") + def test_return_to_repeating_grand_calculated_summary_from_calculated_summary( + self, + section_id, + block_id, + list_name, + list_item_id, + return_to_list_item_id, + grand_calculated_summary_in_repeating_section_schema, + ): + """ + This tests that if you use a change link from a repeating GCS to return to: + either a non-repeating CS in another section or a repeating CS in the same section, + the continue button for the CS has a next location url of the original repeating GCS. + """ + self.schema = grand_calculated_summary_in_repeating_section_schema + self.data_stores.list_store = ListStore( + [{"items": ["ZIrqqR"], "name": "vehicles"}] + ) + + self.data_stores.progress_store = ProgressStore( + [ + ProgressDict( + section_id="base-costs-section", + block_ids=[ + "any-cost", + "finance-cost", + "calculated-summary-base-cost", + ], + status=CompletionStatus.COMPLETED, + ), + ProgressDict( + section_id="vehicle-details-section", + block_ids=[ + "vehicle-maintenance-block", + "vehicle-fuel-block", + "calculated-summary-running-cost", + "grand-calculated-summary-vehicle", + ], + status=CompletionStatus.COMPLETED, + list_item_id="ZIrqqR", + ), + ] + ) + current_location = Location( + section_id=section_id, + block_id=block_id, + list_name=list_name, + list_item_id=list_item_id, + ) + routing_path = RoutingPath( + block_ids=[ + "vehicle-maintenance-block", + "vehicle-fuel-block", + "calculated-summary-running-cost", + "grand-calculated-summary-vehicle", + ], + section_id="vehicle-details-section", + list_name="vehicles", + list_item_id="ZIrqqR", + ) + return_location = ReturnLocation( + return_to="grand-calculated-summary", + return_to_block_id="grand-calculated-summary-vehicle", + return_to_list_item_id=return_to_list_item_id, + ) + next_location_url = self.router.get_next_location_url( + current_location, routing_path, return_location + ) + expected_next_url = url_for( + "questionnaire.block", + list_name="vehicles", + list_item_id="ZIrqqR", + block_id="grand-calculated-summary-vehicle", + ) + + assert expected_next_url == next_location_url + + @pytest.mark.parametrize( + "return_to_block_id", + ("grand-calculated-summary-1", "grand-calculated-summary-2"), + ) + @pytest.mark.usefixtures("app") + def test_return_to_grand_calculated_summary_from_incomplete_section( + self, return_to_block_id + ): + """ + This tests that if you try to return to a grand calculated summary from an incomplete section + (or the same section but before the dependencies of the grand calculated summary are complete) + you are routed to the next block in the incomplete section rather than the grand calculated summary + """ + self.schema = load_schema_from_name( + "test_grand_calculated_summary_repeating_answers" + ) + # calculated summary 3 is not complete yet + self.data_stores.progress_store = ProgressStore( + [ + ProgressDict( + section_id="section-1", + block_ids=[ + "block-1", + "block-2", + "calculated-summary-1", + "block-3", + "calculated-summary-2", + ], + status=CompletionStatus.IN_PROGRESS, + ) + ] + ) + + current_location = Location(section_id="section-1", block_id="block-2") + routing_path = RoutingPath( + block_ids=[ + "block-1", + "block-2", + "calculated-summary-1", + "block-3", + "calculated-summary-2", + "calculated-summary-3", + "grand-calculated-summary-1", + ], + section_id="section-1", + ) + return_location = ReturnLocation( + return_to="grand-calculated-summary", + return_to_answer_id="calculated-summary-1", + return_to_block_id=return_to_block_id, + ) + next_location_url = self.router.get_next_location_url( + current_location, + routing_path, + return_location, + ) + + # because calculated summary 3 isn't done, should go there before jumping to the grand calculated summary + # test from grand-calculated-summary-1 which is in the same section, and grand-calculated-summary-2 which is in another + expected_next_url = url_for( + "questionnaire.block", + return_to="grand-calculated-summary", + return_to_block_id=return_to_block_id, + return_to_answer_id=return_location.return_to_answer_id, + block_id="calculated-summary-3", + ) + + assert expected_next_url == next_location_url + + @pytest.mark.usefixtures("app") + def test_return_to_calculated_summary_from_incomplete_section( + self, grand_calculated_summary_schema + ): + """ + This tests that if you try to return to a calculated summary section from an incomplete section + you are routed to the next block in the incomplete section rather than the calculated summary + """ + self.schema = grand_calculated_summary_schema + # second-number block not complete yet + self.data_stores.progress_store = ProgressStore( + [ + ProgressDict( + section_id="section-1", + block_ids=[ + "first-number-block", + "distance-calculated-summary-1", + ], + status=CompletionStatus.IN_PROGRESS, + ) + ] + ) + + current_location = Location( + section_id="section-1", block_id="first-number-block" + ) + routing_path = RoutingPath( + block_ids=[ + "first-number-block", + "second-number-block", + "distance-calculated-summary-1", + "number-calculated-summary-1", + ], + section_id="section-1", + ) + return_location = ReturnLocation( + return_to="calculated-summary,grand-calculated-summary", + return_to_answer_id="q1-a1,distance-calculated-summary-1", + return_to_block_id="distance-calculated-summary-1,distance-grand-calculated-summary", + ) + # the test is being done as part of a two-step return to but its identical functionally + next_location_url = self.router.get_next_location_url( + current_location, + routing_path, + return_location, + ) + + # should take you to the second-number-block before going back to the calculated summary + expected_next_url = url_for( + "questionnaire.block", + return_to="calculated-summary,grand-calculated-summary", + return_to_block_id="distance-calculated-summary-1,distance-grand-calculated-summary", + return_to_answer_id="q1-a1,distance-calculated-summary-1", + block_id="second-number-block", + ) + + assert expected_next_url == next_location_url + + @pytest.mark.parametrize( + "schema,section,block_id,return_to,return_to_block_id,routing_path_block_ids,next_incomplete_block_id", + [ + ( + "test_list_collector_repeating_blocks_section_summary", + "section-companies", + "responsible-party", + "section-summary", + None, + [ + "responsible-party", + "any-other-companies-or-branches", + "any-companies-or-branches", + "any-other-trading-details", + ], + "any-other-companies-or-branches", + ), + ( + "test_list_collector_repeating_blocks_section_summary", + "section-companies", + "any-other-companies-or-branches", + "submit", + None, + [ + "responsible-party", + "any-other-companies-or-branches", + "any-companies-or-branches", + "any-other-trading-details", + ], + "any-other-trading-details", + ), + ( + "test_new_calculated_summary_repeating_and_static_answers", + "section-1", + "list-collector", + "section-1", + None, + [ + "any-supermarket", + "list-collector", + "dynamic-answer", + "extra-spending-block", + "extra-spending-method-block", + "calculated-summary-spending", + "calculated-summary-visits", + ], + "extra-spending-method-block", + ), + ( + "test_new_calculated_summary_repeating_and_static_answers", + "section-1", + "dynamic-answer", + "section-1", + "calculated-summary-visits", + [ + "any-supermarket", + "list-collector", + "dynamic-answer", + "extra-spending-block", + "extra-spending-method-block", + "calculated-summary-spending", + ], + "calculated-summary-spending", + ), + ], + ) + @pytest.mark.usefixtures("app") + def test_return_to_inaccessible_summary_routes_to_next_incomplete_block( + self, + schema, + section, + block_id, + return_to, + return_to_block_id, + routing_path_block_ids, + next_incomplete_block_id, + ): + """ + This tests that if you try to return to a section/final/calculated summary which is not yet accessible + then you route to the next incomplete block in the section with all return to parameters preserved + """ + self.schema = load_schema_from_name(schema) + current_location = Location(section_id=section, block_id=block_id) + routing_path = RoutingPath(block_ids=routing_path_block_ids, section_id=section) + + # make a copy where next_incomplete_block_id is not yet completed + completed_block_ids = [*routing_path_block_ids] + completed_block_ids.remove(next_incomplete_block_id) + + self.data_stores.progress_store = ProgressStore( + [ + ProgressDict( + section_id=section, + block_ids=completed_block_ids, + status=CompletionStatus.IN_PROGRESS, + ) + ] + ) + + return_location = ReturnLocation( + return_to=return_to, + return_to_block_id=return_to_block_id, + ) + + next_location_url = self.router.get_next_location_url( + current_location, routing_path, return_location + ) + + expected_next_url = url_for( + "questionnaire.block", + return_to=return_to, + return_to_block_id=return_to_block_id, + block_id=next_incomplete_block_id, + ) + + assert expected_next_url == next_location_url + + +class TestRouterNextLocationLinearFlow(RouterTestCase): + @pytest.mark.usefixtures("app") + def test_redirects_to_submit_page_when_questionnaire_complete( + self, + ): + self.schema = load_schema_from_name("test_textfield") + self.data_stores.progress_store = ProgressStore( + [ + ProgressDict( + section_id="default-section", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["name-block"], + ) + ] + ) + + current_location = Location(section_id="default-section", block_id="name-block") + routing_path = RoutingPath( + block_ids=["name-block"], section_id="default-section" + ) + return_location = ReturnLocation() + next_location = self.router.get_next_location_url( + current_location, routing_path, return_location + ) + + assert url_for("questionnaire.submit_questionnaire") == next_location + + @pytest.mark.usefixtures("app") + def test_return_to_final_summary_questionnaire_and_section_is_complete(self): + self.schema = load_schema_from_name( + "test_routing_to_questionnaire_end_single_section" + ) + self.data_stores.progress_store = ProgressStore( + [ + ProgressDict( + section_id="test-section", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["test-forced"], + ) + ] + ) + current_location = Location(section_id="test-section", block_id="test-forced") + routing_path = RoutingPath(block_ids=["test-forced"], section_id="test-section") + return_location = ReturnLocation(return_to="final-summary") + next_location = self.router.get_next_location_url( + current_location, routing_path, return_location + ) + + assert url_for("questionnaire.submit_questionnaire") == next_location + + @pytest.mark.usefixtures("app") + def test_return_to_final_summary_section_is_in_progress(self): self.schema = load_schema_from_name("test_submit_with_summary") - self.progress_store = ProgressStore( + self.data_stores.progress_store = ProgressStore( + [ + ProgressDict( + section_id="default-section", + list_item_id=None, + status=CompletionStatus.IN_PROGRESS, + block_ids=["radio", "dessert", "dessert-confirmation"], + ) + ] + ) + current_location = Location( + section_id="default-section", block_id="dessert-confirmation" + ) + routing_path = RoutingPath( + block_ids=["radio", "dessert", "dessert-confirmation", "numbers"], + section_id="default-section", + ) + return_location = ReturnLocation(return_to="final-summary") + next_location = self.router.get_next_location_url( + current_location, routing_path, return_location + ) + + assert "/questionnaire/numbers/?return_to=final-summary" in next_location + + @pytest.mark.usefixtures("app") + def test_return_to_final_summary_questionnaire_is_not_complete(self): + self.schema = load_schema_from_name( + "test_routing_to_questionnaire_end_multiple_sections" + ) + self.data_stores.answer_store = AnswerStore( + [{"answer_id": "test-answer", "value": "Yes"}] + ) + self.data_stores.progress_store = ProgressStore( + [ + ProgressDict( + section_id="test-section", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["test-forced"], + ) + ] + ) + + current_location = Location(section_id="test-section", block_id="test-forced") + routing_path = RoutingPath(block_ids=["test-forced"], section_id="test-section") + return_location = ReturnLocation(return_to="final-summary") + next_location = self.router.get_next_location_url( + current_location, routing_path, return_location + ) + expected_location = Location( + section_id="test-section-2", + block_id="test-optional", + list_item_id=None, + ) + + assert expected_location.url() == next_location + + +class TestRouterPreviousLocation(RouterTestCase): + @pytest.mark.usefixtures("app") + def test_within_section(self): + self.schema = load_schema_from_name("test_checkbox") + + current_location = Location( + section_id="default-section", block_id="non-mandatory-checkbox" + ) + + routing_path = RoutingPath( + block_ids=["mandatory-checkbox", "non-mandatory-checkbox"], + section_id="default-section", + ) + return_location = ReturnLocation() + previous_location_url = self.router.get_previous_location_url( + current_location, routing_path, return_location + ) + expected_location_url = Location( + section_id="default-section", block_id="mandatory-checkbox" + ).url() + + assert expected_location_url == previous_location_url + + @pytest.mark.usefixtures("app") + def test_return_to_calculated_summary(self): + self.schema = load_schema_from_name("test_calculated_summary") + + current_location = Location( + section_id="default-section", block_id="second-number-block" + ) + + routing_path = RoutingPath( + block_ids=[ + "currency-total-playback-skipped-fourth", + ], + section_id="default-section", + ) + + return_location = ReturnLocation( + return_to="calculated-summary", + return_to_answer_id="first-number-answer", + return_to_block_id="currency-total-playback-skipped-fourth", + ) + + previous_location_url = self.router.get_previous_location_url( + current_location, + routing_path, + return_location, + ) + + expected_location = Location( + section_id="default-section", + block_id="currency-total-playback-skipped-fourth", + ) + + expected_location_url = url_for( + "questionnaire.block", + list_item_id=expected_location.list_item_id, + block_id=expected_location.block_id, + _anchor="first-number-answer", + ) + + assert expected_location_url == previous_location_url + + @pytest.mark.usefixtures("app") + def test_return_to_grand_calculated_summary_from_answer_incomplete_section( + self, grand_calculated_summary_schema + ): + """ + This tests that if you are on a calculated summary, and your return_to_block_id is another calculated summary that you cannot reach yet + if you click previous, then you are taken to the previous block in the section + (rather than the first incomplete block of the section which is what next location would return) + """ + self.schema = grand_calculated_summary_schema + # trying to go to number-calculated-summary-1 but distance-calculated-summary-1 which comes before is not complete yet + self.data_stores.progress_store = ProgressStore( + [ + ProgressDict( + section_id="section-1", + block_ids=[ + "first-number-block", + "second-number-block", + "number-calculated-summary-1", + ], + status=CompletionStatus.IN_PROGRESS, + ) + ] + ) + + current_location = Location( + section_id="section-1", block_id="second-number-block" + ) + + routing_path = RoutingPath( + block_ids=[ + "first-number-block", + "second-number-block", + "distance-calculated-summary-1", + "number-calculated-summary-1", + ], + section_id="section-1", + ) + + return_location = ReturnLocation( + return_to="calculated-summary,grand-calculated-summary", + return_to_answer_id="q2-a1", + return_to_block_id="number-calculated-summary-1,number-grand-calculated-summary", + ) + previous_location_url = self.router.get_previous_location_url( + current_location, + routing_path, + return_location, + ) + # return to can't go to the distance calculated summary, so go to previous block with return params preserved + expected_previous_url = url_for( + "questionnaire.block", + return_to="calculated-summary,grand-calculated-summary", + return_to_block_id="number-calculated-summary-1,number-grand-calculated-summary", + block_id="first-number-block", + _anchor="q2-a1", + ) + + assert expected_previous_url == previous_location_url + + @pytest.mark.usefixtures("app") + def test_return_to_grand_calculated_summary_from_calculated_summary_incomplete_section( + self, grand_calculated_summary_schema + ): + """ + This tests that if you are on a calculated summary, and your return_to_block_id is a grand calculated summary + if you click previous, then you are taken to the previous block in the section + (rather than the first incomplete block of the section which is what next location would return) + """ + self.schema = grand_calculated_summary_schema + # number calculated summary is not complete, so the section is not complete + self.data_stores.progress_store = ProgressStore( + [ + ProgressDict( + section_id="section-1", + block_ids=[ + "first-number-block", + "second-number-block", + "distance-calculated-summary-1", + ], + status=CompletionStatus.IN_PROGRESS, + ) + ] + ) + + current_location = Location( + section_id="section-1", block_id="distance-calculated-summary-1" + ) + + routing_path = RoutingPath( + block_ids=[ + "first-number-block", + "second-number-block", + "distance-calculated-summary-1", + "number-calculated-summary-1", + ], + section_id="section-1", + ) + + return_location = ReturnLocation( + return_to="grand-calculated-summary", + return_to_answer_id="distance-calculated-summary-1", + return_to_block_id="distance-grand-calculated-summary", + ) + previous_location_url = self.router.get_previous_location_url( + current_location, routing_path, return_location + ) + # return to can't go to the grand calculated summary, so routing is just to the previous block in the section with return params preserved + expected_previous_url = url_for( + "questionnaire.block", + return_to="grand-calculated-summary", + return_to_block_id="distance-grand-calculated-summary", + block_id="second-number-block", + _anchor="distance-calculated-summary-1", + ) + + assert expected_previous_url == previous_location_url + + @pytest.mark.parametrize( + "return_to, current_block, return_to_block_id, expected_url", + [ + ( + "grand-calculated-summary", + "distance-calculated-summary-1", + "invalid-block", + "/questionnaire/second-number-block/?return_to=grand-calculated-summary&return_to_block_id=invalid-block#distance-calculated-summary-1", + ), + ( + "calculated-summary,invalid", + "second-number-block", + "invalid-1,invalid-2", + "/questionnaire/first-number-block/?return_to=calculated-summary,invalid&return_to_block_id=invalid-1,invalid-2#distance-calculated-summary-1", + ), + ( + "invalid", + "distance-calculated-summary-1", + "first-number-block", + "/questionnaire/second-number-block/?return_to=invalid&return_to_block_id=first-number-block#distance-calculated-summary-1", + ), + ], + ) + @pytest.mark.usefixtures("app") + def test_return_to_grand_calculated_summary_invalid_url( + self, + return_to, + current_block, + return_to_block_id, + expected_url, + grand_calculated_summary_schema, + ): + self.schema = grand_calculated_summary_schema + + current_location = Location(section_id="section-1", block_id=current_block) + + routing_path = RoutingPath( + block_ids=[ + "first-number-block", + "second-number-block", + "distance-calculated-summary-1", + "number-calculated-summary-1", + ], + section_id="section-1", + ) + + return_location = ReturnLocation( + return_to=return_to, + return_to_answer_id="distance-calculated-summary-1", + return_to_block_id=return_to_block_id, + ) + previous_location_url = self.router.get_previous_location_url( + current_location, + routing_path, + return_location, + ) + + assert expected_url == previous_location_url + + @pytest.mark.usefixtures("app") + def test_return_to_grand_calculated_summary_from_repeating_answer( + self, + grand_calculated_summary_repeating_answers_progress_store, + grand_calculated_summary_repeating_answers_schema, + ): + """ + Test returning to a calculated summary from a list repeating question as part of a grand calculated summary change link + """ + self.schema = grand_calculated_summary_repeating_answers_schema + self.data_stores.progress_store = ( + grand_calculated_summary_repeating_answers_progress_store + ) + + parent_location = Location( + section_id="section-5", + block_id="any-other-streaming-services", + ) + + routing_path = RoutingPath( + block_ids=["calculated-summary-6"], + section_id="section-5", + ) + + return_location = ReturnLocation( + return_to="calculated-summary,grand-calculated-summary", + return_to_answer_id="streaming-service-monthly-cost-JSfZqh,calculated-summary-6", + return_to_block_id="calculated-summary-6,grand-calculated-summary-3", + ) + next_location_url = self.router.get_previous_location_url( + parent_location, + routing_path, + return_location, + ) + + expected_previous_url = url_for( + "questionnaire.block", + return_to="grand-calculated-summary", + block_id="calculated-summary-6", + return_to_block_id="grand-calculated-summary-3", + return_to_answer_id="calculated-summary-6", + _anchor="streaming-service-monthly-cost-JSfZqh", + ) + + assert expected_previous_url == next_location_url + + @pytest.mark.usefixtures("app") + def test_return_to_section_summary_section_is_complete(self): + self.schema = load_schema_from_name("test_section_summary") + self.data_stores.progress_store = ProgressStore( + [ + ProgressDict( + section_id="property-details-section", + list_item_id=None, + status=CompletionStatus.COMPLETED, + block_ids=["insurance-type", "insurance-address", "listed"], + ) + ] + ) + + current_location = Location( + section_id="property-details-section", block_id="insurance-type" + ) + routing_path = RoutingPath( + block_ids=["insurance-type", "insurance-address", "listed"], + section_id="default-section", + ) + return_location = ReturnLocation( + return_to="section-summary", + return_to_answer_id="insurance-address-answer", + ) + previous_location_url = self.router.get_previous_location_url( + current_location, + routing_path, + return_location, + ) + + assert ( + "/questionnaire/sections/property-details-section/#insurance-address-answer" + in previous_location_url + ) + + @pytest.mark.usefixtures("app") + def test_return_to_section_summary_section_is_in_progress(self): + self.schema = load_schema_from_name("test_section_summary") + self.data_stores.progress_store = ProgressStore( + [ + ProgressDict( + section_id="property-details-section", + list_item_id=None, + status=CompletionStatus.IN_PROGRESS, + block_ids=["insurance-type", "insurance-address", "listed"], + ) + ] + ) + + current_location = Location( + section_id="property-details-section", block_id="insurance-address" + ) + routing_path = RoutingPath( + block_ids=["insurance-type", "insurance-address", "listed"], + section_id="default-section", + ) + return_location = ReturnLocation( + return_to="section-summary", + return_to_answer_id="insurance-address-answer", + ) + previous_location_url = self.router.get_previous_location_url( + current_location, + routing_path, + return_location, + ) + + assert ( + "/questionnaire/insurance-type/?return_to=section-summary#insurance-address-answer" + in previous_location_url + ) + + @pytest.mark.usefixtures("app") + def test_return_to_final_summary_section_is_complete(self): + self.schema = load_schema_from_name("test_submit_with_summary") + self.data_stores.progress_store = ProgressStore( [ { "section_id": "default-section", @@ -721,14 +1908,17 @@ def test_return_to_final_summary_section_is_complete(self): current_location = Location(section_id="default-section", block_id="radio") routing_path = RoutingPath( - ["radio", "dessert", "dessert-confirmation", "numbers"], + block_ids=["radio", "dessert", "dessert-confirmation", "numbers"], section_id="default-section", ) + return_location = ReturnLocation( + return_to="final-summary", + return_to_answer_id="dessert-answer", + ) previous_location = self.router.get_previous_location_url( current_location, routing_path, - return_to="final-summary", - return_to_answer_id="dessert-answer", + return_location, ) assert "/questionnaire/submit/#dessert-answer" in previous_location @@ -736,7 +1926,7 @@ def test_return_to_final_summary_section_is_complete(self): @pytest.mark.usefixtures("app") def test_return_to_final_summary_section_is_in_progress(self): self.schema = load_schema_from_name("test_submit_with_summary") - self.progress_store = ProgressStore( + self.data_stores.progress_store = ProgressStore( [ { "section_id": "default-section", @@ -754,14 +1944,17 @@ def test_return_to_final_summary_section_is_in_progress(self): current_location = Location(section_id="default-section", block_id="dessert") routing_path = RoutingPath( - ["radio", "dessert", "dessert-confirmation", "numbers"], + block_ids=["radio", "dessert", "dessert-confirmation", "numbers"], section_id="default-section", ) + return_location = ReturnLocation( + return_to="final-summary", + return_to_answer_id="dessert-answer", + ) previous_location = self.router.get_previous_location_url( current_location, routing_path, - return_to="final-summary", - return_to_answer_id="dessert-answer", + return_location, ) assert ( @@ -774,26 +1967,34 @@ class TestRouterPreviousLocationLinearFlow(RouterTestCase): @pytest.mark.usefixtures("app") def test_is_none_on_first_block_single_section(self): self.schema = load_schema_from_name("test_checkbox") - self.progress_store = ProgressStore( + self.data_stores.progress_store = ProgressStore( [ - { - "section_id": "default-section", - "list_item_id": None, - "status": CompletionStatus.IN_PROGRESS, - "block_ids": ["mandatory-checkbox"], - } + ProgressDict( + section_id="default-section", + list_item_id=None, + status=CompletionStatus.IN_PROGRESS, + block_ids=["mandatory-checkbox"], + ) ] ) routing_path = RoutingPath( - ["mandatory-checkbox", "non-mandatory-checkbox", "single-checkbox"], + block_ids=[ + "mandatory-checkbox", + "non-mandatory-checkbox", + "single-checkbox", + ], section_id="default-section", ) current_location = Location( section_id="default-section", block_id="mandatory-checkbox" ) + return_location = ReturnLocation() + previous_location_url = self.router.get_previous_location_url( - current_location, routing_path + current_location, + routing_path, + return_location, ) assert previous_location_url is None @@ -801,7 +2002,7 @@ def test_is_none_on_first_block_single_section(self): @pytest.mark.usefixtures("app") def test_is_none_on_first_block_second_section(self): self.schema = load_schema_from_name("test_section_summary") - self.progress_store = ProgressStore( + self.data_stores.progress_store = ProgressStore( [ { "section_id": "property-details-section", @@ -819,9 +2020,15 @@ def test_is_none_on_first_block_second_section(self): current_location = Location( section_id="house-details-section", block_id="house-type" ) - routing_path = RoutingPath(["house-type"], section_id="house-details-section") + routing_path = RoutingPath( + block_ids=["house-type"], section_id="house-details-section" + ) + return_location = ReturnLocation() + previous_location_url = self.router.get_previous_location_url( - current_location, routing_path + current_location, + routing_path, + return_location, ) assert previous_location_url is None @@ -837,10 +2044,16 @@ def test_is_not_none_on_first_block_in_section(self): ) routing_path = RoutingPath( - ["employment-status", "employment-type"], section_id="employment-section" + block_ids=["employment-status", "employment-type"], + section_id="employment-section", ) + + return_location = ReturnLocation() + previous_location_url = self.router.get_previous_location_url( - current_location, routing_path + current_location, + routing_path, + return_location, ) assert url_for("questionnaire.get_questionnaire") == previous_location_url @@ -850,17 +2063,17 @@ class TestRouterLastLocationLinearFlow(RouterTestCase): @pytest.mark.usefixtures("app") def test_block_on_path(self): self.schema = load_schema_from_name("test_checkbox") - self.progress_store = ProgressStore( + self.data_stores.progress_store = ProgressStore( [ - { - "section_id": "default-section", - "block_ids": [ + ProgressDict( + section_id="default-section", + block_ids=[ "mandatory-checkbox", "non-mandatory-checkbox", "single-checkbox", ], - "status": CompletionStatus.COMPLETED, - } + status=CompletionStatus.COMPLETED, + ) ] ) last_location_url = self.router.get_last_location_in_questionnaire_url() @@ -873,9 +2086,9 @@ def test_block_on_path(self): @pytest.mark.usefixtures("app") def test_last_block_not_on_path(self): self.schema = load_schema_from_name( - "test_new_routing_to_questionnaire_end_multiple_sections" + "test_routing_to_questionnaire_end_multiple_sections" ) - self.answer_store = AnswerStore( + self.data_stores.answer_store = AnswerStore( [ {"answer_id": "test-answer", "value": "No"}, { @@ -887,13 +2100,13 @@ def test_last_block_not_on_path(self): section_id = "test-section" last_block_on_path = "test-forced" completed_block_not_on_path = "test-optional" - self.progress_store = ProgressStore( + self.data_stores.progress_store = ProgressStore( [ - { - "section_id": section_id, - "block_ids": [last_block_on_path, completed_block_not_on_path], - "status": CompletionStatus.COMPLETED, - } + ProgressDict( + section_id=section_id, + block_ids=[last_block_on_path, completed_block_not_on_path], + status=CompletionStatus.COMPLETED, + ) ] ) @@ -904,8 +2117,8 @@ def test_last_block_not_on_path(self): ).url() last_completed_block_in_progress_store = ( - self.progress_store.get_completed_block_ids( - section_id=section_id, list_item_id=None + self.data_stores.progress_store.get_completed_block_ids( + SectionKey(section_id) )[-1] ) @@ -914,25 +2127,48 @@ def test_last_block_not_on_path(self): assert completed_block_not_on_path == last_completed_block_in_progress_store assert expected_location_url == last_location_url + @pytest.mark.usefixtures("app") + def test_list_collector_final_summary_returns_to_section_summary(self): + self.schema = load_schema_from_name("test_list_collector_list_summary") + + self.data_stores.progress_store = ProgressStore( + [ + ProgressDict( + section_id="section", + block_ids=[ + "introduction", + "primary-person-list-collector", + "list-collector", + "visitor-list-collector", + ], + status=CompletionStatus.COMPLETED, + ) + ] + ) + + last_location_url = self.router.get_last_location_in_questionnaire_url() + + assert "/questionnaire/sections/section/" == last_location_url + class TestRouterSectionResume(RouterTestCase): @pytest.mark.usefixtures("app") def test_section_in_progress_returns_url_for_first_incomplete_location(self): self.schema = load_schema_from_name("test_section_summary") - self.progress_store = ProgressStore( + self.data_stores.progress_store = ProgressStore( [ - { - "section_id": "property-details-section", - "list_item_id": None, - "status": CompletionStatus.IN_PROGRESS, - "block_ids": ["insurance-type"], - } + ProgressDict( + section_id="property-details-section", + list_item_id=None, + status=CompletionStatus.IN_PROGRESS, + block_ids=["insurance-type"], + ) ] ) section_routing_path = RoutingPath( - ["insurance-type", "insurance-address"], + block_ids=["insurance-type", "insurance-address"], section_id="property-details-section", ) @@ -947,18 +2183,19 @@ def test_section_complete_returns_url_for_first_location( self, ): self.schema = load_schema_from_name("test_hub_complete_sections") - self.progress_store = ProgressStore( + self.data_stores.progress_store = ProgressStore( [ - { - "section_id": "employment-section", - "block_ids": ["employment-status", "employment-type"], - "status": CompletionStatus.COMPLETED, - } + ProgressDict( + section_id="employment-section", + block_ids=["employment-status", "employment-type"], + status=CompletionStatus.COMPLETED, + ) ], ) routing_path = RoutingPath( - ["employment-status", "employment-type"], section_id="employment-section" + block_ids=["employment-status", "employment-type"], + section_id="employment-section", ) section_resume_url = self.router.get_section_resume_url( @@ -966,3 +2203,62 @@ def test_section_complete_returns_url_for_first_location( ) assert "questionnaire/employment-status/" in section_resume_url + + @pytest.mark.usefixtures("app") + def test_return_to_calculated_summary_no_return_to_answer_id(self): + self.schema = load_schema_from_name("test_validation_sum_against_value_source") + self.data_stores.progress_store = ProgressStore( + [ + ProgressDict( + section_id="default-section", + block_ids=[ + "total-block", + "breakdown-block", + "number-total-playback", + ], + status=CompletionStatus.IN_PROGRESS, + ) + ] + ) + + current_location = Location( + section_id="default-section", block_id="second-breakdown-block" + ) + + routing_path = RoutingPath( + block_ids=[ + "total-block", + "breakdown-block", + "number-total-playback", + "second-breakdown-block", + "another-number-total-playback", + ], + section_id="default-section", + ) + + return_location = ReturnLocation( + return_to_answer_id=None, + return_to="calculated-summary", + return_to_block_id="another-number-total-playback", + ) + + next_location_url = self.router.get_next_location_url( + current_location, + routing_path, + return_location, + ) + + expected_location = Location( + section_id="default-section", + block_id="second-breakdown-block", + ) + + expected_location_url = url_for( + "questionnaire.block", + block_id=expected_location.block_id, + return_to=return_location.return_to, + return_to_block_id=return_location.return_to_block_id, + return_to_answer_id=return_location.return_to_answer_id, + ) + + assert expected_location_url == next_location_url diff --git a/tests/app/questionnaire/test_routing_path.py b/tests/app/questionnaire/test_routing_path.py index da64c2b54d..226e2fac69 100644 --- a/tests/app/questionnaire/test_routing_path.py +++ b/tests/app/questionnaire/test_routing_path.py @@ -1,10 +1,10 @@ from app.questionnaire.routing_path import RoutingPath +from app.utilities.types import SectionKey def test_eq_to_routing_path(block_ids, routing_path): - assert routing_path == RoutingPath( - block_ids, + block_ids=block_ids, section_id="section-1", list_item_id="list_item_id", list_name="list_name", @@ -41,3 +41,4 @@ def test_properties(block_ids, routing_path): assert "section-1" == routing_path.section_id assert "list_item_id" == routing_path.list_item_id assert "list_name" == routing_path.list_name + assert SectionKey("section-1", "list_item_id") == routing_path.section_key diff --git a/tests/app/questionnaire/test_schema_utils.py b/tests/app/questionnaire/test_schema_utils.py index 6880e44ceb..9ebc6b1c3d 100644 --- a/tests/app/questionnaire/test_schema_utils.py +++ b/tests/app/questionnaire/test_schema_utils.py @@ -1,5 +1,5 @@ from app.data_models.answer_store import Answer, AnswerStore -from app.data_models.list_store import ListStore +from app.data_models.data_stores import DataStores from app.questionnaire.location import Location from app.questionnaire.questionnaire_schema import QuestionnaireSchema from app.questionnaire.variants import ( @@ -19,8 +19,6 @@ def test_transform_variants_with_question_variants(question_variant_schema): schema = QuestionnaireSchema(question_variant_schema) answer_store = AnswerStore({}) answer_store.add_or_update(Answer(answer_id="when-answer", value="no")) - metadata = {} - response_metadata = {} block = schema.get_block("block1") section_id = schema.get_section_id_for_block_id(block["id"]) @@ -28,11 +26,8 @@ def test_transform_variants_with_question_variants(question_variant_schema): transformed_block = transform_variants( block, schema, - metadata, - response_metadata, - answer_store, - ListStore({}), - Location(section_id=section_id, block_id=block["id"]), + data_stores=DataStores(answer_store=answer_store), + current_location=Location(section_id=section_id, block_id=block["id"]), ) compare_transformed_block(block, transformed_block, "Question 1, No") @@ -42,11 +37,8 @@ def test_transform_variants_with_question_variants(question_variant_schema): transformed_block = transform_variants( block, schema, - metadata, - response_metadata, - answer_store, - ListStore({}), - Location(section_id=section_id, block_id=block["id"]), + data_stores=DataStores(answer_store=answer_store), + current_location=Location(section_id=section_id, block_id=block["id"]), ) compare_transformed_block(block, transformed_block, "Question 1, Yes") @@ -55,9 +47,7 @@ def test_transform_variants_with_question_variants(question_variant_schema): def test_transform_variants_with_content(content_variant_schema): schema = QuestionnaireSchema(content_variant_schema) answer_store = AnswerStore({}) - answer_store.add_or_update(Answer(answer_id="age-answer", value="18")) - metadata = {} - response_metadata = {} + answer_store.add_or_update(Answer(answer_id="age-answer", value=18)) block = schema.get_block("block1") section_id = schema.get_section_id_for_block_id(block["id"]) @@ -65,23 +55,18 @@ def test_transform_variants_with_content(content_variant_schema): transformed_block = transform_variants( block, schema, - metadata, - response_metadata, - answer_store, - ListStore({}), - Location(section_id=section_id, block_id=block["id"]), + data_stores=DataStores(answer_store=answer_store), + current_location=Location(section_id=section_id, block_id=block["id"]), ) assert transformed_block != block assert "content_variants" not in transformed_block - assert transformed_block["content"][0]["title"] == "You are over 16" + assert transformed_block["content"]["title"] == "You are over 16" def test_transform_variants_with_no_variants(question_schema): schema = QuestionnaireSchema(question_schema) answer_store = AnswerStore({}) - metadata = {} - response_metadata = {} block = schema.get_block("block1") section_id = schema.get_section_id_for_block_id(block["id"]) @@ -89,11 +74,8 @@ def test_transform_variants_with_no_variants(question_schema): transformed_block = transform_variants( block, schema, - metadata, - response_metadata, - answer_store, - ListStore({}), - Location(section_id=section_id, block_id=block["id"]), + data_stores=DataStores(answer_store=answer_store), + current_location=Location(section_id=section_id, block_id=block["id"]), ) assert transformed_block == block @@ -103,8 +85,6 @@ def test_transform_variants_list_collector(list_collector_variant_schema): schema = QuestionnaireSchema(list_collector_variant_schema) answer_store = AnswerStore({}) answer_store.add_or_update(Answer(answer_id="when-answer", value="no")) - metadata = {} - response_metadata = {} block = schema.get_block("block1") section_id = schema.get_section_id_for_block_id(block["id"]) @@ -112,11 +92,8 @@ def test_transform_variants_list_collector(list_collector_variant_schema): transformed_block = transform_variants( block, schema, - metadata, - response_metadata, - answer_store, - ListStore({}), - Location(section_id=section_id, block_id=block["id"]), + data_stores=DataStores(answer_store=answer_store), + current_location=Location(section_id=section_id, block_id=block["id"]), ) compare_transformed_block( @@ -134,11 +111,8 @@ def test_transform_variants_list_collector(list_collector_variant_schema): transformed_block = transform_variants( block, schema, - metadata, - response_metadata, - answer_store, - ListStore({}), - Location(section_id=section_id, block_id=block["id"]), + data_stores=DataStores(answer_store=answer_store), + current_location=Location(section_id=section_id, block_id=block["id"]), ) compare_transformed_block( @@ -155,7 +129,7 @@ def test_transform_variants_list_collector(list_collector_variant_schema): def test_choose_content_to_display(content_variant_schema): schema = QuestionnaireSchema(content_variant_schema) answer_store = AnswerStore({}) - answer_store.add_or_update(Answer(answer_id="age-answer", value="18")) + answer_store.add_or_update(Answer(answer_id="age-answer", value=18)) metadata = {} response_metadata = {} @@ -165,36 +139,36 @@ def test_choose_content_to_display(content_variant_schema): content_to_display = choose_content_to_display( schema.get_block("block1"), schema, - metadata, - response_metadata, - answer_store, - ListStore({}), - Location(section_id=section_id, block_id=block["id"]), + data_stores=DataStores( + answer_store=answer_store, + metadata=metadata, + response_metadata=response_metadata, + ), + current_location=Location(section_id=section_id, block_id=block["id"]), ) - assert content_to_display[0]["title"] == "You are over 16" + assert content_to_display["title"] == "You are over 16" answer_store = AnswerStore({}) content_to_display = choose_content_to_display( schema.get_block("block1"), schema, - metadata, - response_metadata, - answer_store, - ListStore({}), - Location(section_id=section_id, block_id=block["id"]), + data_stores=DataStores( + answer_store=answer_store, + metadata=metadata, + response_metadata=response_metadata, + ), + current_location=Location(section_id=section_id, block_id=block["id"]), ) - assert content_to_display[0]["title"] == "You are ageless" + assert content_to_display["title"] == "You are ageless" def test_choose_question_to_display(question_variant_schema): schema = QuestionnaireSchema(question_variant_schema) answer_store = AnswerStore({}) answer_store.add_or_update(Answer(answer_id="when-answer", value="yes")) - metadata = {} - response_metadata = {} block = schema.get_block("block1") section_id = schema.get_section_id_for_block_id(block["id"]) @@ -202,11 +176,8 @@ def test_choose_question_to_display(question_variant_schema): question_to_display = choose_question_to_display( schema.get_block("block1"), schema, - metadata, - response_metadata, - answer_store, - ListStore({}), - Location(section_id=section_id, block_id=block["id"]), + data_stores=DataStores(answer_store=answer_store), + current_location=Location(section_id=section_id, block_id=block["id"]), ) assert question_to_display["title"] == "Question 1, Yes" @@ -216,11 +187,8 @@ def test_choose_question_to_display(question_variant_schema): question_to_display = choose_question_to_display( schema.get_block("block1"), schema, - metadata, - response_metadata, - answer_store, - ListStore({}), - Location(section_id=section_id, block_id=block["id"]), + data_stores=DataStores(answer_store=answer_store), + current_location=Location(section_id=section_id, block_id=block["id"]), ) assert question_to_display["title"] == "Question 1, No" diff --git a/tests/app/questionnaire/test_value_source_resolver.py b/tests/app/questionnaire/test_value_source_resolver.py index 79deb00fd9..0e4e43fcd0 100644 --- a/tests/app/questionnaire/test_value_source_resolver.py +++ b/tests/app/questionnaire/test_value_source_resolver.py @@ -1,15 +1,19 @@ -from typing import Mapping, Optional, Union -from unittest.mock import Mock +# pylint: disable=too-many-lines import pytest +from mock import MagicMock, Mock -from app.data_models import AnswerStore, ListStore +from app.data_models import AnswerStore, ListStore, ProgressStore from app.data_models.answer import Answer, AnswerDict +from app.data_models.data_stores import DataStores +from app.data_models.metadata_proxy import NoMetadataException +from app.data_models.supplementary_data_store import InvalidSupplementaryDataSelector from app.questionnaire import Location, QuestionnaireSchema from app.questionnaire.location import InvalidLocationException from app.questionnaire.relationship_location import RelationshipLocation from app.questionnaire.value_source_resolver import ValueSourceResolver from tests.app.data_model.test_answer import ESCAPED_CONTENT, HTML_CONTENT +from tests.app.questionnaire.conftest import get_metadata def get_list_items(num: int): @@ -17,7 +21,7 @@ def get_list_items(num: int): def get_mock_schema(): - schema = Mock( + schema = MagicMock( QuestionnaireSchema( { "questionnaire_flow": { @@ -27,35 +31,57 @@ def get_mock_schema(): } ) ) + schema.is_answer_dynamic = Mock(return_value=False) + schema.is_answer_in_list_collector_repeating_block = Mock(return_value=False) return schema +def get_calculation_block( + block_id: str, summary_type: str, source_type: str, identifiers: list[str] +) -> dict: + return { + "id": block_id, + "type": summary_type, + "calculation": { + "operation": { + "+": [ + { + "source": source_type, + "identifier": identifier, + } + for identifier in identifiers + ] + } + }, + } + + def get_value_source_resolver( schema: QuestionnaireSchema = None, - answer_store: AnswerStore = AnswerStore(), - list_store: ListStore = ListStore(), - metadata: Optional[dict] = None, - response_metadata: Mapping = None, - location: Union[Location, RelationshipLocation] = Location( + data_stores: DataStores = None, + location: Location | RelationshipLocation = Location( section_id="test-section", block_id="test-block" ), - list_item_id: Optional[str] = None, - routing_path_block_ids: Optional[list] = None, + list_item_id: str | None = None, + routing_path_block_ids: list | None = None, use_default_answer=False, escape_answer_values=False, ): + data_stores = data_stores or DataStores() if not schema: schema = get_mock_schema() schema.is_repeating_answer = Mock(return_value=bool(list_item_id)) + schema.get_list_name_for_answer_id = Mock( + return_value="list" if list_item_id else None + ) + schema.is_answer_dynamic = Mock(return_value=False) + schema.is_answer_in_list_collector_repeating_block = Mock(return_value=False) if not use_default_answer: schema.get_default_answer = Mock(return_value=None) return ValueSourceResolver( - answer_store=answer_store, - list_store=list_store, - metadata=metadata, - response_metadata=response_metadata, + data_stores=data_stores, schema=schema, location=location, list_item_id=list_item_id, @@ -67,7 +93,9 @@ def get_value_source_resolver( def test_answer_source(): value_source_resolver = get_value_source_resolver( - answer_store=AnswerStore([{"answer_id": "some-answer", "value": "Yes"}]), + data_stores=DataStores( + answer_store=AnswerStore([{"answer_id": "some-answer", "value": "Yes"}]) + ), ) assert ( @@ -80,14 +108,16 @@ def test_answer_source(): def test_answer_source_with_dict_answer_selector(): value_source_resolver = get_value_source_resolver( - answer_store=AnswerStore( - [ - { - "answer_id": "some-answer", - "value": {"years": 1, "months": 10}, - } - ] - ), + data_stores=DataStores( + answer_store=AnswerStore( + [ + { + "answer_id": "some-answer", + "value": {"years": 1, "months": 10}, + } + ] + ), + ) ) assert ( @@ -104,8 +134,10 @@ def test_answer_source_with_dict_answer_selector(): def test_answer_source_with_list_item_id_no_list_item_selector(): value_source_resolver = get_value_source_resolver( - answer_store=AnswerStore( - [{"answer_id": "some-answer", "list_item_id": "item-1", "value": "Yes"}] + data_stores=DataStores( + answer_store=AnswerStore( + [{"answer_id": "some-answer", "list_item_id": "item-1", "value": "Yes"}] + ) ), list_item_id="item-1", ) @@ -120,11 +152,13 @@ def test_answer_source_with_list_item_id_no_list_item_selector(): def test_list_item_id_ignored_if_answer_not_in_list_collector_or_repeat(): schema = get_mock_schema() - schema.is_repeating_answer = Mock(return_value=False) + schema.get_list_name_for_answer_id = Mock(return_value=None) value_source_resolver = get_value_source_resolver( schema=schema, - answer_store=AnswerStore([{"answer_id": "some-answer", "value": "Yes"}]), + data_stores=DataStores( + answer_store=AnswerStore([{"answer_id": "some-answer", "value": "Yes"}]) + ), list_item_id="item-1", ) @@ -138,14 +172,16 @@ def test_list_item_id_ignored_if_answer_not_in_list_collector_or_repeat(): def test_answer_source_with_list_item_selector_location(): value_source_resolver = get_value_source_resolver( - answer_store=AnswerStore( - [ - { - "answer_id": "some-answer", - "list_item_id": "item-1", - "value": "Yes", - } - ] + data_stores=DataStores( + answer_store=AnswerStore( + [ + { + "answer_id": "some-answer", + "list_item_id": "item-1", + "value": "Yes", + } + ] + ) ), location=Location( section_id="some-section", block_id="some-block", list_item_id="item-1" @@ -169,14 +205,16 @@ def test_answer_source_with_list_item_selector_location(): def test_answer_source_with_list_item_selector_location_none(): value_source_resolver = get_value_source_resolver( - answer_store=AnswerStore( - [ - { - "answer_id": "some-answer", - "list_item_id": "item-1", - "value": "Yes", - } - ] + data_stores=DataStores( + answer_store=AnswerStore( + [ + { + "answer_id": "some-answer", + "list_item_id": "item-1", + "value": "Yes", + } + ] + ) ), location=None, ) @@ -192,16 +230,18 @@ def test_answer_source_with_list_item_selector_location_none(): def test_answer_source_with_list_item_selector_list_first_item(): value_source_resolver = get_value_source_resolver( - answer_store=AnswerStore( - [ - { - "answer_id": "some-answer", - "list_item_id": "item-1", - "value": "Yes", - } - ] + data_stores=DataStores( + answer_store=AnswerStore( + [ + { + "answer_id": "some-answer", + "list_item_id": "item-1", + "value": "Yes", + } + ] + ), + list_store=ListStore([{"name": "some-list", "items": get_list_items(3)}]), ), - list_store=ListStore([{"name": "some-list", "items": get_list_items(3)}]), ) assert ( @@ -223,13 +263,15 @@ def test_answer_source_with_list_item_selector_list_first_item(): def test_answer_source_outside_of_repeating_section(): schema = get_mock_schema() - schema.is_repeating_answer = Mock(return_value=False) + schema.get_list_name_for_answer_id = Mock(return_value=None) answer_store = AnswerStore([{"answer_id": "some-answer", "value": "Yes"}]) value_source_resolver = get_value_source_resolver( schema=schema, - answer_store=answer_store, - list_store=ListStore([{"name": "some-list", "items": get_list_items(3)}]), + data_stores=DataStores( + answer_store=answer_store, + list_store=ListStore([{"name": "some-list", "items": get_list_items(3)}]), + ), location=Location( section_id="some-section", block_id="some-block", list_item_id="item-1" ), @@ -262,8 +304,10 @@ def test_answer_source_not_on_path_non_repeating_section(is_answer_on_path): value_source_resolver = get_value_source_resolver( schema=schema, - answer_store=AnswerStore([answer.to_dict()]), - list_store=ListStore([{"name": "some-list", "items": get_list_items(3)}]), + data_stores=DataStores( + answer_store=AnswerStore([answer.to_dict()]), + list_store=ListStore([{"name": "some-list", "items": get_list_items(3)}]), + ), location=location, list_item_id=location.list_item_id, routing_path_block_ids=["block-on-path"], @@ -280,7 +324,7 @@ def test_answer_source_not_on_path_non_repeating_section(is_answer_on_path): @pytest.mark.parametrize("is_answer_on_path", [True, False]) def test_answer_source_not_on_path_repeating_section(is_answer_on_path): schema = get_mock_schema() - schema.is_repeating_answer = Mock(return_value=True) + schema.get_list_name_for_answer_id = Mock(return_value="some-list") location = Location( section_id="test-section", block_id="test-block", list_item_id="item-1" ) @@ -298,8 +342,10 @@ def test_answer_source_not_on_path_repeating_section(is_answer_on_path): value_source_resolver = get_value_source_resolver( schema=schema, - answer_store=AnswerStore([answer.to_dict()]), - list_store=ListStore([{"name": "some-list", "items": get_list_items(3)}]), + data_stores=DataStores( + answer_store=AnswerStore([answer.to_dict()]), + list_store=ListStore([{"name": "some-list", "items": get_list_items(3)}]), + ), location=location, list_item_id=location.list_item_id, routing_path_block_ids=["block-on-path"], @@ -337,13 +383,206 @@ def test_answer_source_default_answer(use_default_answer): ) +@pytest.mark.parametrize( + "answer_values,escape_answer_values", + (([], False), ([10, 5], False), ([100, 200, 300], False), ([HTML_CONTENT], True)), +) +def test_answer_source_dynamic_answer( + mocker, + placeholder_transform_question_dynamic_answers_json, + answer_values, + escape_answer_values, +): + """ + Tests that a dynamic answer id as a value source resolves to the list of answers for that list and question + """ + schema = mocker.MagicMock() + schema.is_answer_dynamic = Mock(return_value=True) + schema.get_block_for_answer_id = Mock( + return_value={"question": placeholder_transform_question_dynamic_answers_json} + ) + list_item_ids = get_list_items(len(answer_values)) + value_source_resolver = get_value_source_resolver( + data_stores=DataStores( + answer_store=AnswerStore( + [ + AnswerDict( + answer_id="percentage-of-shopping", + value=value, + list_item_id=list_item_id, + ) + for list_item_id, value in zip(list_item_ids, answer_values) + ] + ), + list_store=ListStore([{"name": "supermarkets", "items": list_item_ids}]), + ), + schema=schema, + escape_answer_values=escape_answer_values, + ) + expected_result = [ESCAPED_CONTENT] if escape_answer_values else answer_values + assert ( + value_source_resolver.resolve( + {"source": "answers", "identifier": "percentage-of-shopping"} + ) + == expected_result + ) + + +@pytest.mark.parametrize( + "answer_values, list_name, list_item_id, expected", + ( + ([10, 5], "supermarkets", "item-1", 10), + ([10, 5], "cars", "item-1", [10, 5]), + ([10, 5], None, None, [10, 5]), + ), +) +def test_answer_source_dynamic_answer_in_different_repeat( + placeholder_transform_question_dynamic_answers_json, + answer_values, + list_name, + list_item_id, + expected, +): + """ + Tests that a dynamic answer id as a value source resolves to the specific instance in the context of a repeat + and the list of all answers when outside a repeat or in a repeat for a different list. + """ + schema = MagicMock() + schema.is_answer_dynamic = Mock(return_value=True) + schema.get_block_for_answer_id = Mock( + return_value={"question": placeholder_transform_question_dynamic_answers_json} + ) + schema.get_list_name_for_answer_id = Mock(return_value="supermarkets") + list_item_ids = get_list_items(len(answer_values)) + value_source_resolver = get_value_source_resolver( + data_stores=DataStores( + answer_store=AnswerStore( + [ + AnswerDict( + answer_id="percentage-of-shopping", + value=value, + list_item_id=list_item_id, + ) + for list_item_id, value in zip(list_item_ids, answer_values) + ] + ), + list_store=ListStore([{"name": "supermarkets", "items": list_item_ids}]), + ), + location=Location( + section_id="section-1", list_name=list_name, list_item_id=list_item_id + ), + list_item_id=list_item_id, + schema=schema, + ) + assert ( + value_source_resolver.resolve( + {"source": "answers", "identifier": "percentage-of-shopping"} + ) + == expected + ) + + +@pytest.mark.parametrize( + "answer_values, list_name, list_item_id, expected", + ( + ([], None, None, []), + ([10, 5], None, None, [10, 5]), + ([100, 200, 300], None, None, [100, 200, 300]), + ([10, 5], "transport", "item-1", 10), + ([100, 200, 300], "transport", "item-2", 200), + ([10, 5], "shopping", "item-1", [10, 5]), + ([100, 200, 300], "shopping", "item-2", [100, 200, 300]), + ), +) +def test_answer_source_repeating_block_answers_in_repeat( + placeholder_transform_question_repeating_block, + answer_values, + list_name, + list_item_id, + expected, +): + """ + Tests that an answer id from a repeating block resolves to the specific answer in the context of a repeat. + And the list of answers for that list and repeating block question outside a repeat or in a repeat for another list. + """ + schema = MagicMock() + schema.list_names_by_list_repeating_block_id = {"repeating-block-1": "transport"} + schema.is_answer_dynamic = Mock(return_value=False) + schema.get_list_name_for_answer_id = Mock(return_value="transport") + schema.get_block_for_answer_id = Mock( + return_value=placeholder_transform_question_repeating_block + ) + list_item_ids = get_list_items(len(answer_values)) + value_source_resolver = get_value_source_resolver( + data_stores=DataStores( + answer_store=AnswerStore( + [ + AnswerDict( + answer_id="transport-cost", + value=value, + list_item_id=list_item_id, + ) + for list_item_id, value in zip(list_item_ids, answer_values) + ] + ), + list_store=ListStore([{"name": "transport", "items": list_item_ids}]), + ), + schema=schema, + location=Location( + section_id="section-1", list_name=list_name, list_item_id=list_item_id + ), + list_item_id=list_item_id, + ) + assert ( + value_source_resolver.resolve( + {"source": "answers", "identifier": "transport-cost"} + ) + == expected + ) + + @pytest.mark.parametrize( "metadata_identifier, expected_result", [("region_code", "GB-ENG"), ("language_code", None)], ) def test_metadata_source(metadata_identifier, expected_result): value_source_resolver = get_value_source_resolver( - metadata={"region_code": "GB-ENG"}, + data_stores=DataStores( + metadata=get_metadata(extra_metadata={"region_code": "GB-ENG"}) + ) + ) + + source = {"source": "metadata", "identifier": metadata_identifier} + assert value_source_resolver.resolve(source) == expected_result + + +def test_resolve_metadata_source_with_no_metadata_raises_exception(): + value_source_resolver = get_value_source_resolver() + + source = {"source": "metadata", "identifier": "identifier"} + + with pytest.raises(NoMetadataException): + value_source_resolver.resolve(source) + + +@pytest.mark.parametrize( + "metadata_identifier, expected_result", + [ + ("region_code", "GB-ENG"), + ("display_address", "68 Abingdon Road, Goathill"), + ("language_code", None), + ], +) +def test_metadata_source_v2_metadata_structure(metadata_identifier, expected_result): + metadata = get_metadata( + extra_metadata={ + "region_code": "GB-ENG", + "display_address": "68 Abingdon Road, Goathill", + }, + ) + + value_source_resolver = get_value_source_resolver( + data_stores=DataStores(metadata=metadata) ) source = {"source": "metadata", "identifier": metadata_identifier} @@ -356,8 +595,10 @@ def test_metadata_source(metadata_identifier, expected_result): ) def test_list_source(list_count): value_source_resolver = get_value_source_resolver( - list_store=ListStore( - [{"name": "some-list", "items": get_list_items(list_count)}] + data_stores=DataStores( + list_store=ListStore( + [{"name": "some-list", "items": get_list_items(list_count)}] + ) ), ) @@ -371,7 +612,9 @@ def test_list_source(list_count): def test_list_source_with_id_selector_first(): value_source_resolver = get_value_source_resolver( - list_store=ListStore([{"name": "some-list", "items": get_list_items(3)}]), + data_stores=DataStores( + list_store=ListStore([{"name": "some-list", "items": get_list_items(3)}]) + ), ) assert ( @@ -384,28 +627,27 @@ def test_list_source_with_id_selector_first(): def test_list_source_with_id_selector_same_name_items(): value_source_resolver = get_value_source_resolver( - list_store=ListStore( - [ - { - "name": "some-list", - "items": get_list_items(5), - "same_name_items": get_list_items(3), - } - ] - ), - ) - - assert ( - value_source_resolver.resolve( - { - "source": "list", - "identifier": "some-list", - "selector": "same_name_items", - } + data_stores=DataStores( + list_store=ListStore( + [ + { + "name": "some-list", + "items": get_list_items(5), + "same_name_items": get_list_items(3), + } + ] + ), ) - == get_list_items(3) ) + assert value_source_resolver.resolve( + { + "source": "list", + "identifier": "some-list", + "selector": "same_name_items", + } + ) == get_list_items(3) + @pytest.mark.parametrize( "primary_person_list_item_id", @@ -413,15 +655,17 @@ def test_list_source_with_id_selector_same_name_items(): ) def test_list_source_id_selector_primary_person(primary_person_list_item_id): value_source_resolver = get_value_source_resolver( - list_store=ListStore( - [ - { - "name": "some-list", - "primary_person": primary_person_list_item_id, - "items": get_list_items(3), - } - ] - ), + data_stores=DataStores( + list_store=ListStore( + [ + { + "name": "some-list", + "primary_person": primary_person_list_item_id, + "items": get_list_items(3), + } + ] + ), + ) ) assert ( @@ -448,7 +692,9 @@ def test_location_source(): def test_response_metadata_source(): value_source_resolver = get_value_source_resolver( - response_metadata={"started_at": "2021-10-11T09:40:11.220038+00:00"} + data_stores=DataStores( + response_metadata={"started_at": "2021-10-11T09:40:11.220038+00:00"} + ) ) assert ( value_source_resolver.resolve( @@ -476,15 +722,17 @@ def test_calculated_summary_value_source(mocker, list_item_id): ) value_source_resolver = get_value_source_resolver( - answer_store=AnswerStore( - [ - AnswerDict( - answer_id="number-answer-1", value=10, list_item_id=list_item_id - ), - AnswerDict( - answer_id="number-answer-2", value=5, list_item_id=list_item_id - ), - ] + data_stores=DataStores( + answer_store=AnswerStore( + [ + AnswerDict( + answer_id="number-answer-1", value=10, list_item_id=list_item_id + ), + AnswerDict( + answer_id="number-answer-2", value=5, list_item_id=list_item_id + ), + ] + ) ), schema=schema, list_item_id=list_item_id, @@ -497,6 +745,217 @@ def test_calculated_summary_value_source(mocker, list_item_id): ) +@pytest.mark.parametrize( + "list_item_id", + [None, "item-1"], +) +def test_new_calculated_summary_value_source(mocker, list_item_id): + schema = mocker.MagicMock() + schema.get_block = Mock( + return_value={ + "id": "number-total", + "type": "CalculatedSummary", + "calculation": { + "operation": { + "+": [ + {"source": "answers", "identifier": "number-answer-1"}, + {"source": "answers", "identifier": "number-answer-2"}, + ] + } + }, + }, + ) + schema.is_answer_dynamic = Mock(return_value=False) + schema.is_answer_in_list_collector_repeating_block = Mock(return_value=False) + + location = Location( + section_id="test-section", block_id="test-block", list_item_id=list_item_id + ) + + value_source_resolver = get_value_source_resolver( + data_stores=DataStores( + answer_store=AnswerStore( + [ + AnswerDict( + answer_id="number-answer-1", + value=10, + list_item_id=location.list_item_id, + ), + AnswerDict( + answer_id="number-answer-2", value=5, list_item_id=list_item_id + ), + ] + ) + ), + schema=schema, + list_item_id=list_item_id, + location=location, + ) + assert ( + value_source_resolver.resolve( + {"source": "calculated_summary", "identifier": "number-total"} + ) + == 15 + ) + + +@pytest.mark.parametrize( + "list_item_id", + [None, "item-1"], +) +def test_new_calculated_summary_nested_value_source(mocker, list_item_id): + schema = mocker.MagicMock() + schema.get_block = Mock( + return_value={ + "id": "number-total", + "type": "CalculatedSummary", + "calculation": { + "operation": { + "+": [ + { + "+": [ + {"source": "answers", "identifier": "number-answer-1"}, + {"source": "answers", "identifier": "number-answer-2"}, + ] + }, + {"source": "answers", "identifier": "number-answer-3"}, + ] + } + }, + }, + ) + schema.is_answer_dynamic = Mock(return_value=False) + schema.is_answer_in_list_collector_repeating_block = Mock(return_value=False) + + location = Location( + section_id="test-section", block_id="test-block", list_item_id=list_item_id + ) + + value_source_resolver = get_value_source_resolver( + data_stores=DataStores( + answer_store=AnswerStore( + [ + AnswerDict( + answer_id="number-answer-1", value=10, list_item_id=list_item_id + ), + AnswerDict( + answer_id="number-answer-2", value=5, list_item_id=list_item_id + ), + AnswerDict( + answer_id="number-answer-3", value=5, list_item_id=list_item_id + ), + ] + ) + ), + schema=schema, + list_item_id=list_item_id, + location=location, + ) + assert ( + value_source_resolver.resolve( + {"source": "calculated_summary", "identifier": "number-total"} + ) + == 20 + ) + + +@pytest.mark.parametrize( + "gcs_list_item_id, cs_list_item_id_1, cs_list_item_id_2", + [ + (None, None, None), + ("item-1", "item-1", "item-1"), + ("item-1", "item-1", None), + ("item-1", None, None), + ], +) +def test_grand_calculated_summary_value_source( + mocker, gcs_list_item_id, cs_list_item_id_1, cs_list_item_id_2 +): + """ + Mocks out the grand calculated summary block and its child calculated summary blocks and tests + that the value source resolver correctly sums up all child answers when + 1) The GCS is in a repeat alongside both CS + 2) The GCS is in a repeat alongside one CS + 3) The GCS is in a repeat but neither CS is + 3) The GCS is not in a repeat and neither CS is + """ + schema = mocker.MagicMock() + + def mock_get_block(block_id: str) -> dict: + blocks = { + "number-total": get_calculation_block( + "number-total", + "GrandCalculatedSummary", + "calculated_summary", + ["calculated-summary-1", "calculated-summary-2"], + ), + "calculated-summary-1": get_calculation_block( + "calculated-summary-1", + "CalculatedSummary", + "answers", + ["answer-1", "answer-2"], + ), + "calculated-summary-2": get_calculation_block( + "calculated-summary-2", + "CalculatedSummary", + "answers", + ["answer-3", "answer-4"], + ), + } + return blocks[block_id] + + def mock_get_list_name_for_answer_id(answer_id: str) -> str | None: + return ( + "mock-list" + if (answer_id in {"answer-1", "answer-2"} and cs_list_item_id_1) + or (answer_id in {"answer-3", "answer-4"} and cs_list_item_id_2) + else None + ) + + schema.get_block = Mock(side_effect=mock_get_block) + schema.get_list_name_for_answer_id = Mock( + side_effect=mock_get_list_name_for_answer_id + ) + schema.is_answer_dynamic = Mock(return_value=False) + schema.is_answer_in_list_collector_repeating_block = Mock(return_value=False) + + location = Location( + section_id="test-section", + block_id="test-block", + list_item_id=gcs_list_item_id, + ) + + value_source_resolver = get_value_source_resolver( + data_stores=DataStores( + answer_store=AnswerStore( + [ + AnswerDict( + answer_id="answer-1", value=10, list_item_id=cs_list_item_id_1 + ), + AnswerDict( + answer_id="answer-2", value=5, list_item_id=cs_list_item_id_1 + ), + AnswerDict( + answer_id="answer-3", value=20, list_item_id=cs_list_item_id_2 + ), + AnswerDict( + answer_id="answer-4", value=30, list_item_id=cs_list_item_id_2 + ), + ] + ) + ), + schema=schema, + list_item_id=gcs_list_item_id, + location=location, + ) + assert ( + value_source_resolver.resolve( + {"source": "grand_calculated_summary", "identifier": "number-total"} + ) + == 65 + ) + + @pytest.mark.parametrize( "answer_value, escaped_value", [ @@ -508,13 +967,15 @@ def test_calculated_summary_value_source(mocker, list_item_id): ) def test_answer_value_can_be_escaped(answer_value, escaped_value): value_source_resolver = get_value_source_resolver( - answer_store=AnswerStore( - [ - { - "answer_id": "some-answer", - "value": answer_value, - } - ] + data_stores=DataStores( + answer_store=AnswerStore( + [ + { + "answer_id": "some-answer", + "value": answer_value, + } + ] + ) ), escape_answer_values=True, ) @@ -528,13 +989,15 @@ def test_answer_value_can_be_escaped(answer_value, escaped_value): def test_answer_value_with_selector_can_be_escaped(): value_source_resolver = get_value_source_resolver( - answer_store=AnswerStore( - [ - { - "answer_id": "some-answer", - "value": {"key_1": HTML_CONTENT, "key_2": 1}, - } - ] + data_stores=DataStores( + answer_store=AnswerStore( + [ + { + "answer_id": "some-answer", + "value": {"key_1": HTML_CONTENT, "key_2": 1}, + } + ] + ) ), escape_answer_values=True, ) @@ -544,3 +1007,222 @@ def test_answer_value_with_selector_can_be_escaped(): ) == ESCAPED_CONTENT ) + + +def test_progress_values_source_throws_if_no_location_given(): + value_source_resolver = get_value_source_resolver( + data_stores=DataStores(progress_store=ProgressStore()), location=None + ) + with pytest.raises(ValueError): + value_source_resolver.resolve( + {"source": "progress", "selector": "block", "identifier": "a-block"} + ) + + +@pytest.mark.parametrize("in_repeating_section", [True, False]) +@pytest.mark.parametrize( + "value_source,expected_result", + [ + ( + {"identifier": "guidance"}, + "Some supplementary guidance about the survey", + ), + ( + {"identifier": "note", "selectors": ["title"]}, + "Volume of total production", + ), + ( + {"identifier": "note", "selectors": ["example", "title"]}, + "Including", + ), + ( + {"identifier": "note", "selectors": ["example", "description"]}, + "Sales across all UK stores", + ), + ( + {"identifier": "note", "selectors": ["invalid", "description"]}, + None, + ), + ( + {"identifier": "INVALID"}, + None, + ), + ], +) +def test_supplementary_data_value_source_non_list_items( + supplementary_data_store_with_data, + value_source, + expected_result, + in_repeating_section, +): + list_store = ListStore([{"name": "some-list", "items": get_list_items(3)}]) + location = ( + Location( + section_id="section", + block_id="block-id", + list_name="some-list", + list_item_id="item-1", + ) + if in_repeating_section + else Location(section_id="section", block_id="block-id") + ) + value_source_resolver = get_value_source_resolver( + data_stores=DataStores( + supplementary_data_store=supplementary_data_store_with_data, + list_store=list_store, + ), + location=location, + list_item_id=location.list_item_id, + ) + assert ( + value_source_resolver.resolve( + { + "source": "supplementary_data", + **value_source, + } + ) + == expected_result + ) + + +@pytest.mark.parametrize( + "list_item_id, value_source ,expected_result", + [ + ( + "item-1", + {"identifier": "products", "selectors": ["name"]}, + "Articles and equipment for sports or outdoor games", + ), + ( + "item-1", + {"identifier": "products", "selectors": ["value_sales", "answer_code"]}, + "89929001", + ), + ( + "item-1", + {"identifier": "products", "selectors": ["value_sales", "label"]}, + "Value of sales", + ), + ( + "item-1", + {"identifier": "products", "selectors": ["guidance", "description"]}, + "sportswear", + ), + ( + "item-2", + {"identifier": "products", "selectors": ["guidance", "description"]}, + None, + ), + ( + "item-2", + {"identifier": "products", "selectors": ["non_existing_optional_key"]}, + None, + ), + ( + "item-2", + {"identifier": "products", "selectors": ["name"]}, + "Other Minerals", + ), + ( + "item-2", + {"identifier": "products", "selectors": ["value_sales", "answer_code"]}, + "201630601", + ), + ( + None, + {"identifier": "products", "selectors": ["name"]}, + ["Articles and equipment for sports or outdoor games", "Other Minerals"], + ), + ( + None, + {"identifier": "products", "selectors": ["value_sales", "answer_code"]}, + ["89929001", "201630601"], + ), + ( + None, + {"identifier": "products", "selectors": ["non_existing_optional_key"]}, + [], + ), + ], +) +def test_supplementary_data_value_source_list_items( + supplementary_data_store_with_data, + list_item_id, + value_source, + expected_result, +): + list_store = ListStore([{"name": "products", "items": get_list_items(2)}]) + location = Location( + section_id="section", + block_id="block-id", + list_name="products", + list_item_id=list_item_id, + ) + value_source_resolver = get_value_source_resolver( + data_stores=DataStores( + supplementary_data_store=supplementary_data_store_with_data, + list_store=list_store, + ), + location=location, + list_item_id=list_item_id, + ) + assert ( + value_source_resolver.resolve( + { + "source": "supplementary_data", + **value_source, + } + ) + == expected_result + ) + + +def test_supplementary_data_value_source_list_items_value_missing_excluded( + supplementary_data_store_with_data_extra_item, +): + list_store = ListStore([{"name": "products", "items": get_list_items(3)}]) + location = Location( + section_id="section", + block_id="block-id", + list_name="products", + list_item_id=None, + ) + value_source_resolver = get_value_source_resolver( + data_stores=DataStores( + supplementary_data_store=supplementary_data_store_with_data_extra_item, + list_store=list_store, + ), + location=location, + list_item_id=None, + ) + assert value_source_resolver.resolve( + { + "source": "supplementary_data", + **{"identifier": "products", "selectors": ["name"]}, + } + ) == ["Articles and equipment for sports or outdoor games", "Other Minerals"] + + +def test_supplementary_data_invalid_selector_raises_exception( + supplementary_data_store_with_data, +): + location = Location( + section_id="section", + block_id="block-id", + ) + value_source_resolver = get_value_source_resolver( + data_stores=DataStores( + supplementary_data_store=supplementary_data_store_with_data + ), + location=location, + ) + with pytest.raises(InvalidSupplementaryDataSelector) as e: + value_source_resolver.resolve( + { + "source": "supplementary_data", + "identifier": "guidance", + "selectors": ["invalid"], + } + ) + + assert e.value.args[0] == "Cannot use the selector `invalid` on non-nested data" diff --git a/tests/app/questionnaire/test_when_rules.py b/tests/app/questionnaire/test_when_rules.py deleted file mode 100644 index 5cc78d00d1..0000000000 --- a/tests/app/questionnaire/test_when_rules.py +++ /dev/null @@ -1,802 +0,0 @@ -# pylint: disable=too-many-lines -import pytest - -from app.data_models.answer_store import Answer -from app.data_models.list_store import ListStore -from app.questionnaire.location import Location -from app.questionnaire.path_finder import PathFinder -from app.questionnaire.relationship_location import RelationshipLocation -from app.questionnaire.routing_path import RoutingPath -from app.questionnaire.when_rules import ( - evaluate_goto, - evaluate_rule, - evaluate_when_rules, -) - - -@pytest.mark.parametrize( - "when_rule, answers, expected", - ( - ({"value": "singleAnswer", "condition": "contains"}, ["singleAnswer"], True), - ( - {"value": "firstAnswer", "condition": "equals"}, - ["firstAnswer", "secondAnswer"], - False, - ), - ({"value": False, "condition": "equals"}, False, True), - ({"value": True, "condition": "not equals"}, False, True), - ({"condition": "set"}, "", True), - ({"condition": "set"}, "0", True), - ({"condition": "set"}, "Yes", True), - ({"condition": "set"}, "No", True), - ({"condition": "set"}, 0, True), - ({"condition": "set"}, 1, True), - ({"condition": "set"}, None, False), - ({"condition": "not set"}, None, True), - ({"condition": "not set"}, "", False), - ({"condition": "not set"}, "some text", False), - ({"condition": "not set"}, [], True), - ({"condition": "not set"}, ["123"], False), - ({"condition": "set"}, ["123"], True), - ({"condition": "set"}, [], False), - ({"value": 0, "condition": "equals"}, 2, False), - ({"value": 0, "condition": "equals"}, 0, True), - ({"value": "answervalue", "condition": "equals"}, "answerValue", True), - ({"value": "answervalue", "condition": "equals"}, "answervalue", True), - ({"value": "answervalue", "condition": "equals"}, "answer-value", False), - ({"value": "answervalue", "condition": "not equals"}, "answerValue", False), - ({"value": "answervalue", "condition": "not equals"}, "answervalue", False), - ({"value": "answervalue", "condition": "not equals"}, "answer-value", True), - ( - {"value": ["answerValue", "notAnswerValue"], "condition": "equals any"}, - "answerValue", - True, - ), - ( - {"value": ["answerValue", "notAnswerValue"], "condition": "equals any"}, - "answervalue", - True, - ), - ( - {"value": ["answerValue", "notAnswerValue"], "condition": "equals any"}, - "answer-value", - False, - ), - ( - {"value": ["answerValue", "notAnswerValue"], "condition": "not equals any"}, - "answerValue", - False, - ), - ( - {"value": ["answerValue", "notAnswerValue"], "condition": "not equals any"}, - "answervalue", - False, - ), - ( - {"value": ["answerValue", "notAnswerValue"], "condition": "not equals any"}, - "answer-value", - True, - ), - ({"value": 0, "condition": "not equals"}, 2, True), - ({"value": 0, "condition": "not equals"}, 0, False), - ({"value": 4, "condition": "greater than or equal to"}, 4, True), - ({"value": 4, "condition": "greater than or equal to"}, 5, True), - ({"value": 4, "condition": "greater than or equal to"}, 3, False), - ({"value": 4, "condition": "greater than or equal to"}, None, False), - ({"value": 4, "condition": "less than or equal to"}, 4, True), - ({"value": 4, "condition": "less than or equal to"}, 3, True), - ({"value": 4, "condition": "less than or equal to"}, 5, False), - ({"value": 4, "condition": "less than or equal to"}, None, False), - ({"value": 5, "condition": "greater than"}, 7, True), - ({"value": 5, "condition": "greater than"}, 5, False), - ({"value": 5, "condition": "greater than"}, 3, False), - ({"value": 5, "condition": "less than"}, 3, True), - ({"value": 5, "condition": "less than"}, 5, False), - ({"value": 5, "condition": "less than"}, 7, False), - ), -) -def test_evaluate_rule(when_rule, answers, expected): - assert evaluate_rule(when_rule, answers) is expected - - -@pytest.mark.parametrize( - "goto, answers, metadata, expected", - ( - ( - { - "id": "next-question", - "when": [{"id": "my_answer", "condition": "equals", "value": "Yes"}], - }, - [{"answer_id": "my_answer", "value": "Yes"}], - None, - True, - ), - ( - { - "id": "next-question", - "when": [{"id": "my_answer", "condition": "equals", "value": "Yes"}], - }, - [{"answer_id": "my_answer", "value": "No"}], - None, - False, - ), - ( - { - "id": "next-question", - "when": [ - {"id": "my_answers", "condition": "contains", "value": "answer1"} - ], - }, - [{"answer_id": "my_answer", "value": "No"}], - None, - False, - ), - ( - { - "id": "next-question", - "when": [ - { - "id": "my_answers", - "condition": "not contains", - "value": "answer1", - } - ], - }, - [{"answer_id": "my_answer", "value": "No"}], - None, - False, - ), - ( - { - "id": "next-question", - "when": [ - {"id": "my_answers", "condition": "contains", "value": "answer1"} - ], - }, - [{"answer_id": "my_answers", "value": ["answer1", "answer2", "answer3"]}], - None, - True, - ), - ( - { - "id": "next-question", - "when": [ - { - "id": "my_answers", - "condition": "not contains", - "value": "answer1", - } - ], - }, - [{"answer_id": "my_answers", "value": ["answer2", "answer3"]}], - None, - True, - ), - ( - { - "id": "next-question", - "when": [ - { - "id": "my_answers", - "condition": "contains any", - "value": ["answer1", "answer2"], - } - ], - }, - [{"answer_id": "my_answers", "value": ["answer1", "answer4"]}], - None, - True, - ), - ( - { - "id": "next-question", - "when": [ - { - "id": "my_answers", - "condition": "contains all", - "value": ["answer1", "answer2"], - } - ], - }, - [{"answer_id": "my_answers", "value": ["answer1", "answer2", "answer3"]}], - None, - True, - ), - ( - { - "id": "next-question", - "when": [ - { - "id": "my_answers", - "condition": "equals any", - "values": ["answer1", "answer2"], - } - ], - }, - [{"answer_id": "my_answers", "value": "answer2"}], - None, - True, - ), - ( - { - "id": "next-question", - "when": [ - { - "id": "my_answers", - "condition": "not equals any", - "values": ["answer1", "answer2"], - } - ], - }, - [{"answer_id": "my_answers", "value": "answer3"}], - None, - True, - ), - ( - { - "id": "next-question", - "when": [ - { - "id": "my_answers", - "condition": "not equals any", - "values": ["answer1", "answer2"], - } - ], - }, - [ - {"answer_id": "my_answer", "value": "Yes"}, - {"answer_id": "my_other_answer", "value": "2"}, - ], - None, - True, - ), - ( - { - "id": "next-question", - "when": [ - {"id": "my_answer", "condition": "equals", "value": "Yes"}, - {"id": "my_other_answer", "condition": "equals", "value": "2"}, - ], - }, - [ - {"answer_id": "my_answer", "value": "No"}, - ], - None, - False, - ), - ( - { - "id": "next-question", - "when": [ - {"id": "my_answer", "condition": "equals", "value": "Yes"}, - {"condition": "equals", "meta": "sexual_identity", "value": True}, - ], - }, - [ - {"answer_id": "my_answer", "value": "Yes"}, - ], - {"sexual_identity": True}, - True, - ), - ( - { - "id": "next-question", - "when": [ - {"id": "my_answer", "condition": "equals", "value": "Yes"}, - {"condition": "equals", "meta": "sexual_identity", "value": False}, - ], - }, - [ - {"answer_id": "my_answer", "value": "Yes"}, - ], - {"varient_flags": {"sexual_identity": True}}, - False, - ), - ( - { - "id": "next-question", - "when": [ - { - "condition": "equals", - "meta": "variant_flags.does_not_exist.does_not_exist", - "value": True, - } - ], - }, - [ - {"answer_id": "my_answer", "value": "Yes"}, - ], - {"sexual_identity": True}, - False, - ), - ), -) -def test_go_to( - goto, - answers, - metadata, - expected, - answer_store, - list_store, - current_location, - questionnaire_schema, -): - for answer in answers: - answer_store.add_or_update(Answer(**answer)) - - assert ( - evaluate_goto( - goto_rule=goto, - schema=questionnaire_schema, - metadata=metadata or {}, - answer_store=answer_store, - list_store=list_store, - current_location=current_location, - ) - is expected - ) - - -@pytest.mark.parametrize( - "skip_conditions, answers, expected", - ( - ( - [ - {"when": [{"id": "this", "condition": "equals", "value": "value"}]}, - { - "when": [ - {"id": "that", "condition": "equals", "value": "other value"} - ] - }, - ], - [{"answer_id": "this", "value": "value"}], - True, - ), - ( - [ - {"when": [{"id": "this", "condition": "equals", "value": "value"}]}, - { - "when": [ - {"id": "that", "condition": "equals", "value": "other value"} - ] - }, - ], - [{"answer_id": "that", "value": "other value"}], - True, - ), - ( - [ - {"when": [{"id": "this", "condition": "equals", "value": "value"}]}, - { - "when": [ - {"id": "that", "condition": "equals", "value": "other value"} - ] - }, - ], - [ - {"answer_id": "that", "value": "other value"}, - {"answer_id": "this", "value": "value"}, - ], - True, - ), - ( - [ - {"when": [{"id": "this", "condition": "equals", "value": "value"}]}, - { - "when": [ - {"id": "that", "condition": "equals", "value": "other value"} - ] - }, - ], - [ - {"answer_id": "that", "value": "not correct"}, - {"answer_id": "this", "value": "not correct"}, - ], - False, - ), - ( - None, - [], - False, - ), - ), -) -def test_skip_conditions( - skip_conditions, - answers, - expected, - answer_store, - list_store, - current_location, - questionnaire_schema, - progress_store, -): - for answer in answers: - answer_store.add_or_update(Answer(**answer)) - - path_finder = PathFinder( - questionnaire_schema, - answer_store, - list_store=list_store, - metadata={}, - progress_store=progress_store, - response_metadata={}, - ) - - routing_path_block_ids = [] - - condition = path_finder.evaluate_skip_conditions( - current_location, routing_path_block_ids, skip_conditions, [] - ) - - assert condition is expected - - -@pytest.mark.parametrize( - "when_rules, answers, expected", - (([{"id": "my_answers", "condition": "not set"}], {}, True),), -) -def test_evaluate_not_set_when_rules_should_return_true( - when_rules, - answers, - expected, - answer_store, - list_store, - current_location, - questionnaire_schema, -): - for answer in answers.values(): - answer_store.add_or_update(answer) - - assert ( - evaluate_when_rules( - when_rules=when_rules, - schema=questionnaire_schema, - metadata={}, - answer_store=answer_store, - list_store=list_store, - current_location=current_location, - ) - is expected - ) - - -@pytest.mark.parametrize( - "lhs_answer, comparison, rhs_answer, expected", - ( - ("medium", "equals", "medium", True), - ("medium", "equals", "low", False), - ("medium", "greater than", "low", True), - ("medium", "greater than", "high", False), - ("medium", "less than", "high", True), - ("medium", "less than", "low", False), - ("medium", "equals", "missing_answer", False), - ("list_answer", "contains", "text_answer", True), - ("list_answer", "contains", "other_text_answer", False), - ("list_answer", "not contains", "other_text_answer", True), - ("list_answer", "not contains", "text_answer", False), - ("list_answer", "contains any", "other_list_answer_2", True), - ("list_answer", "contains any", "other_list_answer", False), - ("list_answer", "contains all", "other_list_answer", False), - ("list_answer", "contains all", "other_list_answer_2", True), - ("text_answer", "equals any", "list_answer", True), - ("text_answer", "equals any", "other_list_answer", False), - ("text_answer", "not equals any", "other_list_answer", True), - ("text_answer", "not equals any", "list_answer", False), - ), -) -def test_when_rule_comparing_answer_values( - lhs_answer, - comparison, - rhs_answer, - expected, - answers, - answer_store, - list_store, - questionnaire_schema, - current_location, -): - - for answer in answers.values(): - answer_store.add_or_update(answer) - - when = [ - { - "id": answers[lhs_answer].answer_id, - "condition": comparison, - "comparison": {"id": answers[rhs_answer].answer_id, "source": "answers"}, - } - ] - - assert ( - evaluate_when_rules( - when_rules=when, - schema=questionnaire_schema, - metadata={}, - answer_store=answer_store, - list_store=list_store, - current_location=current_location, - routing_path_block_ids=None, - ) - is expected - ) - - -@pytest.mark.parametrize( - "list_item_id, expected", - ( - ("abc123", True), - ("123abc", False), - ), -) -def test_evaluate_when_rule_with_list_item_id( - list_item_id, - expected, - answer_store, - list_store, - questionnaire_schema, - mocker, -): - when_rules = [{"id": "my_answer", "condition": "equals", "value": "an answer"}] - - answer_store.add_or_update( - Answer(answer_id="my_answer", value="an answer", list_item_id="abc123") - ) - - current_location = Location( - section_id="some-section", block_id="some-block", list_item_id=list_item_id - ) - - schema = mocker.Mock(questionnaire_schema) - schema.is_repeating_answer = mocker.Mock(return_value=True) - - assert ( - evaluate_when_rules( - when_rules=when_rules, - schema=schema, - metadata={}, - answer_store=answer_store, - list_store=list_store, - current_location=current_location, - ) - is expected - ) - - -def test_evaluate_when_rule_raises_if_bad_when_condition( - answer_store, list_store, questionnaire_schema, current_location -): - when_rules = [{"condition": "not set"}] - with pytest.raises(Exception): - evaluate_when_rules( - when_rules=when_rules, - schema=questionnaire_schema, - metadata={}, - answer_store=answer_store, - list_store=list_store, - current_location=current_location, - ) - - -@pytest.mark.parametrize( - "when_rules, expected", - ( - ([{"list": "people", "condition": "less than", "value": 2}], True), - ([{"list": "people", "condition": "equals", "value": 1}], True), - ), -) -def test_evaluate_when_rule_with_list_rules( - when_rules, - expected, - answer_store, - questionnaire_schema, - current_location, -): - list_store = ListStore(existing_items=[{"name": "people", "items": ["abcdef"]}]) - - assert ( - evaluate_when_rules( - when_rules=when_rules, - schema=questionnaire_schema, - metadata={}, - answer_store=answer_store, - list_store=list_store, - current_location=current_location, - ) - is expected - ) - - -@pytest.mark.parametrize( - "routing_path, is_on_answer_path, expected", - ( - ( - RoutingPath( - ["test_block_id", "some-block"], - section_id="some-section", - list_name="people", - list_item_id="abc123", - ), - True, - True, - ), - ( - [ - Location( - section_id="some-section", - block_id="test_block_id", - list_name="people", - list_item_id="abc123", - ) - ], - False, - False, - ), - ), -) -def test_routing_answer_on_path_when_in_a_repeat( - routing_path, - is_on_answer_path, - expected, - answer_store, - list_store, - questionnaire_schema, - mocker, -): - when_rules = [{"id": "some-answer", "condition": "equals", "value": "some value"}] - - answer = Answer(answer_id="some-answer", value="some value") - answer_store.add_or_update(answer) - - routing_path = RoutingPath( - ["test_block_id", "some-block"], - section_id="some-section", - list_name="people", - list_item_id="abc123", - ) - - current_location = Location( - section_id="some-section", - block_id="some-block", - list_name="people", - list_item_id="abc123", - ) - - with mocker.patch( - "app.questionnaire.when_rules.get_answer_for_answer_id", return_value=answer - ), mocker.patch( - "app.questionnaire.when_rules._is_answer_on_path", - return_value=is_on_answer_path, - ): - assert ( - evaluate_when_rules( - when_rules=when_rules, - schema=questionnaire_schema, - metadata={}, - answer_store=answer_store, - list_store=list_store, - current_location=current_location, - routing_path_block_ids=routing_path, - ) - is expected - ) - - -def test_routing_ignores_answers_not_on_path( - answer_store, list_store, current_location, questionnaire_schema, mocker -): - when_rules = [{"id": "some-answer", "condition": "equals", "value": "some value"}] - answer_store.add_or_update(Answer(answer_id="some-answer", value="some value")) - - routing_path = [Location(section_id="some-section", block_id="test_block_id")] - - assert evaluate_when_rules( - when_rules=when_rules, - schema=questionnaire_schema, - metadata={}, - answer_store=answer_store, - list_store=list_store, - current_location=current_location, - ) - - with mocker.patch( - "app.questionnaire.when_rules._is_answer_on_path", return_value=False - ): - assert not ( - evaluate_when_rules( - when_rules=when_rules, - schema=questionnaire_schema, - metadata={}, - answer_store=answer_store, - list_store=list_store, - current_location=current_location, - routing_path_block_ids=routing_path, - ) - ) - - -@pytest.mark.parametrize( - "when_rule_comparison_id, expected", - ( - ( - "list_item_id", - True, - ), - ( - "invalid-location-id", - False, - ), - ), -) -def test_primary_person_checks_location( - when_rule_comparison_id, expected, answer_store, questionnaire_schema -): - list_store = ListStore( - existing_items=[ - { - "name": "people", - "primary_person": "abcdef", - "items": ["abcdef", "12345"], - } - ] - ) - - current_location = RelationshipLocation( - section_id="some-section", - block_id="some-block", - list_item_id="abcdef", - to_list_item_id="12345", - list_name="household", - ) - - when_rules = [ - { - "list": "people", - "id_selector": "primary_person", - "condition": "equals", - "comparison": {"source": "location", "id": when_rule_comparison_id}, - } - ] - - assert ( - evaluate_when_rules( - when_rules=when_rules, - schema=questionnaire_schema, - metadata={}, - answer_store=answer_store, - list_store=list_store, - current_location=current_location, - ) - is expected - ) - - -def test_when_rule_returns_first_item_in_list(answer_store, questionnaire_schema): - list_store = ListStore( - existing_items=[{"name": "people", "items": ["abcdef", "12345"]}] - ) - - current_location = Location( - section_id="some-section", - block_id="some-block", - list_name="people", - list_item_id="abcdef", - ) - - when_rules = [ - { - "list": "people", - "id_selector": "first", - "condition": "equals", - "comparison": {"source": "location", "id": "list_item_id"}, - } - ] - - assert evaluate_when_rules( - when_rules=when_rules, - schema=questionnaire_schema, - metadata={}, - answer_store=answer_store, - list_store=list_store, - current_location=current_location, - ) diff --git a/tests/app/services/__init__.py b/tests/app/services/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/app/services/conftest.py b/tests/app/services/conftest.py new file mode 100644 index 0000000000..8fa7f85adf --- /dev/null +++ b/tests/app/services/conftest.py @@ -0,0 +1,67 @@ +import pytest + + +@pytest.fixture +def decrypted_mock_supplementary_data_payload(): + return { + "dataset_id": "44f1b432-9421-49e5-bd26-e63e18a30b69", + "survey_id": "123", + "data": { + "schema_version": "v1", + "identifier": "12345678901", + "items": { + "local_units": [ + { + "identifier": "0001", + "lu_name": "TEST NAME. 1", + "lu_address": [ + "FIRST ADDRESS 1", + "FIRST ADDRESS 2", + "TOWN", + "COUNTY", + "POST CODE", + ], + }, + { + "identifier": "0002", + "lu_name": "TEST NAME 2", + "lu_address": [ + "SECOND ADDRESS 1", + "SECOND ADDRESS 1", + "TOWN", + "COUNTY", + "POSTCODE", + ], + }, + ] + }, + }, + } + + +@pytest.fixture +def encrypted_mock_supplementary_data_payload(): + return { + "dataset_id": "44f1b432-9421-49e5-bd26-e63e18a30b69", + "survey_id": "123", + # pylint: disable-next=line-too-long + "data": "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00iLCJraWQiOiJkZjg4ZmRhZDI2MTJhZTFlODA1NzExMjBlNmM2MzcxZjU1ODk2Njk2In0.tK1Duhk7FmvMn7X6QaSvrpx5U0wOzOyzFwOaLUksCBrPI2J7Tnz1mRMoQla-ekuG-B0DLaQUWA74vgN44UhWbLd7fYbIwjt9TCgdfVyP7P9tOebe55xEHsbvMmRf7F1F37KjPXXk7usDkFuuzl_fjhLwmrjNn90YA93QLy1PbGiMcC8JFYDwLL-vaWslB-VGgKEdud4LCY80xvwxzxxHYtpEQ2NpBXt9zpodwQCFqn8LHHGN80h8_-TE2gOInWccRd7GsyzvH5hGb-wxmgodJQFrPse4v6VBgTJbixMFvxulZqAyYULzU7LiFGSQZqrhqqL4-UXWRmeJX5aYR9AtzO2kGl6gVDZ2d0jGLuHwOSiJ9CLtvltKU8ai0ZfN-qUY6ZnV2lZ7wq_fZrat2-VdxE-ktMy6owGBD3DwHcd_QwAPlDFDEJeAS1G4JKuyfCb_siM5wLd1_HV_6kG2IcyAtt0usRj2D36N2K2lufJjJJlDh2QJm6r5H1pGci8AR7YCJ7nAbmlyMJ7RLJ65TswcvWT7A8jQnVBZT2NCL3_WZ9o2cGj9HixzATzVPkP54LWPTgDI-Q0-nLaBfkHFcR9-6v9f_DFW_zcj2yFfu93HRSc4GSjGs7x0dZkpY-JLFqcNzxcEeUMODUgeE9QZAIF2VsS7NcodbpqJJlOBbevJeZU.Z6Gte9FcRQfo3vos.kwL0nw0aZj6hYugPWt5oa4j-SwdBhDl-ynd6UDtqf1WFDLZcS_DBaXXT2tB7vh_zHNJaMhkJfZV4iT1jr6Wat8pDhL4c_IrclxMLagU22dST6aZcCBkVtvVUCB5dt0AO64NUBysiDf8nTnJyzLnC-pMXvc1QXSS5OMdrFdLPm3jaTAT7y1NYNtIQyRrLZj7PayRQK5w5JS3clq3BSn780rECVxM4FLuDsO0hft-feDKW700Xe4HAeXqaLwADM3bUXMeriiX2CvdTkBB2gv2uBQdR6FIVwGPDbTvXHf1bvTCLmaIT-Ki3pDh_BIrZhgg9ZO8Pft5ZEWWeQR0kF-2eglkro90AWSzUFrQO5YsYsQ-YdjoajeyyiQTg1zW3LyGM3F7-EDKrAZzUgiUhK9UC1Lfheg7Owg7aG-gYN3N6UU5ngnXegcJtNEnoULOLnck0aLS7Ku0v19tw4ObGPW28KZo.Ewzo-AyJbBMCWPqlhjhhAw", + } + + +@pytest.fixture +def mock_supplementary_data_payload_invalid_kid_in_data(): + return { + "dataset_id": "44f1b432-9421-49e5-bd26-e63e18a30b69", + "survey_id": "123", + # pylint: disable-next=line-too-long + "data": "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00iLCJraWQiOiJkZjg4ZmRhZDI2MTJhZTFlODA1NzExMjBlNmM2MzcxZjU1ODk2Njk3In0=.lssJXsMUE3dhWtQRUt7DTaZJvx4DpNdLW98cu8g4NijYX9TFpJiOFyzPxUlpFZb-fMa4zW9q6qZofQeQTbl_Ae3QAwGhuWF7v9NMdWM1aH377byyJJyJpdqlU4t-P03evRWZqAG2HtsNE2Zn1ORXn80Dc9IRkzutgrziLI8OBIZeO6-XEgbVCapsQApWkyux7QRdFH95wfda75nVvGqTbBOYvQiMTKd8KzpH2Vl200IOqEpmrcjUCE-yqdTupzcr88hwNI2ZYdv-pTNowJw1FPODZ7V_sE4Ac-JYv3yBTDcXdz3I5-rX8i2HXqz-g3VhveZiAl9q0AgklPkaO_oNWJzjrCb7DZGL4DjiGYuOcw8OSdOpKLXwkExMlado-wigxy1IWoCzFu2E5tWpmLc0WWcjKuBgD7-4tcn059F7GcwhX2uMRESCmc39pblvseM2UnmmQnwr8GvD7gqWdFwtBsECyXQ5UXAxWLJor_MtU8lAFZxiorRcrXZJwAivroPO9iEB-1Mvt2zZFWI_vMgpJCAIpETscotDKMVCG0UMfkKckJqLnmQpvF4oYTr77w1COBX5bi-AV8UrLJ7sVVktSXOBc_KCGRpoImA5cE67hW7mFUdJi1EHA39qt0tTqZD7izpu8sSLxsiuCkfsqrd4uAedcDdQm4QGxXOPD4pxois.wfWsetB3M0x9qfw5.43Wns86lGlbHj63b0ZxE2bxBQVus6FIqelb9LfSbvopLn5oR8FM4vDEnDp_rIyvjmV9YAZJ6HAHaYaWoNyIO0EorgamrB4R3-LqInANoe9c8xLZ9wl_QpE9aWnxsmFGZUWLO3q2fVTPnwBtA_LxK8FD0vjdLL9eHGYEmPVCGVX0BJX04TVW9aoemsx9Yn3ZtfvmQHuROiB-GcA5wOSb-GvhzfplY09GQr7g7221MiYCHYimmEJyxLV5clWPXu6izzVLDyG9l2ewCifiuBLD0O1U_fPlahHTmidwHKJEAEn39biNw5E_dr8WyZ3xBvJa9dP50m0xeyN4COR-xlYcEbuDcKoqN6BnY0bMNDxQYlBO--QcPLQ6h48uTJszwzsmNIwHoi0xy5dQah7c9Nt2lpMuNt1Wix-O8JWYCqaiCKxjwt9G8kabMbzhp1n3LetWweoyV7qJTbiB13Byv6SZwMO9M.8j8wtvwBAHzqRhv5Ii9jjQ", + } + + +@pytest.fixture +def mock_supplementary_data_payload_missing_data(): + return { + "dataset_id": "44f1b432-9421-49e5-bd26-e63e18a30b69", + "survey_id": "123", + } diff --git a/tests/app/services/test_request_supplementary_data.py b/tests/app/services/test_request_supplementary_data.py new file mode 100644 index 0000000000..7efe05f690 --- /dev/null +++ b/tests/app/services/test_request_supplementary_data.py @@ -0,0 +1,254 @@ +import pytest +import responses +from flask import Flask, current_app +from marshmallow import ValidationError +from mock import Mock +from requests import RequestException +from sdc.crypto.key_store import KeyStore + +from app.oidc.gcp_oidc import OIDCCredentialsServiceGCP +from app.services.supplementary_data import ( + SUPPLEMENTARY_DATA_REQUEST_MAX_RETRIES, + InvalidSupplementaryData, + MissingSupplementaryDataKey, + SupplementaryDataRequestFailed, + decrypt_supplementary_data, + get_supplementary_data_v1, +) +from tests.app.utilities.test_schema import get_mocked_make_request + +TEST_SDS_URL = "http://test.domain" +EXPECTED_SDS_DECRYPTION_VALIDATION_ERROR = "Supplementary data has no data to decrypt" + + +@responses.activate +def test_get_supplementary_data_v1_200( + app: Flask, + encrypted_mock_supplementary_data_payload, + decrypted_mock_supplementary_data_payload, +): + with app.app_context(): + current_app.config["SDS_API_BASE_URL"] = TEST_SDS_URL + + responses.add( + responses.GET, + f"{TEST_SDS_URL}/v1/unit_data", + json=encrypted_mock_supplementary_data_payload, + status=200, + ) + loaded_supplementary_data = get_supplementary_data_v1( + dataset_id="44f1b432-9421-49e5-bd26-e63e18a30b69", + identifier="12345678901", + survey_id="123", + ) + + assert loaded_supplementary_data == decrypted_mock_supplementary_data_payload + + +@pytest.mark.parametrize( + "status_code", + [401, 403, 404, 501, 511], +) +@responses.activate +def test_get_supplementary_data_v1_non_200( + app: Flask, status_code, encrypted_mock_supplementary_data_payload +): + with app.app_context(): + current_app.config["SDS_API_BASE_URL"] = TEST_SDS_URL + + responses.add( + responses.GET, + f"{TEST_SDS_URL}/v1/unit_data", + json=encrypted_mock_supplementary_data_payload, + status=status_code, + ) + + with pytest.raises(SupplementaryDataRequestFailed) as exc: + get_supplementary_data_v1( + dataset_id="44f1b432-9421-49e5-bd26-e63e18a30b69", + identifier="12345678901", + survey_id="123", + ) + + assert str(exc.value) == "Supplementary Data request failed" + + +@responses.activate +def test_get_supplementary_data_v1_request_failed(app: Flask): + with app.app_context(): + current_app.config["SDS_API_BASE_URL"] = TEST_SDS_URL + + responses.add(responses.GET, TEST_SDS_URL, body=RequestException()) + with pytest.raises(SupplementaryDataRequestFailed) as exc: + get_supplementary_data_v1( + dataset_id="44f1b432-9421-49e5-bd26-e63e18a30b69", + identifier="12345678901", + survey_id="123", + ) + + assert str(exc.value) == "Supplementary Data request failed" + + +def test_get_supplementary_data_v1_retries_timeout_error( + app: Flask, + mocker, + mocked_make_request_with_timeout, + decrypted_mock_supplementary_data_payload, +): + with app.app_context(): + current_app.config["SDS_API_BASE_URL"] = TEST_SDS_URL + mocker.patch( + "app.services.supplementary_data.decrypt_supplementary_data", + return_value=decrypted_mock_supplementary_data_payload, + ) + + try: + supplementary_data = get_supplementary_data_v1( + dataset_id="44f1b432-9421-49e5-bd26-e63e18a30b69", + identifier="12345678901", + survey_id="123", + ) + except SupplementaryDataRequestFailed: + return pytest.fail("Supplementary data request unexpectedly failed") + + assert supplementary_data == decrypted_mock_supplementary_data_payload + + expected_call = ( + SUPPLEMENTARY_DATA_REQUEST_MAX_RETRIES + 1 + ) # Max retries + the initial request + assert mocked_make_request_with_timeout.call_count == expected_call + + +@pytest.mark.usefixtures("mocked_response_content") +def test_get_supplementary_data_v1_retries_transient_error( + app: Flask, mocker, decrypted_mock_supplementary_data_payload +): + with app.app_context(): + current_app.config["SDS_API_BASE_URL"] = TEST_SDS_URL + mocked_make_request = get_mocked_make_request( + mocker, status_codes=[500, 500, 200] + ) + + mocker.patch( + "app.services.supplementary_data.decrypt_supplementary_data", + return_value=decrypted_mock_supplementary_data_payload, + ) + + try: + supplementary_data = get_supplementary_data_v1( + dataset_id="44f1b432-9421-49e5-bd26-e63e18a30b69", + identifier="12345678901", + survey_id="123", + ) + except SupplementaryDataRequestFailed: + return pytest.fail("Supplementary data request unexpectedly failed") + + assert supplementary_data == decrypted_mock_supplementary_data_payload + + expected_call = ( + SUPPLEMENTARY_DATA_REQUEST_MAX_RETRIES + 1 + ) # Max retries + the initial request + + assert mocked_make_request.call_count == expected_call + + +def test_get_supplementary_data_v1_max_retries(app: Flask, mocker): + with app.app_context(): + current_app.config["SDS_API_BASE_URL"] = TEST_SDS_URL + + mocked_make_request = get_mocked_make_request( + mocker, status_codes=[500, 500, 500, 500] + ) + + with pytest.raises(SupplementaryDataRequestFailed) as exc: + get_supplementary_data_v1( + dataset_id="44f1b432-9421-49e5-bd26-e63e18a30b69", + identifier="12345678901", + survey_id="123", + ) + + assert str(exc.value) == "Supplementary Data request failed" + assert mocked_make_request.call_count == 3 + + +def test_decrypt_supplementary_data_v1_decrypts_when_encrypted_payload_is_valid( + app: Flask, + encrypted_mock_supplementary_data_payload, + decrypted_mock_supplementary_data_payload, +): + with app.app_context(): + result = decrypt_supplementary_data( + key_store=app.eq["key_store"], + supplementary_data=encrypted_mock_supplementary_data_payload, + ) + assert result == decrypted_mock_supplementary_data_payload + + +def test_decrypt_supplementary_data_v1_raises_validation_error_when_encrypted_payload_missing_data( + app: Flask, mock_supplementary_data_payload_missing_data +): + with app.app_context(): + with pytest.raises(ValidationError) as e: + decrypt_supplementary_data( + key_store=app.eq["key_store"], + supplementary_data=mock_supplementary_data_payload_missing_data, + ) + assert EXPECTED_SDS_DECRYPTION_VALIDATION_ERROR in e.value.messages + + +def test_decrypt_supplementary_data_v1_raises_invalid_token_error_when_encrypted_data_kid_invalid( + app: Flask, mock_supplementary_data_payload_invalid_kid_in_data +): + with app.app_context(): + with pytest.raises(InvalidSupplementaryData): + decrypt_supplementary_data( + key_store=app.eq["key_store"], + supplementary_data=mock_supplementary_data_payload_invalid_kid_in_data, + ) + + +def test_get_supplementary_data_v1_raises_missing_supplementary_data_key_error_when_key_is_missing( + app: Flask, mocker +): + with app.app_context(): + mocker.patch.dict( + "app.services.supplementary_data.current_app.eq", + {"key_store": KeyStore({"keys": {}})}, + ) + + with pytest.raises(MissingSupplementaryDataKey): + get_supplementary_data_v1( + dataset_id="44f1b432-9421-49e5-bd26-e63e18a30b69", + identifier="12345678901", + survey_id="123", + ) + + +@responses.activate +def test_get_supplementary_data_v1_with_gcp_authentication( + app: Flask, mocker, encrypted_mock_supplementary_data_payload +): + with app.app_context(): + current_app.config["SDS_API_BASE_URL"] = TEST_SDS_URL + + mock_oidc_service = Mock(spec=OIDCCredentialsServiceGCP) + mocker.patch.dict( + "app.services.supplementary_data.current_app.eq", + {"oidc_credentials_service": mock_oidc_service}, + ) + + responses.add( + responses.GET, + f"{TEST_SDS_URL}/v1/unit_data", + json=encrypted_mock_supplementary_data_payload, + status=200, + ) + + get_supplementary_data_v1( + dataset_id="44f1b432-9421-49e5-bd26-e63e18a30b69", + identifier="12345678901", + survey_id="123", + ) + mock_oidc_service.get_credentials.assert_called_once_with( + iap_client_id=current_app.config["SDS_OAUTH2_CLIENT_ID"] + ) diff --git a/tests/app/storage/conftest.py b/tests/app/storage/conftest.py index 4db26dd50b..dc7c15c253 100644 --- a/tests/app/storage/conftest.py +++ b/tests/app/storage/conftest.py @@ -5,7 +5,7 @@ import fakeredis import pytest from flask import current_app -from moto import mock_dynamodb2 +from moto import mock_aws from app.data_models.app_models import EQSession, QuestionnaireState from app.storage.dynamodb import Dynamodb @@ -16,13 +16,13 @@ @pytest.fixture def dynamodb(): - with mock_dynamodb2() as mock_dynamo: - mock_dynamo.start() + with mock_aws() as mocked_aws_environment: + mocked_aws_environment.start() boto3_client = boto3.resource("dynamodb", endpoint_url=None) for config in StorageModel.TABLE_CONFIG_BY_TYPE.values(): table_name = current_app.config[config["table_name_key"]] if table_name: - boto3_client.create_table( # pylint: disable=no-member + boto3_client.create_table( TableName=table_name, AttributeDefinitions=[ {"AttributeName": config["key_field"], "AttributeType": "S"} @@ -36,7 +36,7 @@ def dynamodb(): }, ) yield Dynamodb(boto3_client) - mock_dynamo.stop() + mocked_aws_environment.stop() @pytest.fixture diff --git a/tests/app/storage/test_datastore.py b/tests/app/storage/test_datastore.py index 3a320545ac..f6f90b9972 100644 --- a/tests/app/storage/test_datastore.py +++ b/tests/app/storage/test_datastore.py @@ -40,7 +40,6 @@ def test_get_not_found(datastore, mock_client): @pytest.mark.usefixtures("app") def test_put(datastore, mock_client, questionnaire_state): - datastore.put(questionnaire_state, True) put_data = mock_client.put.call_args[0][0] @@ -57,7 +56,6 @@ def test_put(datastore, mock_client, questionnaire_state): @pytest.mark.usefixtures("app") def test_put_without_overwrite(datastore, questionnaire_state): - with pytest.raises(NotImplementedError) as exc: datastore.put(questionnaire_state, False) @@ -97,7 +95,6 @@ def test_delete(datastore, mock_client, questionnaire_state): @pytest.mark.usefixtures("app") def test_retry(datastore, mock_client, mocker, questionnaire_state): - mock_client.put = mocker.Mock( side_effect=[exceptions.InternalServerError("error"), mocker.DEFAULT] ) diff --git a/tests/app/submitter/conftest.py b/tests/app/submitter/conftest.py index 20164b6267..75316125ba 100644 --- a/tests/app/submitter/conftest.py +++ b/tests/app/submitter/conftest.py @@ -1,77 +1,53 @@ -# pylint: disable=redefined-outer-name import uuid -from unittest.mock import MagicMock import pytest from google.cloud.storage import Blob -from google.resumable_media import InvalidResponse +from mock import MagicMock from requests import Response -from app.data_models import QuestionnaireStore +from app.authentication.auth_payload_versions import AuthPayloadVersion +from app.data_models import ListStore, QuestionnaireStore from app.data_models.answer import Answer from app.data_models.answer_store import AnswerStore +from app.data_models.metadata_proxy import MetadataProxy +from app.data_models.supplementary_data_store import SupplementaryDataStore from app.questionnaire.questionnaire_schema import QuestionnaireSchema +from app.settings import ACCOUNT_SERVICE_BASE_URL_SOCIAL from app.submitter import RabbitMQSubmitter -from app.utilities.metadata_parser import ( - validate_questionnaire_claims, - validate_runner_claims, -) - - -@pytest.fixture -def fake_metadata(): - def parse_metadata(claims, schema_metadata): - runner_claims = validate_runner_claims(claims) - questionnaire_claims = validate_questionnaire_claims(claims, schema_metadata) - return {**runner_claims, **questionnaire_claims} - - schema_metadata = [ - {"name": "user_id", "type": "string"}, - {"name": "period_id", "type": "string"}, - {"name": "ref_p_start_date", "type": "string"}, - {"name": "ref_p_end_date", "type": "string"}, - {"name": "display_address", "type": "string"}, - {"name": "case_ref", "type": "string"}, - ] - - metadata = parse_metadata( - { - "tx_id": str(uuid.uuid4()), - "user_id": "789473423", - "schema_name": "1_0000", - "collection_exercise_sid": "test-sid", - "account_service_url": "https://rh.ons.gov.uk/", +from tests.app.parser.conftest import get_response_expires_at + +RAW_METADATA_V2 = { + "version": AuthPayloadVersion.V2.value, + "tx_id": str(uuid.uuid4()), + "schema_name": "1_0000", + "collection_exercise_sid": "test-sid", + "account_service_url": ACCOUNT_SERVICE_BASE_URL_SOCIAL, + "survey_metadata": { + "data": { "period_id": "2016-02-01", "period_str": "2016-01-01", "ref_p_start_date": "2016-02-02", "ref_p_end_date": "2016-03-03", - "ru_ref": "432423423423", - "response_id": "1234567890123456", + "ru_ref": "12345678901A", "ru_name": "Apple", - "return_by": "2016-07-07", - "case_id": str(uuid.uuid4()), - "form_type": "I", "case_type": "SPG", - "region_code": "GB-ENG", - "channel": "RH", - "display_address": "68 Abingdon Road, Goathill", + "form_type": "I", "case_ref": "1000000000000001", - "jti": str(uuid.uuid4()), + "display_address": "68 Abingdon Road, Goathill", + "user_id": "789473423", }, - schema_metadata, - ) - - return metadata - - -@pytest.fixture -def fake_response_metadata(): - response_metadata = {"started_at": "2018-07-04T14:49:33.448608+00:00"} - return response_metadata - - -@pytest.fixture -def fake_questionnaire_store(fake_metadata, fake_response_metadata): + }, + "response_id": "1234567890123456", + "case_id": str(uuid.uuid4()), + "region_code": "GB-ENG", + "channel": "RH", + "jti": str(uuid.uuid4()), + "response_expires_at": get_response_expires_at(), +} +METADATA_V2 = MetadataProxy.from_dict(RAW_METADATA_V2) + + +def get_questionnaire_store(): user_answer = Answer(answer_id="GHI", value=0, list_item_id=None) storage = MagicMock() @@ -80,14 +56,40 @@ def fake_questionnaire_store(fake_metadata, fake_response_metadata): store = QuestionnaireStore(storage) - store.answer_store = AnswerStore() - store.answer_store.add_or_update(user_answer) - store.metadata = fake_metadata - store.response_metadata = fake_response_metadata + store.data_stores.answer_store = AnswerStore() + store.supplementary_data_store = SupplementaryDataStore() + store.data_stores.answer_store.add_or_update(user_answer) + store.data_stores.metadata = METADATA_V2 + + store.data_stores.response_metadata = { + "started_at": "2018-07-04T14:49:33.448608+00:00" + } return store +@pytest.fixture +def fake_metadata_v2_schema_url(): + copy = RAW_METADATA_V2.copy() + copy["schema_url"] = "https://schema_url.com" + del copy["schema_name"] + return MetadataProxy.from_dict(copy) + + +@pytest.fixture +def fake_metadata_v2_cir_instrument_id(): + copy = RAW_METADATA_V2.copy() + copy["cir_instrument_id"] = "f0519981-426c-8b93-75c0-bfc40c66fe25" + del copy["schema_name"] + return MetadataProxy.from_dict(copy) + + +@pytest.fixture +def fake_response_metadata(): + response_metadata = {"started_at": "2018-07-04T14:49:33.448608+00:00"} + return response_metadata + + @pytest.fixture def fake_questionnaire_schema(): questionnaire = {"survey_id": "021", "data_version": "0.0.3"} @@ -119,7 +121,7 @@ def patch_gcs_client(mocker): @pytest.fixture def gcs_blob_with_retry(mocker): - blob = Blob(name="some-blob", bucket=mocker.Mock()) + blob = Blob(name="some-blob", bucket=mocker.MagicMock()) response_503 = Response() response_503.status_code = 503 @@ -130,9 +132,7 @@ def gcs_blob_with_retry(mocker): ) response_200.status_code = 200 - mock_transport_request = mocker.Mock( - side_effect=[InvalidResponse(response_503), response_200] - ) + mock_transport_request = mocker.Mock(side_effect=[response_503, response_200]) mock_transport = mocker.Mock() mock_transport.request = mock_transport_request blob._get_transport = mocker.Mock( # pylint: disable=protected-access @@ -140,3 +140,72 @@ def gcs_blob_with_retry(mocker): ) return blob + + +@pytest.fixture +def repeating_blocks_answer_store(): + return AnswerStore( + [ + {"answer_id": "responsible-party-answer", "value": "Yes"}, + {"answer_id": "any-companies-or-branches-answer", "value": "Yes"}, + { + "answer_id": "company-or-branch-name", + "value": "CompanyA", + "list_item_id": "PlwgoG", + }, + { + "answer_id": "registration-number", + "value": "123", + "list_item_id": "PlwgoG", + }, + { + "answer_id": "registration-date", + "value": "2023-01-01", + "list_item_id": "PlwgoG", + }, + { + "answer_id": "authorised-trader-uk-radio", + "value": "Yes", + "list_item_id": "PlwgoG", + }, + { + "answer_id": "authorised-trader-eu-radio", + "value": "Yes", + "list_item_id": "PlwgoG", + }, + { + "answer_id": "company-or-branch-name", + "value": "CompanyB", + "list_item_id": "UHPLbX", + }, + { + "answer_id": "registration-number", + "value": "456", + "list_item_id": "UHPLbX", + }, + { + "answer_id": "registration-date", + "value": "2023-01-01", + "list_item_id": "UHPLbX", + }, + { + "answer_id": "authorised-trader-uk-radio", + "value": "No", + "list_item_id": "UHPLbX", + }, + { + "answer_id": "authorised-trader-eu-radio", + "value": "No", + "list_item_id": "UHPLbX", + }, + { + "answer_id": "any-other-trading-details", + "value": "N/A", + }, + ] + ) + + +@pytest.fixture +def repeating_blocks_list_store(): + return ListStore([{"items": ["PlwgoG", "UHPLbX"], "name": "companies"}]) diff --git a/tests/app/submitter/test_convert_payload_0_0_1.py b/tests/app/submitter/test_convert_payload_0_0_1.py index a63b2ef553..706d0ef56d 100644 --- a/tests/app/submitter/test_convert_payload_0_0_1.py +++ b/tests/app/submitter/test_convert_payload_0_0_1.py @@ -1,11 +1,14 @@ from datetime import datetime, timezone +import pytest + from app.data_models.answer import Answer from app.data_models.answer_store import AnswerStore from app.questionnaire.questionnaire_schema import QuestionnaireSchema from app.questionnaire.routing_path import RoutingPath from app.submitter.convert_payload_0_0_1 import convert_answers_to_payload_0_0_1 -from app.submitter.converter import convert_answers +from app.submitter.converter_v2 import get_payload_data +from tests.app.submitter.conftest import get_questionnaire_store from tests.app.submitter.schema import make_schema SUBMITTED_AT = datetime.now(timezone.utc) @@ -15,8 +18,10 @@ def create_answer(answer_id, value): return {"answer_id": answer_id, "value": value} -def test_convert_answers_to_payload_0_0_1_with_key_error(fake_questionnaire_store): - fake_questionnaire_store.answer_store = AnswerStore( +def test_convert_answers_v2_to_payload_0_0_1_with_key_error(): + questionnaire_store = get_questionnaire_store() + + questionnaire_store.data_stores.answer_store = AnswerStore( [ Answer("ABC", "2016-01-01").to_dict(), Answer("DEF", "2016-03-30").to_dict(), @@ -37,22 +42,23 @@ def test_convert_answers_to_payload_0_0_1_with_key_error(fake_questionnaire_stor questionnaire = make_schema("0.0.1", "section-1", "group-1", "block-1", question) full_routing_path = [ - RoutingPath(["block-1"], section_id="section-1", list_item_id=None) + RoutingPath(block_ids=["block-1"], section_id="section-1", list_item_id=None) ] answer_object = convert_answers_to_payload_0_0_1( - fake_questionnaire_store.metadata, - fake_questionnaire_store.response_metadata, - fake_questionnaire_store.answer_store, - fake_questionnaire_store.list_store, - QuestionnaireSchema(questionnaire), - full_routing_path, + data_stores=questionnaire_store.data_stores, + schema=QuestionnaireSchema(questionnaire), + full_routing_path=full_routing_path, ) assert answer_object["002"] == "2016-03-30" assert len(answer_object) == 1 -def test_answer_with_zero(fake_questionnaire_store): - fake_questionnaire_store.answer_store = AnswerStore([Answer("GHI", 0).to_dict()]) +def test_answer_with_zero(): + questionnaire_store = get_questionnaire_store() + + questionnaire_store.data_stores.answer_store = AnswerStore( + [Answer("GHI", 0).to_dict()] + ) question = { "id": "question-2", @@ -63,21 +69,24 @@ def test_answer_with_zero(fake_questionnaire_store): questionnaire = make_schema("0.0.1", "section-1", "group-1", "block-1", question) full_routing_path = [ - RoutingPath(["block-1"], section_id="section-1", list_item_id=None) + RoutingPath(block_ids=["block-1"], section_id="section-1", list_item_id=None) ] - answer_object = convert_answers( - QuestionnaireSchema(questionnaire), - fake_questionnaire_store, + schema = QuestionnaireSchema(questionnaire) + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, full_routing_path, - SUBMITTED_AT, ) - assert answer_object["data"]["003"] == "0" + assert data_payload["003"] == "0" -def test_answer_with_float(fake_questionnaire_store): - fake_questionnaire_store.answer_store = AnswerStore( +def test_answer_with_float(): + questionnaire_store = get_questionnaire_store() + + questionnaire_store.data_stores.answer_store = AnswerStore( [Answer("GHI", 10.02).to_dict()] ) @@ -90,22 +99,25 @@ def test_answer_with_float(fake_questionnaire_store): questionnaire = make_schema("0.0.1", "section-1", "group-1", "block-1", question) full_routing_path = [ - RoutingPath(["block-1"], section_id="section-1", list_item_id=None) + RoutingPath(block_ids=["block-1"], section_id="section-1", list_item_id=None) ] - answer_object = convert_answers( - QuestionnaireSchema(questionnaire), - fake_questionnaire_store, + schema = QuestionnaireSchema(questionnaire) + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, full_routing_path, - SUBMITTED_AT, ) # Check the converter correctly - assert answer_object["data"]["003"] == "10.02" + assert data_payload["003"] == "10.02" -def test_answer_with_string(fake_questionnaire_store): - fake_questionnaire_store.answer_store = AnswerStore( +def test_answer_with_string(): + questionnaire_store = get_questionnaire_store() + + questionnaire_store.data_stores.answer_store = AnswerStore( [Answer("GHI", "String test + !").to_dict()] ) @@ -118,22 +130,25 @@ def test_answer_with_string(fake_questionnaire_store): questionnaire = make_schema("0.0.1", "section-1", "group-1", "block-1", question) full_routing_path = [ - RoutingPath(["block-1"], section_id="section-1", list_item_id=None) + RoutingPath(block_ids=["block-1"], section_id="section-1", list_item_id=None) ] - answer_object = convert_answers( - QuestionnaireSchema(questionnaire), - fake_questionnaire_store, + schema = QuestionnaireSchema(questionnaire) + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, full_routing_path, - SUBMITTED_AT, ) # Check the converter correctly - assert answer_object["data"]["003"] == "String test + !" + assert data_payload["003"] == "String test + !" -def test_answer_without_qcode(fake_questionnaire_store): - fake_questionnaire_store.answer_store = AnswerStore( +def test_answer_without_qcode(): + questionnaire_store = get_questionnaire_store() + + questionnaire_store.data_stores.answer_store = AnswerStore( [Answer("GHI", "String test + !").to_dict()] ) @@ -146,22 +161,25 @@ def test_answer_without_qcode(fake_questionnaire_store): questionnaire = make_schema("0.0.1", "section-1", "group-1", "block-1", question) full_routing_path = [ - RoutingPath(["block-1"], section_id="section-1", list_item_id=None) + RoutingPath(block_ids=["block-1"], section_id="section-1", list_item_id=None) ] - answer_object = convert_answers( - QuestionnaireSchema(questionnaire), - fake_questionnaire_store, + schema = QuestionnaireSchema(questionnaire) + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, full_routing_path, - SUBMITTED_AT, ) - assert not answer_object["data"] + assert not data_payload -def test_converter_checkboxes_with_q_codes(fake_questionnaire_store): - full_routing_path = [RoutingPath(["crisps"], section_id="food", list_item_id=None)] - fake_questionnaire_store.answer_store = AnswerStore( +def test_converter_checkboxes_with_q_codes(): + questionnaire_store = get_questionnaire_store() + + full_routing_path = [RoutingPath(block_ids=["crisps"], section_id="food")] + questionnaire_store.data_stores.answer_store = AnswerStore( [Answer("crisps-answer", ["Ready salted", "Sweet chilli"]).to_dict()] ) @@ -202,23 +220,35 @@ def test_converter_checkboxes_with_q_codes(fake_questionnaire_store): ) # When - answer_object = convert_answers( - QuestionnaireSchema(questionnaire), - fake_questionnaire_store, + schema = QuestionnaireSchema(questionnaire) + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, full_routing_path, - SUBMITTED_AT, ) # Then - assert len(answer_object["data"]) == 2 - assert answer_object["data"]["1"] == "Ready salted" - assert answer_object["data"]["2"] == "Sweet chilli" - + assert len(data_payload) == 2 + assert data_payload["1"] == "Ready salted" + assert data_payload["2"] == "Sweet chilli" + + +@pytest.mark.parametrize( + "detail_answer_q_code_field, expected_data_length", + [ + ({"q_code": "401"}, 3), + ({}, 2), + ], +) +def test_converter_checkboxes_with_q_codes_and_other_value( + detail_answer_q_code_field, expected_data_length +): + questionnaire_store = get_questionnaire_store() -def test_converter_checkboxes_with_q_codes_and_other_value(fake_questionnaire_store): - full_routing_path = [RoutingPath(["crisps"], section_id="food", list_item_id=None)] + full_routing_path = [RoutingPath(block_ids=["crisps"], section_id="food")] - fake_questionnaire_store.answer_store = AnswerStore( + questionnaire_store.data_stores.answer_store = AnswerStore( [ Answer("crisps-answer", ["Ready salted", "Other"]).to_dict(), Answer("other-answer-mandatory", "Bacon").to_dict(), @@ -250,6 +280,7 @@ def test_converter_checkboxes_with_q_codes_and_other_value(fake_questionnaire_st "id": "other-answer-mandatory", "label": "Please specify other", "type": "TextField", + **detail_answer_q_code_field, }, }, ], @@ -262,28 +293,32 @@ def test_converter_checkboxes_with_q_codes_and_other_value(fake_questionnaire_st ) # When - answer_object = convert_answers( - QuestionnaireSchema(questionnaire), - fake_questionnaire_store, + schema = QuestionnaireSchema(questionnaire) + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, full_routing_path, - SUBMITTED_AT, ) # Then - assert len(answer_object["data"]) == 2 - assert answer_object["data"]["1"] == "Ready salted" - assert answer_object["data"]["4"] == "Bacon" + assert len(data_payload) == expected_data_length + assert data_payload["1"] == "Ready salted" + assert data_payload["4"] == "Bacon" + # If detail answer has a q_code then that should be used in the data outputted in the payload + if detail_answer_q_code_field: + assert data_payload[detail_answer_q_code_field["q_code"]] == "Bacon" -def test_converter_checkboxes_with_q_codes_and_empty_other_value( - fake_questionnaire_store, -): - full_routing_path = [RoutingPath(["crisps"], section_id="food", list_item_id=None)] - fake_questionnaire_store.answer_store = AnswerStore( +def test_converter_checkboxes_with_missing_detail_answer_value_in_answer_store(): + questionnaire_store = get_questionnaire_store() + + full_routing_path = [RoutingPath(block_ids=["crisps"], section_id="food")] + + questionnaire_store.data_stores.answer_store = AnswerStore( [ Answer("crisps-answer", ["Ready salted", "Other"]).to_dict(), - Answer("other-answer-mandatory", "").to_dict(), ] ) @@ -308,7 +343,7 @@ def test_converter_checkboxes_with_q_codes_and_empty_other_value( "description": "Choose any other flavour", "value": "Other", "detail_answer": { - "mandatory": True, + "mandatory": False, "id": "other-answer-mandatory", "label": "Please specify other", "type": "TextField", @@ -324,25 +359,26 @@ def test_converter_checkboxes_with_q_codes_and_empty_other_value( ) # When - answer_object = convert_answers( - QuestionnaireSchema(questionnaire), - fake_questionnaire_store, + schema = QuestionnaireSchema(questionnaire) + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, full_routing_path, - SUBMITTED_AT, ) # Then - assert len(answer_object["data"]) == 2 - assert answer_object["data"]["1"] == "Ready salted" - assert answer_object["data"]["4"] == "Other" + assert len(data_payload) == 2 + assert data_payload["1"] == "Ready salted" + assert data_payload["4"] == "Other" -def test_converter_checkboxes_with_missing_q_codes_uses_answer_q_code( - fake_questionnaire_store, -): - full_routing_path = [RoutingPath(["crisps"], section_id="food", list_item_id=None)] +def test_converter_checkboxes_with_missing_q_codes_uses_answer_q_code(): + questionnaire_store = get_questionnaire_store() - fake_questionnaire_store.answer_store = AnswerStore( + full_routing_path = [RoutingPath(block_ids=["crisps"], section_id="food")] + + questionnaire_store.data_stores.answer_store = AnswerStore( [Answer("crisps-answer", ["Ready salted", "Sweet chilli"]).to_dict()] ) @@ -384,21 +420,24 @@ def test_converter_checkboxes_with_missing_q_codes_uses_answer_q_code( ) # When - answer_object = convert_answers( - QuestionnaireSchema(questionnaire), - fake_questionnaire_store, + schema = QuestionnaireSchema(questionnaire) + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, full_routing_path, - SUBMITTED_AT, ) # Then - assert len(answer_object["data"]) == 1 - assert answer_object["data"]["0"], "['Ready salted' == 'Sweet chilli']" + assert len(data_payload) == 1 + assert data_payload["0"], "['Ready salted' == 'Sweet chilli']" -def test_converter_q_codes_for_empty_strings(fake_questionnaire_store): - full_routing_path = [RoutingPath(["crisps"], section_id="food", list_item_id=None)] - fake_questionnaire_store.answer_store = AnswerStore( +def test_converter_q_codes_for_empty_strings(): + questionnaire_store = get_questionnaire_store() + + full_routing_path = [RoutingPath(block_ids=["crisps"], section_id="food")] + questionnaire_store.data_stores.answer_store = AnswerStore( [ Answer("crisps-answer", "").to_dict(), Answer("other-crisps-answer", "Ready salted").to_dict(), @@ -424,24 +463,32 @@ def test_converter_q_codes_for_empty_strings(fake_questionnaire_store): ) # When - answer_object = convert_answers( - QuestionnaireSchema(questionnaire), - fake_questionnaire_store, + schema = QuestionnaireSchema(questionnaire) + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, full_routing_path, - SUBMITTED_AT, ) # Then - assert len(answer_object["data"]) == 1 - assert answer_object["data"]["2"] == "Ready salted" + assert len(data_payload) == 1 + assert data_payload["2"] == "Ready salted" -def test_radio_answer(fake_questionnaire_store): +def test_radio_answer(): + questionnaire_store = get_questionnaire_store() + full_routing_path = [ - RoutingPath(["radio-block"], section_id="section-1", list_item_id=None) + RoutingPath( + block_ids=["radio-block"], section_id="section-1", list_item_id=None + ) ] - fake_questionnaire_store.answer_store = AnswerStore( - [Answer("radio-answer", "Coffee").to_dict()] + questionnaire_store.data_stores.answer_store = AnswerStore( + [ + Answer("radio-answer", "Coffee").to_dict(), + Answer("other-answer-mandatory", "Water").to_dict(), + ], ) question = { @@ -455,6 +502,17 @@ def test_radio_answer(fake_questionnaire_store): "options": [ {"label": "Coffee", "value": "Coffee"}, {"label": "Tea", "value": "Tea"}, + { + "label": "Other", + "value": "Other", + "detail_answer": { + "mandatory": True, + "id": "other-answer-mandatory", + "label": "Please specify other", + "type": "TextField", + "q_code": "101", + }, + }, ], } ], @@ -464,23 +522,29 @@ def test_radio_answer(fake_questionnaire_store): ) # When - answer_object = convert_answers( - QuestionnaireSchema(questionnaire), - fake_questionnaire_store, + schema = QuestionnaireSchema(questionnaire) + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, full_routing_path, - SUBMITTED_AT, ) # Then - assert len(answer_object["data"]) == 1 - assert answer_object["data"]["1"] == "Coffee" + assert len(data_payload) == 2 + assert data_payload["1"] == "Coffee" + assert data_payload["101"] == "Water" + +def test_number_answer(): + questionnaire_store = get_questionnaire_store() -def test_number_answer(fake_questionnaire_store): full_routing_path = [ - RoutingPath(["number-block"], section_id="section-1", list_item_id=None) + RoutingPath( + block_ids=["number-block"], section_id="section-1", list_item_id=None + ) ] - fake_questionnaire_store.answer_store = AnswerStore( + questionnaire_store.data_stores.answer_store = AnswerStore( [Answer("number-answer", 0.9999).to_dict()] ) @@ -495,23 +559,28 @@ def test_number_answer(fake_questionnaire_store): ) # When - answer_object = convert_answers( - QuestionnaireSchema(questionnaire), - fake_questionnaire_store, + schema = QuestionnaireSchema(questionnaire) + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, full_routing_path, - SUBMITTED_AT, ) # Then - assert len(answer_object["data"]) == 1 - assert answer_object["data"]["1"] == "0.9999" + assert len(data_payload) == 1 + assert data_payload["1"] == "0.9999" + +def test_percentage_answer(): + questionnaire_store = get_questionnaire_store() -def test_percentage_answer(fake_questionnaire_store): full_routing_path = [ - RoutingPath(["percentage-block"], section_id="section-1", list_item_id=None) + RoutingPath( + block_ids=["percentage-block"], section_id="section-1", list_item_id=None + ) ] - fake_questionnaire_store.answer_store = AnswerStore( + questionnaire_store.data_stores.answer_store = AnswerStore( [Answer("percentage-answer", 100).to_dict()] ) @@ -526,23 +595,28 @@ def test_percentage_answer(fake_questionnaire_store): ) # When - answer_object = convert_answers( - QuestionnaireSchema(questionnaire), - fake_questionnaire_store, + schema = QuestionnaireSchema(questionnaire) + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, full_routing_path, - SUBMITTED_AT, ) # Then - assert len(answer_object["data"]) == 1 - assert answer_object["data"]["1"] == "100" + assert len(data_payload) == 1 + assert data_payload["1"] == "100" -def test_textarea_answer(fake_questionnaire_store): +def test_textarea_answer(): + questionnaire_store = get_questionnaire_store() + full_routing_path = [ - RoutingPath(["textarea-block"], section_id="section-1", list_item_id=None) + RoutingPath( + block_ids=["textarea-block"], section_id="section-1", list_item_id=None + ) ] - fake_questionnaire_store.answer_store = AnswerStore( + questionnaire_store.data_stores.answer_store = AnswerStore( [Answer("textarea-answer", "example text.").to_dict()] ) @@ -557,23 +631,28 @@ def test_textarea_answer(fake_questionnaire_store): ) # When - answer_object = convert_answers( - QuestionnaireSchema(questionnaire), - fake_questionnaire_store, + schema = QuestionnaireSchema(questionnaire) + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, full_routing_path, - SUBMITTED_AT, ) # Then - assert len(answer_object["data"]) == 1 - assert answer_object["data"]["1"] == "example text." + assert len(data_payload) == 1 + assert data_payload["1"] == "example text." -def test_currency_answer(fake_questionnaire_store): +def test_currency_answer(): + questionnaire_store = get_questionnaire_store() + full_routing_path = [ - RoutingPath(["currency-block"], section_id="section-1", list_item_id=None) + RoutingPath( + block_ids=["currency-block"], section_id="section-1", list_item_id=None + ) ] - fake_questionnaire_store.answer_store = AnswerStore( + questionnaire_store.data_stores.answer_store = AnswerStore( [Answer("currency-answer", 99.99).to_dict()] ) @@ -588,23 +667,28 @@ def test_currency_answer(fake_questionnaire_store): ) # When - answer_object = convert_answers( - QuestionnaireSchema(questionnaire), - fake_questionnaire_store, + schema = QuestionnaireSchema(questionnaire) + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, full_routing_path, - SUBMITTED_AT, ) # Then - assert len(answer_object["data"]) == 1 - assert answer_object["data"]["1"] == "99.99" + assert len(data_payload) == 1 + assert data_payload["1"] == "99.99" -def test_dropdown_answer(fake_questionnaire_store): +def test_dropdown_answer(): + questionnaire_store = get_questionnaire_store() + full_routing_path = [ - RoutingPath(["dropdown-block"], section_id="section-1", list_item_id=None) + RoutingPath( + block_ids=["dropdown-block"], section_id="section-1", list_item_id=None + ) ] - fake_questionnaire_store.answer_store = AnswerStore( + questionnaire_store.data_stores.answer_store = AnswerStore( [Answer("dropdown-answer", "Liverpool").to_dict()] ) @@ -630,24 +714,25 @@ def test_dropdown_answer(fake_questionnaire_store): ) # When - answer_object = convert_answers( - QuestionnaireSchema(questionnaire), - fake_questionnaire_store, + schema = QuestionnaireSchema(questionnaire) + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, full_routing_path, - SUBMITTED_AT, ) # Then - assert len(answer_object["data"]) == 1 - assert answer_object["data"]["1"] == "Liverpool" + assert len(data_payload) == 1 + assert data_payload["1"] == "Liverpool" -def test_date_answer(fake_questionnaire_store): - full_routing_path = [ - RoutingPath(["date-block"], section_id="section-1", list_item_id=None) - ] +def test_date_answer(): + questionnaire_store = get_questionnaire_store() + + full_routing_path = [RoutingPath(block_ids=["date-block"], section_id="section-1")] - fake_questionnaire_store.answer_store = AnswerStore( + questionnaire_store.data_stores.answer_store = AnswerStore( [ create_answer("single-date-answer", "1990-02-01"), create_answer("month-year-answer", "1990-01"), @@ -668,24 +753,25 @@ def test_date_answer(fake_questionnaire_store): ) # When - answer_object = convert_answers( - QuestionnaireSchema(questionnaire), - fake_questionnaire_store, + schema = QuestionnaireSchema(questionnaire) + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, full_routing_path, - SUBMITTED_AT, ) # Then - assert len(answer_object["data"]) == 2 - assert answer_object["data"]["1"] == "01/02/1990" - assert answer_object["data"]["2"] == "01/1990" + assert len(data_payload) == 2 + assert data_payload["1"] == "01/02/1990" + assert data_payload["2"] == "01/1990" -def test_unit_answer(fake_questionnaire_store): - full_routing_path = [ - RoutingPath(["unit-block"], section_id="section-1", list_item_id=None) - ] - fake_questionnaire_store.answer_store = AnswerStore( +def test_unit_answer(): + questionnaire_store = get_questionnaire_store() + + full_routing_path = [RoutingPath(block_ids=["unit-block"], section_id="section-1")] + questionnaire_store.data_stores.answer_store = AnswerStore( [Answer("unit-answer", 10).to_dict()] ) @@ -700,13 +786,14 @@ def test_unit_answer(fake_questionnaire_store): ) # When - answer_object = convert_answers( - QuestionnaireSchema(questionnaire), - fake_questionnaire_store, + schema = QuestionnaireSchema(questionnaire) + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, full_routing_path, - SUBMITTED_AT, ) # Then - assert len(answer_object["data"]) == 1 - assert answer_object["data"]["1"] == "10" + assert len(data_payload) == 1 + assert data_payload["1"] == "10" diff --git a/tests/app/submitter/test_convert_payload_0_0_3.py b/tests/app/submitter/test_convert_payload_0_0_3.py index 32874f6223..5bc2b82780 100644 --- a/tests/app/submitter/test_convert_payload_0_0_3.py +++ b/tests/app/submitter/test_convert_payload_0_0_3.py @@ -1,24 +1,32 @@ +# pylint: disable=too-many-lines from datetime import datetime, timezone from app.data_models.answer import Answer from app.data_models.answer_store import AnswerStore from app.data_models.list_store import ListStore +from app.data_models.supplementary_data_store import SupplementaryDataStore from app.questionnaire.questionnaire_schema import QuestionnaireSchema from app.questionnaire.routing_path import RoutingPath -from app.submitter.converter import convert_answers +from app.submitter.converter_v2 import get_payload_data from app.utilities.json import json_dumps, json_loads +from app.utilities.make_immutable import make_immutable from app.utilities.schema import load_schema_from_name +from tests.app.submitter.conftest import get_questionnaire_store from tests.app.submitter.schema import make_schema SUBMITTED_AT = datetime.now(timezone.utc) -def test_convert_answers_to_payload_0_0_3(fake_questionnaire_store): +def test_convert_answers_v2_to_payload_0_0_3(): + questionnaire_store = get_questionnaire_store() + full_routing_path = [ - RoutingPath(["about you", "where you live"], section_id="household-section") + RoutingPath( + block_ids=["about you", "where you live"], section_id="household-section" + ) ] - fake_questionnaire_store.answer_store = AnswerStore( + questionnaire_store.data_stores.answer_store = AnswerStore( [ Answer("name", "Joe Bloggs", None).to_dict(), Answer("address", "62 Somewhere", None).to_dict(), @@ -66,25 +74,28 @@ def test_convert_answers_to_payload_0_0_3(fake_questionnaire_store): } # When - answer_object = convert_answers( - QuestionnaireSchema(questionnaire), - fake_questionnaire_store, + schema = QuestionnaireSchema(questionnaire) + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, full_routing_path, - SUBMITTED_AT, ) # Then - assert len(answer_object["data"]["answers"]) == 2 - assert answer_object["data"]["answers"][0].value == "Joe Bloggs" - assert answer_object["data"]["answers"][1].value, "62 Somewhere" + assert len(data_payload["answers"]) == 2 + assert data_payload["answers"][0].value == "Joe Bloggs" + assert data_payload["answers"][1].value, "62 Somewhere" -def test_convert_payload_0_0_3_multiple_answers(fake_questionnaire_store): - full_routing_path = [RoutingPath(["crisps"], section_id="section-1")] +def test_convert_payload_0_0_3_multiple_answers(): + questionnaire_store = get_questionnaire_store() + + full_routing_path = [RoutingPath(block_ids=["crisps"], section_id="section-1")] answers = AnswerStore( [Answer("crisps-answer", ["Ready salted", "Sweet chilli"]).to_dict()] ) - fake_questionnaire_store.answer_store = answers + questionnaire_store.data_stores.answer_store = answers questionnaire = make_schema( "0.0.3", @@ -109,21 +120,25 @@ def test_convert_payload_0_0_3_multiple_answers(fake_questionnaire_store): ) # When - answer_object = convert_answers( - QuestionnaireSchema(questionnaire), - fake_questionnaire_store, + schema = QuestionnaireSchema(questionnaire) + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, full_routing_path, - SUBMITTED_AT, ) + # Then - assert len(answer_object["data"]["answers"]) == 1 - assert answer_object["data"]["answers"][0].value == ["Ready salted", "Sweet chilli"] + assert len(data_payload["answers"]) == 1 + assert data_payload["answers"][0].value == ["Ready salted", "Sweet chilli"] -def test_radio_answer(fake_questionnaire_store): - full_routing_path = [RoutingPath(["radio-block"], section_id="section-1")] +def test_radio_answer(): + questionnaire_store = get_questionnaire_store() + + full_routing_path = [RoutingPath(block_ids=["radio-block"], section_id="section-1")] answers = AnswerStore([Answer("radio-answer", "Coffee").to_dict()]) - fake_questionnaire_store.answer_store = answers + questionnaire_store.data_stores.answer_store = answers questionnaire = make_schema( "0.0.3", @@ -146,21 +161,26 @@ def test_radio_answer(fake_questionnaire_store): }, ) - answer_object = convert_answers( - QuestionnaireSchema(questionnaire), - fake_questionnaire_store, + schema = QuestionnaireSchema(questionnaire) + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, full_routing_path, - SUBMITTED_AT, ) - assert len(answer_object["data"]["answers"]) == 1 - assert answer_object["data"]["answers"][0].value == "Coffee" + assert len(data_payload["answers"]) == 1 + assert data_payload["answers"][0].value == "Coffee" + +def test_number_answer(): + questionnaire_store = get_questionnaire_store() -def test_number_answer(fake_questionnaire_store): - full_routing_path = [RoutingPath(["number-block"], section_id="section-1")] + full_routing_path = [ + RoutingPath(block_ids=["number-block"], section_id="section-1") + ] answers = AnswerStore([Answer("number-answer", 1.755).to_dict()]) - fake_questionnaire_store.answer_store = answers + questionnaire_store.data_stores.answer_store = answers questionnaire = make_schema( "0.0.3", @@ -174,21 +194,26 @@ def test_number_answer(fake_questionnaire_store): }, ) - answer_object = convert_answers( - QuestionnaireSchema(questionnaire), - fake_questionnaire_store, + schema = QuestionnaireSchema(questionnaire) + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, full_routing_path, - SUBMITTED_AT, ) - assert len(answer_object["data"]["answers"]) == 1 - assert answer_object["data"]["answers"][0].value == 1.755 + assert len(data_payload["answers"]) == 1 + assert data_payload["answers"][0].value == 1.755 -def test_percentage_answer(fake_questionnaire_store): - full_routing_path = [RoutingPath(["percentage-block"], section_id="section-1")] +def test_percentage_answer(): + questionnaire_store = get_questionnaire_store() + + full_routing_path = [ + RoutingPath(block_ids=["percentage-block"], section_id="section-1") + ] answers = AnswerStore([Answer("percentage-answer", 99).to_dict()]) - fake_questionnaire_store.answer_store = answers + questionnaire_store.data_stores.answer_store = answers questionnaire = make_schema( "0.0.3", @@ -202,23 +227,28 @@ def test_percentage_answer(fake_questionnaire_store): }, ) - answer_object = convert_answers( - QuestionnaireSchema(questionnaire), - fake_questionnaire_store, + schema = QuestionnaireSchema(questionnaire) + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, full_routing_path, - SUBMITTED_AT, ) - assert len(answer_object["data"]["answers"]) == 1 - assert answer_object["data"]["answers"][0].value == 99 + assert len(data_payload["answers"]) == 1 + assert data_payload["answers"][0].value == 99 + +def test_textarea_answer(): + questionnaire_store = get_questionnaire_store() -def test_textarea_answer(fake_questionnaire_store): - full_routing_path = [RoutingPath(["textarea-block"], section_id="section-1")] + full_routing_path = [ + RoutingPath(block_ids=["textarea-block"], section_id="section-1") + ] answers = AnswerStore( [Answer("textarea-answer", "This is an example text!").to_dict()] ) - fake_questionnaire_store.answer_store = answers + questionnaire_store.data_stores.answer_store = answers questionnaire = make_schema( "0.0.3", @@ -232,21 +262,26 @@ def test_textarea_answer(fake_questionnaire_store): }, ) - answer_object = convert_answers( - QuestionnaireSchema(questionnaire), - fake_questionnaire_store, + schema = QuestionnaireSchema(questionnaire) + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, full_routing_path, - SUBMITTED_AT, ) - assert len(answer_object["data"]["answers"]) == 1 - assert answer_object["data"]["answers"][0].value == "This is an example text!" + assert len(data_payload["answers"]) == 1 + assert data_payload["answers"][0].value == "This is an example text!" + +def test_currency_answer(): + questionnaire_store = get_questionnaire_store() -def test_currency_answer(fake_questionnaire_store): - full_routing_path = [RoutingPath(["currency-block"], section_id="section-1")] + full_routing_path = [ + RoutingPath(block_ids=["currency-block"], section_id="section-1") + ] answers = AnswerStore([Answer("currency-answer", 100).to_dict()]) - fake_questionnaire_store.answer_store = answers + questionnaire_store.data_stores.answer_store = answers questionnaire = make_schema( "0.0.3", @@ -260,21 +295,26 @@ def test_currency_answer(fake_questionnaire_store): }, ) - answer_object = convert_answers( - QuestionnaireSchema(questionnaire), - fake_questionnaire_store, + schema = QuestionnaireSchema(questionnaire) + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, full_routing_path, - SUBMITTED_AT, ) - assert len(answer_object["data"]["answers"]) == 1 - assert answer_object["data"]["answers"][0].value == 100 + assert len(data_payload["answers"]) == 1 + assert data_payload["answers"][0].value == 100 -def test_dropdown_answer(fake_questionnaire_store): - full_routing_path = [RoutingPath(["dropdown-block"], section_id="section-1")] +def test_dropdown_answer(): + questionnaire_store = get_questionnaire_store() + + full_routing_path = [ + RoutingPath(block_ids=["dropdown-block"], section_id="section-1") + ] answers = AnswerStore([Answer("dropdown-answer", "Rugby is better!").to_dict()]) - fake_questionnaire_store.answer_store = answers + questionnaire_store.data_stores.answer_store = answers questionnaire = make_schema( "0.0.3", @@ -298,27 +338,30 @@ def test_dropdown_answer(fake_questionnaire_store): }, ) - answer_object = convert_answers( - QuestionnaireSchema(questionnaire), - fake_questionnaire_store, + schema = QuestionnaireSchema(questionnaire) + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, full_routing_path, - SUBMITTED_AT, ) # Then - assert len(answer_object["data"]["answers"]) == 1 - assert answer_object["data"]["answers"][0].value == "Rugby is better!" + assert len(data_payload["answers"]) == 1 + assert data_payload["answers"][0].value == "Rugby is better!" + +def test_date_answer(): + questionnaire_store = get_questionnaire_store() -def test_date_answer(fake_questionnaire_store): - full_routing_path = [RoutingPath(["date-block"], section_id="section-1")] + full_routing_path = [RoutingPath(block_ids=["date-block"], section_id="section-1")] answers = AnswerStore( [ Answer("single-date-answer", "01-01-1990").to_dict(), Answer("month-year-answer", "01-1990").to_dict(), ] ) - fake_questionnaire_store.answer_store = answers + questionnaire_store.data_stores.answer_store = answers questionnaire = make_schema( "0.0.3", @@ -332,27 +375,30 @@ def test_date_answer(fake_questionnaire_store): }, ) - answer_object = convert_answers( - QuestionnaireSchema(questionnaire), - fake_questionnaire_store, + schema = QuestionnaireSchema(questionnaire) + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, full_routing_path, - SUBMITTED_AT, ) - assert len(answer_object["data"]["answers"]) == 1 + assert len(data_payload["answers"]) == 1 - assert answer_object["data"]["answers"][0].value == "01-01-1990" + assert data_payload["answers"][0].value == "01-01-1990" -def test_month_year_date_answer(fake_questionnaire_store): - full_routing_path = [RoutingPath(["date-block"], section_id="section-1")] +def test_month_year_date_answer(): + questionnaire_store = get_questionnaire_store() + + full_routing_path = [RoutingPath(block_ids=["date-block"], section_id="section-1")] answers = AnswerStore( [ Answer("single-date-answer", "01-01-1990").to_dict(), Answer("month-year-answer", "01-1990").to_dict(), ] ) - fake_questionnaire_store.answer_store = answers + questionnaire_store.data_stores.answer_store = answers questionnaire = make_schema( "0.0.3", @@ -366,22 +412,25 @@ def test_month_year_date_answer(fake_questionnaire_store): }, ) - answer_object = convert_answers( - QuestionnaireSchema(questionnaire), - fake_questionnaire_store, + schema = QuestionnaireSchema(questionnaire) + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, full_routing_path, - SUBMITTED_AT, ) - assert len(answer_object["data"]["answers"]) == 1 + assert len(data_payload["answers"]) == 1 + + assert data_payload["answers"][0].value == "01-1990" - assert answer_object["data"]["answers"][0].value == "01-1990" +def test_unit_answer(): + questionnaire_store = get_questionnaire_store() -def test_unit_answer(fake_questionnaire_store): - full_routing_path = [RoutingPath(["unit-block"], section_id="section-1")] + full_routing_path = [RoutingPath(block_ids=["unit-block"], section_id="section-1")] answers = AnswerStore([Answer("unit-answer", 10).to_dict()]) - fake_questionnaire_store.answer_store = answers + questionnaire_store.data_stores.answer_store = answers questionnaire = make_schema( "0.0.3", @@ -395,21 +444,25 @@ def test_unit_answer(fake_questionnaire_store): }, ) - answer_object = convert_answers( - QuestionnaireSchema(questionnaire), - fake_questionnaire_store, + schema = QuestionnaireSchema(questionnaire) + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, full_routing_path, - SUBMITTED_AT, ) - assert len(answer_object["data"]["answers"]) == 1 - assert answer_object["data"]["answers"][0].value == 10 + assert len(data_payload["answers"]) == 1 + assert data_payload["answers"][0].value == 10 + +def test_primary_person_list_item_conversion(): + questionnaire_store = get_questionnaire_store() -def test_primary_person_list_item_conversion(fake_questionnaire_store): routing_path = [ RoutingPath( - ["primary-person-list-collector", "list-collector"], section_id="section-1" + block_ids=["primary-person-list-collector", "list-collector"], + section_id="section-1", ) ] @@ -425,7 +478,7 @@ def test_primary_person_list_item_conversion(fake_questionnaire_store): answers = AnswerStore(answer_objects) list_store = ListStore( - existing_items=[ + items=[ { "name": "people", "items": ["xJlKBy", "RfAGDc"], @@ -434,26 +487,34 @@ def test_primary_person_list_item_conversion(fake_questionnaire_store): ] ) - fake_questionnaire_store.answer_store = answers - fake_questionnaire_store.list_store = list_store + questionnaire_store.data_stores.answer_store = answers + questionnaire_store.data_stores.list_store = list_store schema = load_schema_from_name("test_list_collector_primary_person") - output = convert_answers( - schema, fake_questionnaire_store, routing_path, SUBMITTED_AT + output = get_payload_data( + questionnaire_store.data_stores, + schema, + routing_path, ) - data_dict = json_loads(json_dumps(output["data"]["answers"])) + data_dict = json_loads(json_dumps(output["answers"])) assert sorted(answer_objects, key=lambda x: x["answer_id"]) == sorted( data_dict, key=lambda x: x["answer_id"] ) -def test_list_item_conversion(fake_questionnaire_store): +def test_list_item_conversion(): + questionnaire_store = get_questionnaire_store() + routing_path = [ RoutingPath( - ["list-collector", "next-interstitial", "another-list-collector-block"], + block_ids=[ + "list-collector", + "next-interstitial", + "another-list-collector-block", + ], section_id="section-1", ) ] @@ -470,34 +531,40 @@ def test_list_item_conversion(fake_questionnaire_store): answers = AnswerStore(answer_objects) - list_store = ListStore( - existing_items=[{"name": "people", "items": ["xJlKBy", "RfAGDc"]}] - ) + list_store = ListStore(items=[{"name": "people", "items": ["xJlKBy", "RfAGDc"]}]) - fake_questionnaire_store.answer_store = answers - fake_questionnaire_store.list_store = list_store + questionnaire_store.data_stores.answer_store = answers + questionnaire_store.data_stores.list_store = list_store schema = load_schema_from_name("test_list_collector") - output = convert_answers( - schema, fake_questionnaire_store, routing_path, SUBMITTED_AT + output = get_payload_data( + questionnaire_store.data_stores, + schema, + routing_path, ) del answer_objects[-1] - data_dict = json_loads(json_dumps(output["data"]["answers"])) + data_dict = json_loads(json_dumps(output["answers"])) assert sorted(answer_objects, key=lambda x: x["answer_id"]) == sorted( data_dict, key=lambda x: x["answer_id"] ) -def test_list_item_conversion_empty_list(fake_questionnaire_store): +def test_list_item_conversion_empty_list(): """Test that the list store is populated with an empty list for lists which do not have answers yet.""" + questionnaire_store = get_questionnaire_store() + routing_path = [ RoutingPath( - ["list-collector", "next-interstitial", "another-list-collector-block"], + block_ids=[ + "list-collector", + "next-interstitial", + "another-list-collector-block", + ], section_id="section-1", ) ] @@ -509,55 +576,66 @@ def test_list_item_conversion_empty_list(fake_questionnaire_store): {"answer_id": "extraneous-answer", "value": "Bad", "list_item_id": "123"}, ] - fake_questionnaire_store.answer_store = AnswerStore(answer_objects) - fake_questionnaire_store.list_store = ListStore() + questionnaire_store.data_stores.answer_store = AnswerStore(answer_objects) + questionnaire_store.data_stores.list_store = ListStore() schema = load_schema_from_name("test_list_collector") - output = convert_answers( - schema, fake_questionnaire_store, routing_path, SUBMITTED_AT + output = get_payload_data( + questionnaire_store.data_stores, + schema, + routing_path, ) # Answers not on the routing path del answer_objects[0] del answer_objects[-1] - data_dict = json_loads(json_dumps(output["data"]["answers"])) + data_dict = json_loads(json_dumps(output["answers"])) assert sorted(answer_objects, key=lambda x: x["answer_id"]) == sorted( data_dict, key=lambda x: x["answer_id"] ) -def test_default_answers_not_present_when_not_answered(fake_questionnaire_store): +def test_default_answers_not_present_when_not_answered(): """Test that default values aren't submitted downstream when an answer with a default value is not present in the answer store.""" + questionnaire_store = get_questionnaire_store() + schema = load_schema_from_name("test_default") answer_objects = [{"answer_id": "number-question-two", "value": "12"}] - fake_questionnaire_store.answer_store = AnswerStore(answer_objects) - fake_questionnaire_store.list_store = ListStore() + questionnaire_store.data_stores.answer_store = AnswerStore(answer_objects) + questionnaire_store.data_stores.list_store = ListStore() routing_path = [ RoutingPath( - ["number-question-one", "number-question-two"], section_id="default-section" + block_ids=["number-question-one", "number-question-two"], + section_id="default-section", ) ] - output = convert_answers( - schema, fake_questionnaire_store, routing_path, SUBMITTED_AT + output = get_payload_data( + questionnaire_store.data_stores, + schema, + routing_path, ) - data = json_loads(json_dumps(output["data"]["answers"])) + + data = json_loads(json_dumps(output["answers"])) answer_ids = {answer["answer_id"] for answer in data} assert "answer-one" not in answer_ids -def test_list_structure_in_payload_is_as_expected(fake_questionnaire_store): +def test_list_structure_in_payload_is_as_expected(): + questionnaire_store = get_questionnaire_store() + routing_path = [ RoutingPath( - ["primary-person-list-collector", "list-collector"], section_id="section-1" + block_ids=["primary-person-list-collector", "list-collector"], + section_id="section-1", ) ] @@ -573,7 +651,7 @@ def test_list_structure_in_payload_is_as_expected(fake_questionnaire_store): answers = AnswerStore(answer_objects) list_store = ListStore( - existing_items=[ + items=[ { "name": "people", "items": ["xJlKBy", "RfAGDc"], @@ -582,26 +660,34 @@ def test_list_structure_in_payload_is_as_expected(fake_questionnaire_store): ] ) - fake_questionnaire_store.answer_store = answers - fake_questionnaire_store.list_store = list_store + questionnaire_store.data_stores.answer_store = answers + questionnaire_store.data_stores.list_store = list_store schema = load_schema_from_name("test_list_collector_primary_person") - output = convert_answers( - schema, fake_questionnaire_store, routing_path, SUBMITTED_AT + output = get_payload_data( + questionnaire_store.data_stores, + schema, + routing_path, ) - data_dict = json_loads(json_dumps(output["data"]["lists"])) + data_dict = json_loads(json_dumps(output["lists"])) assert data_dict[0]["name"] == "people" assert "xJlKBy" in data_dict[0]["items"] assert data_dict[0]["primary_person"] == "xJlKBy" -def test_primary_person_not_in_payload_when_not_answered(fake_questionnaire_store): +def test_primary_person_not_in_payload_when_not_answered(): + questionnaire_store = get_questionnaire_store() + routing_path = [ RoutingPath( - ["list-collector", "next-interstitial", "another-list-collector-block"], + block_ids=[ + "list-collector", + "next-interstitial", + "another-list-collector-block", + ], section_id="section-1", ) ] @@ -618,28 +704,30 @@ def test_primary_person_not_in_payload_when_not_answered(fake_questionnaire_stor answers = AnswerStore(answer_objects) - list_store = ListStore( - existing_items=[{"name": "people", "items": ["xJlKBy", "RfAGDc"]}] - ) + list_store = ListStore(items=[{"name": "people", "items": ["xJlKBy", "RfAGDc"]}]) - fake_questionnaire_store.answer_store = answers - fake_questionnaire_store.list_store = list_store + questionnaire_store.data_stores.answer_store = answers + questionnaire_store.data_stores.list_store = list_store schema = load_schema_from_name("test_list_collector") - output = convert_answers( - schema, fake_questionnaire_store, routing_path, SUBMITTED_AT + output = get_payload_data( + questionnaire_store.data_stores, + schema, + routing_path, ) - data_dict = json_loads(json_dumps(output["data"]["lists"])) + data_dict = json_loads(json_dumps(output["lists"])) assert "primary_person" not in data_dict[0] -def test_relationships_in_payload(fake_questionnaire_store): +def test_relationships_in_payload(): + questionnaire_store = get_questionnaire_store() + routing_path = [ RoutingPath( - ["list-collector", "relationships"], + block_ids=["list-collector", "relationships"], section_id="section", ) ] @@ -672,7 +760,7 @@ def test_relationships_in_payload(fake_questionnaire_store): answers = AnswerStore(answer_objects) list_store = ListStore( - existing_items=[ + items=[ { "name": "people", "items": [ @@ -684,15 +772,18 @@ def test_relationships_in_payload(fake_questionnaire_store): ] ) - fake_questionnaire_store.answer_store = answers - fake_questionnaire_store.list_store = list_store + questionnaire_store.data_stores.answer_store = answers + questionnaire_store.data_stores.list_store = list_store schema = load_schema_from_name("test_relationships") - output = convert_answers( - schema, fake_questionnaire_store, routing_path, SUBMITTED_AT + output = get_payload_data( + questionnaire_store.data_stores, + schema, + routing_path, ) - data = json_loads(json_dumps(output["data"]["answers"])) + + data = json_loads(json_dumps(output["answers"])) answers = {answer["answer_id"]: answer for answer in data} expected_relationships_answer = [ @@ -712,10 +803,12 @@ def test_relationships_in_payload(fake_questionnaire_store): assert expected_relationships_answer == relationships_answer["value"] -def test_no_relationships_in_payload(fake_questionnaire_store): +def test_no_relationships_in_payload(): + questionnaire_store = get_questionnaire_store() + routing_path = [ RoutingPath( - ["list-collector", "relationships"], + block_ids=["list-collector", "relationships"], section_id="section", ) ] @@ -733,7 +826,7 @@ def test_no_relationships_in_payload(fake_questionnaire_store): answers = AnswerStore(answer_objects) list_store = ListStore( - existing_items=[ + items=[ { "name": "people", "items": [ @@ -745,24 +838,29 @@ def test_no_relationships_in_payload(fake_questionnaire_store): ] ) - fake_questionnaire_store.answer_store = answers - fake_questionnaire_store.list_store = list_store + questionnaire_store.data_stores.answer_store = answers + questionnaire_store.data_stores.list_store = list_store schema = load_schema_from_name("test_relationships_unrelated") - output = convert_answers( - schema, fake_questionnaire_store, routing_path, SUBMITTED_AT + output = get_payload_data( + questionnaire_store.data_stores, + schema, + routing_path, ) - data = json_loads(json_dumps(output["data"]["answers"])) + + data = json_loads(json_dumps(output["answers"])) answers = {answer["answer_id"]: answer for answer in data} assert "relationship-answer" not in answers -def test_unrelated_block_answers_in_payload(fake_questionnaire_store): +def test_unrelated_block_answers_in_payload(): + questionnaire_store = get_questionnaire_store() + routing_path = [ RoutingPath( - ["list-collector", "relationships"], + block_ids=["list-collector", "relationships"], section_id="section", ) ] @@ -809,7 +907,7 @@ def test_unrelated_block_answers_in_payload(fake_questionnaire_store): answers = AnswerStore(answer_objects) list_store = ListStore( - existing_items=[ + items=[ { "name": "people", "items": [ @@ -823,15 +921,18 @@ def test_unrelated_block_answers_in_payload(fake_questionnaire_store): ] ) - fake_questionnaire_store.answer_store = answers - fake_questionnaire_store.list_store = list_store + questionnaire_store.data_stores.answer_store = answers + questionnaire_store.data_stores.list_store = list_store schema = load_schema_from_name("test_relationships_unrelated") - output = convert_answers( - schema, fake_questionnaire_store, routing_path, SUBMITTED_AT + output = get_payload_data( + questionnaire_store.data_stores, + schema, + routing_path, ) - data = json_loads(json_dumps(output["data"]["answers"])) + + data = json_loads(json_dumps(output["answers"])) answers = { (answer["answer_id"], answer.get("list_item_id")): answer for answer in data } @@ -859,10 +960,12 @@ def test_unrelated_block_answers_in_payload(fake_questionnaire_store): assert expected_relationships_answer == relationships_answer["value"] -def test_unrelated_block_answers_not_on_path_not_in_payload(fake_questionnaire_store): +def test_unrelated_block_answers_not_on_path_not_in_payload(): + questionnaire_store = get_questionnaire_store() + routing_path = [ RoutingPath( - ["list-collector", "relationships"], + block_ids=["list-collector", "relationships"], section_id="section", ) ] @@ -904,7 +1007,7 @@ def test_unrelated_block_answers_not_on_path_not_in_payload(fake_questionnaire_s answers = AnswerStore(answer_objects) list_store = ListStore( - existing_items=[ + items=[ { "name": "people", "items": [ @@ -918,15 +1021,18 @@ def test_unrelated_block_answers_not_on_path_not_in_payload(fake_questionnaire_s ] ) - fake_questionnaire_store.answer_store = answers - fake_questionnaire_store.list_store = list_store + questionnaire_store.data_stores.answer_store = answers + questionnaire_store.data_stores.list_store = list_store schema = load_schema_from_name("test_relationships_unrelated") - output = convert_answers( - schema, fake_questionnaire_store, routing_path, SUBMITTED_AT + output = get_payload_data( + questionnaire_store.data_stores, + schema, + routing_path, ) - data = json_loads(json_dumps(output["data"]["answers"])) + + data = json_loads(json_dumps(output["answers"])) answers = { (answer["answer_id"], answer.get("list_item_id")): answer for answer in data } @@ -934,10 +1040,12 @@ def test_unrelated_block_answers_not_on_path_not_in_payload(fake_questionnaire_s assert ("related-to-anyone-else-answer", "person1") not in answers -def test_relationship_answers_not_on_path_in_payload(fake_questionnaire_store): +def test_relationship_answers_not_on_path_in_payload(): + questionnaire_store = get_questionnaire_store() + routing_path = [ RoutingPath( - ["list-collector", "relationships"], + block_ids=["list-collector", "relationships"], section_id="section", ) ] @@ -989,7 +1097,7 @@ def test_relationship_answers_not_on_path_in_payload(fake_questionnaire_store): answers = AnswerStore(answer_objects) list_store = ListStore( - existing_items=[ + items=[ { "name": "people", "items": [ @@ -1003,15 +1111,18 @@ def test_relationship_answers_not_on_path_in_payload(fake_questionnaire_store): ] ) - fake_questionnaire_store.answer_store = answers - fake_questionnaire_store.list_store = list_store + questionnaire_store.data_stores.answer_store = answers + questionnaire_store.data_stores.list_store = list_store schema = load_schema_from_name("test_relationships_unrelated") - output = convert_answers( - schema, fake_questionnaire_store, routing_path, SUBMITTED_AT + output = get_payload_data( + questionnaire_store.data_stores, + schema, + routing_path, ) - data = json_loads(json_dumps(output["data"]["answers"])) + + data = json_loads(json_dumps(output["answers"])) answers = { (answer["answer_id"], answer.get("list_item_id")): answer for answer in data } @@ -1042,3 +1153,288 @@ def test_relationship_answers_not_on_path_in_payload(fake_questionnaire_store): assert ("related-to-anyone-else-answer", "person1") in answers relationships_answer = answers[("relationship-answer", None)] assert expected_relationships_answer == relationships_answer["value"] + + +def test_answers_codes_only_present_for_answered_questions(): + questionnaire_store = get_questionnaire_store() + + full_routing_path = [ + RoutingPath( + block_ids=["mandatory-checkbox", "name-block"], section_id="default-section" + ) + ] + + questionnaire_store.data_stores.answer_store = AnswerStore( + [ + Answer("name-answer", "Joe Bloggs", None).to_dict(), + ] + ) + + schema = load_schema_from_name("test_answer_codes") + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, + full_routing_path, + ) + + # Then + assert len(data_payload["answer_codes"]) == 1 + assert data_payload["answer_codes"][0]["answer_id"] == "name-answer" + assert data_payload["answer_codes"][0]["code"] == "2" + + +def test_all_answers_codes_for_answer_options_in_payload_when_one_is_answered(): + questionnaire_store = get_questionnaire_store() + + full_routing_path = [ + RoutingPath(block_ids=["mandatory-checkbox"], section_id="default-section") + ] + + questionnaire_store.data_stores.answer_store = AnswerStore( + [ + Answer("mandatory-checkbox-answer", ["Ham"]).to_dict(), + ] + ) + + schema = load_schema_from_name("test_answer_codes") + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, + full_routing_path, + ) + + # Then + assert len(data_payload["answer_codes"]) == 5 + assert all( + answer_code["answer_id"] == "mandatory-checkbox-answer" + for answer_code in data_payload["answer_codes"] + ) + + +def test_no_answers_codes_in_payload_when_no_questions_answered(): + questionnaire_store = get_questionnaire_store() + + full_routing_path = [ + RoutingPath(block_ids=["mandatory-checkbox"], section_id="default-section") + ] + + questionnaire_store.data_stores.answer_store = AnswerStore() + + schema = load_schema_from_name("test_answer_codes") + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, + full_routing_path, + ) + + # Then + assert "answer_codes" not in data_payload + + +def test_payload_dynamic_answers(): + questionnaire_store = get_questionnaire_store() + + full_routing_path = [ + RoutingPath( + block_ids=["any-supermarket", "list-collector", "dynamic-answer"], + section_id="section", + ) + ] + + questionnaire_store.data_stores.answer_store = AnswerStore( + [ + Answer("any-supermarket-answer", "Yes", None).to_dict(), + Answer("supermarket-name", "Tesco", "tUJzGV").to_dict(), + Answer("supermarket-name", "Aldi", "vhECeh").to_dict(), + Answer("list-collector-answer", "No", None).to_dict(), + Answer("percentage-of-shopping", 12, "tUJzGV").to_dict(), + Answer("percentage-of-shopping", 21, "vhECeh").to_dict(), + ] + ) + + questionnaire_store.data_stores.list_store = ListStore( + [{"items": ["tUJzGV", "vhECeh"], "name": "supermarkets"}] + ) + + schema = load_schema_from_name("test_dynamic_answers_list_source") + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, + full_routing_path, + ) + + # Then + assert ( + Answer(answer_id="percentage-of-shopping", value=12, list_item_id="tUJzGV") + in data_payload["answers"] + ) + assert ( + Answer(answer_id="percentage-of-shopping", value=21, list_item_id="vhECeh") + in data_payload["answers"] + ) + + +def test_repeating_block_answers_present( + repeating_blocks_answer_store, repeating_blocks_list_store +): + questionnaire_store = get_questionnaire_store() + + full_routing_path = [ + RoutingPath( + block_ids=[ + "responsible-party", + "any-companies-or-branches", + "any-other-companies-or-branches", + "any-other-trading-details", + ], + section_id="section-companies", + ) + ] + + questionnaire_store.data_stores.answer_store = repeating_blocks_answer_store + questionnaire_store.data_stores.list_store = repeating_blocks_list_store + + schema = load_schema_from_name( + "test_list_collector_repeating_blocks_section_summary" + ) + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, + full_routing_path, + ) + + expected_answer_codes = [ + {"answer_id": "responsible-party-answer", "code": "1"}, + {"answer_id": "any-companies-or-branches-answer", "code": "2"}, + {"answer_id": "company-or-branch-name", "code": "2a"}, + {"answer_id": "registration-number", "code": "2b"}, + {"answer_id": "registration-date", "code": "2c"}, + {"answer_id": "authorised-trader-uk-radio", "code": "2d"}, + {"answer_id": "authorised-trader-eu-radio", "code": "2e"}, + ] + + expected_answers = [ + {"answer_id": "responsible-party-answer", "value": "Yes"}, + {"answer_id": "any-companies-or-branches-answer", "value": "Yes"}, + { + "answer_id": "company-or-branch-name", + "value": "CompanyA", + "list_item_id": "PlwgoG", + }, + { + "answer_id": "registration-number", + "value": "123", + "list_item_id": "PlwgoG", + }, + { + "answer_id": "registration-date", + "value": "2023-01-01", + "list_item_id": "PlwgoG", + }, + { + "answer_id": "authorised-trader-uk-radio", + "value": "Yes", + "list_item_id": "PlwgoG", + }, + { + "answer_id": "authorised-trader-eu-radio", + "value": "Yes", + "list_item_id": "PlwgoG", + }, + { + "answer_id": "company-or-branch-name", + "value": "CompanyB", + "list_item_id": "UHPLbX", + }, + { + "answer_id": "registration-number", + "value": "456", + "list_item_id": "UHPLbX", + }, + { + "answer_id": "registration-date", + "value": "2023-01-01", + "list_item_id": "UHPLbX", + }, + { + "answer_id": "authorised-trader-uk-radio", + "value": "No", + "list_item_id": "UHPLbX", + }, + { + "answer_id": "authorised-trader-eu-radio", + "value": "No", + "list_item_id": "UHPLbX", + }, + ] + + answers_dict = json_loads(json_dumps(data_payload["answers"])) + answer_codes_dict = json_loads(json_dumps(data_payload["answer_codes"])) + + assert answers_dict == expected_answers + assert answer_codes_dict == expected_answer_codes + + +def test_payload_supplementary_data(): + questionnaire_store = get_questionnaire_store() + + full_routing_path = [ + RoutingPath( + block_ids=["dynamic-answer"], + section_id="section", + ) + ] + + supplementary_data = { + "schema_version": "v1", + "identifier": "12345678901", + "note": {"title": "supermarket test survey", "description": "test data"}, + "items": { + "supermarkets": [ + {"identifier": "123", "name": "Tesco"}, + {"identifier": "456", "name": "Aldi"}, + ] + }, + } + supermarkets_list_mappings = [ + {"identifier": "123", "list_item_id": "tUJzGV"}, + {"identifier": "456", "list_item_id": "vhECeh"}, + ] + + list_item_ids = ["tUJzGV", "vhECeh"] + questionnaire_store.data_stores.supplementary_data_store = SupplementaryDataStore( + supplementary_data=supplementary_data, + list_mappings={"supermarkets": supermarkets_list_mappings}, + ) + questionnaire_store.data_stores.list_store = ListStore( + [{"items": list_item_ids, "name": "supermarkets"}] + ) + questionnaire_store.data_stores.answer_store = AnswerStore( + [ + Answer("percentage-of-shopping", 12, list_item_ids[0]).to_dict(), + Answer("percentage-of-shopping", 21, list_item_ids[1]).to_dict(), + ] + ) + + schema = load_schema_from_name("test_supplementary_data") + + data_payload = get_payload_data( + questionnaire_store.data_stores, + schema, + full_routing_path, + ) + + assert "supplementary_data" in data_payload + assert "lists" in data_payload + assert data_payload["supplementary_data"] == make_immutable(supplementary_data) + assert len(data_payload["lists"]) == 1 + assert data_payload["lists"][0] == { + "items": list_item_ids, + "name": "supermarkets", + "supplementary_data_mappings": make_immutable(supermarkets_list_mappings), + } diff --git a/tests/app/submitter/test_converter.py b/tests/app/submitter/test_converter.py index 76bcb237ad..5c527a4e29 100644 --- a/tests/app/submitter/test_converter.py +++ b/tests/app/submitter/test_converter.py @@ -3,53 +3,33 @@ import pytest from app.questionnaire.questionnaire_schema import QuestionnaireSchema -from app.submitter.converter import DataVersionError, convert_answers +from app.submitter.converter_v2 import ( + DataVersionError, + NoMetadataException, + convert_answers_v2, +) +from tests.app.questionnaire.conftest import get_metadata +from tests.app.submitter.conftest import get_questionnaire_store SUBMITTED_AT = datetime.now(timezone.utc) -def test_convert_answers_flushed_flag_default_is_false( - fake_questionnaire_schema, fake_questionnaire_store -): - answer_object = convert_answers( - fake_questionnaire_schema, fake_questionnaire_store, {}, SUBMITTED_AT - ) - - assert not answer_object["flushed"] - - -def test_ref_period_end_date_is_not_in_output( - fake_questionnaire_schema, fake_questionnaire_store -): - fake_questionnaire_store.metadata["ref_p_end_date"] = None - answer_object = convert_answers( - fake_questionnaire_schema, fake_questionnaire_store, {}, SUBMITTED_AT - ) - assert "ref_period_end_date" not in answer_object["metadata"] +def test_convert_answers_v2_flushed_flag_default_is_false(fake_questionnaire_schema): + questionnaire_store = get_questionnaire_store() - del fake_questionnaire_store.metadata["ref_p_end_date"] - answer_object = convert_answers( - fake_questionnaire_schema, fake_questionnaire_store, {}, SUBMITTED_AT + answer_object = convert_answers_v2( + fake_questionnaire_schema, questionnaire_store, {}, SUBMITTED_AT ) - assert "ref_period_end_date" not in answer_object["metadata"] + assert not answer_object["flushed"] -def test_ref_period_start_and_end_date_is_in_output( - fake_questionnaire_schema, fake_questionnaire_store -): - answer_object = convert_answers( - fake_questionnaire_schema, fake_questionnaire_store, {}, SUBMITTED_AT - ) - assert answer_object["metadata"]["ref_period_start_date"] == "2016-02-02" - assert answer_object["metadata"]["ref_period_end_date"] == "2016-03-03" +def test_convert_answers_v2_flushed_flag_overriden_to_true(fake_questionnaire_schema): + questionnaire_store = get_questionnaire_store() -def test_convert_answers_flushed_flag_overriden_to_true( - fake_questionnaire_schema, fake_questionnaire_store -): - answer_object = convert_answers( + answer_object = convert_answers_v2( fake_questionnaire_schema, - fake_questionnaire_store, + questionnaire_store, {}, SUBMITTED_AT, flushed=True, @@ -59,109 +39,88 @@ def test_convert_answers_flushed_flag_overriden_to_true( def test_started_at_should_be_set_in_payload_if_present_in_response_metadata( - fake_questionnaire_schema, fake_questionnaire_store + fake_questionnaire_schema, ): - answer_object = convert_answers( - fake_questionnaire_schema, fake_questionnaire_store, {}, SUBMITTED_AT + questionnaire_store = get_questionnaire_store() + + answer_object = convert_answers_v2( + fake_questionnaire_schema, questionnaire_store, {}, SUBMITTED_AT ) assert ( answer_object["started_at"] - == fake_questionnaire_store.response_metadata["started_at"] + == questionnaire_store.data_stores.response_metadata["started_at"] ) def test_started_at_should_not_be_set_in_payload_if_absent_in_response_metadata( - fake_questionnaire_schema, - fake_questionnaire_store, - fake_response_metadata, - fake_metadata, + fake_questionnaire_schema, fake_response_metadata ): del fake_response_metadata["started_at"] + questionnaire_store = get_questionnaire_store() + questionnaire_store.data_stores.response_metadata = fake_response_metadata - fake_questionnaire_store.set_metadata(fake_metadata) - fake_questionnaire_store.response_metadata = fake_response_metadata - - answer_object = convert_answers( - fake_questionnaire_schema, fake_questionnaire_store, {}, SUBMITTED_AT + answer_object = convert_answers_v2( + fake_questionnaire_schema, questionnaire_store, {}, SUBMITTED_AT ) assert "started_at" not in answer_object -def test_submitted_at_should_be_set_in_payload( - fake_questionnaire_schema, fake_questionnaire_store -): - answer_object = convert_answers( - fake_questionnaire_schema, fake_questionnaire_store, {}, SUBMITTED_AT +def test_submitted_at_should_be_set_in_payload(fake_questionnaire_schema): + questionnaire_store = get_questionnaire_store() + + answer_object = convert_answers_v2( + fake_questionnaire_schema, questionnaire_store, {}, SUBMITTED_AT ) assert SUBMITTED_AT.isoformat() == answer_object["submitted_at"] -def test_case_id_should_be_set_in_payload( - fake_questionnaire_schema, fake_questionnaire_store -): - answer_object = convert_answers( - fake_questionnaire_schema, fake_questionnaire_store, {}, SUBMITTED_AT - ) - - assert answer_object["case_id"] == fake_questionnaire_store.metadata["case_id"] - +def test_case_id_should_be_set_in_payload(fake_questionnaire_schema): + questionnaire_store = get_questionnaire_store() -def test_case_ref_should_be_set_in_payload( - fake_questionnaire_schema, fake_questionnaire_store -): - answer_object = convert_answers( - fake_questionnaire_schema, fake_questionnaire_store, {}, SUBMITTED_AT + answer_object = convert_answers_v2( + fake_questionnaire_schema, questionnaire_store, {}, SUBMITTED_AT ) - assert answer_object["case_ref"], fake_questionnaire_store.metadata["case_ref"] - + assert answer_object["case_id"] == questionnaire_store.data_stores.metadata.case_id -def test_display_address_should_be_set_in_payload_metadata( - fake_questionnaire_schema, fake_questionnaire_store -): - payload = convert_answers( - fake_questionnaire_schema, fake_questionnaire_store, {}, SUBMITTED_AT - ) - - assert payload["metadata"]["display_address"], fake_questionnaire_store.metadata[ - "display_address" - ] +def test_case_ref_should_be_set_in_payload(fake_questionnaire_schema): + questionnaire_store = get_questionnaire_store() -def test_instrument_id_is_not_in_payload_collection_if_form_type_absent_in_metadata( - fake_questionnaire_schema, fake_questionnaire_store, fake_metadata -): - del fake_metadata["form_type"] - fake_questionnaire_store.set_metadata(fake_metadata) - payload = convert_answers( - fake_questionnaire_schema, fake_questionnaire_store, {}, SUBMITTED_AT + answer_object = convert_answers_v2( + fake_questionnaire_schema, questionnaire_store, {}, SUBMITTED_AT ) - assert "instrument_id" not in payload["collection"] + assert answer_object["survey_metadata"][ + "case_ref" + ], questionnaire_store.data_stores.metadata["survey_metadata"]["data"]["case_ref"] -def test_instrument_id_should_be_set_in_payload_collection_if_form_type_in_metadata( - fake_questionnaire_schema, fake_questionnaire_store -): - payload = convert_answers( - fake_questionnaire_schema, fake_questionnaire_store, {}, SUBMITTED_AT +def test_display_address_should_be_set_in_payload_metadata(fake_questionnaire_schema): + questionnaire_store = get_questionnaire_store() + + payload = convert_answers_v2( + fake_questionnaire_schema, questionnaire_store, {}, SUBMITTED_AT ) - assert payload["collection"]["instrument_id"], "I" + assert payload["survey_metadata"][ + "display_address" + ], questionnaire_store.data_stores.metadata["survey_metadata"]["data"][ + "display_address" + ] -def test_converter_raises_runtime_error_for_unsupported_version( - fake_questionnaire_store, -): +def test_converter_raises_runtime_error_for_unsupported_version(): + questionnaire_store = get_questionnaire_store() questionnaire = {"survey_id": "021", "data_version": "-0.0.1"} with pytest.raises(DataVersionError) as err: - convert_answers( + convert_answers_v2( QuestionnaireSchema(questionnaire), - fake_questionnaire_store, + questionnaire_store, {}, SUBMITTED_AT, ) @@ -170,36 +129,88 @@ def test_converter_raises_runtime_error_for_unsupported_version( def test_converter_language_code_not_set_in_payload( - fake_questionnaire_schema, - fake_questionnaire_store, - fake_response_metadata, - fake_metadata, + fake_questionnaire_schema, fake_response_metadata ): - fake_questionnaire_store.set_metadata(fake_metadata) - fake_questionnaire_store.response_metadata = fake_response_metadata + questionnaire_store = get_questionnaire_store() + questionnaire_store.data_stores.response_metadata = fake_response_metadata - answer_object = convert_answers( - fake_questionnaire_schema, fake_questionnaire_store, {}, SUBMITTED_AT + answer_object = convert_answers_v2( + fake_questionnaire_schema, questionnaire_store, {}, SUBMITTED_AT ) - with pytest.raises(KeyError): - assert fake_questionnaire_store.metadata["language_code"] + assert questionnaire_store.data_stores.metadata["language_code"] is None assert answer_object["launch_language_code"] == "en" def test_converter_language_code_set_in_payload( - fake_questionnaire_schema, - fake_questionnaire_store, - fake_response_metadata, - fake_metadata, + fake_questionnaire_schema, fake_response_metadata ): - fake_metadata["language_code"] = "ga" - fake_questionnaire_store.set_metadata(fake_metadata) - fake_questionnaire_store.response_metadata = fake_response_metadata + questionnaire_store = get_questionnaire_store() + questionnaire_store.data_stores.metadata = get_metadata( + extra_metadata={"language_code": "ga"} + ) + questionnaire_store.data_stores.response_metadata = fake_response_metadata - answer_object = convert_answers( - fake_questionnaire_schema, fake_questionnaire_store, {}, SUBMITTED_AT + answer_object = convert_answers_v2( + fake_questionnaire_schema, questionnaire_store, {}, SUBMITTED_AT ) assert answer_object["launch_language_code"] == "ga" + + +def test_no_metadata_raises_exception(fake_questionnaire_schema): + questionnaire_store = get_questionnaire_store() + + questionnaire_store.data_stores.metadata = None + + with pytest.raises(NoMetadataException): + convert_answers_v2( + fake_questionnaire_schema, questionnaire_store, {}, SUBMITTED_AT + ) + + +def test_data_object_set_in_payload(fake_questionnaire_schema, fake_response_metadata): + questionnaire_store = get_questionnaire_store() + questionnaire_store.data_stores.response_metadata = fake_response_metadata + + answer_object = convert_answers_v2( + fake_questionnaire_schema, questionnaire_store, {}, SUBMITTED_AT + ) + + assert "data" in answer_object + + +def test_schema_url_in_metadata_should_be_in_payload( + fake_metadata_v2_schema_url, fake_questionnaire_schema +): + questionnaire_store = get_questionnaire_store() + questionnaire_store.data_stores.metadata = fake_metadata_v2_schema_url + + payload = convert_answers_v2( + fake_questionnaire_schema, questionnaire_store, {}, SUBMITTED_AT + ) + + assert "schema_url" in payload + assert "schema_name" not in payload + assert "cir_instrument_id" not in payload + assert payload["schema_url"] == fake_metadata_v2_schema_url["schema_url"] + + +def test_cir_instrument_id_in_metadata_should_be_in_payload( + fake_metadata_v2_cir_instrument_id, fake_questionnaire_schema +): + questionnaire_store = get_questionnaire_store() + questionnaire_store.data_stores.metadata = fake_metadata_v2_cir_instrument_id + + payload = convert_answers_v2( + fake_questionnaire_schema, questionnaire_store, {}, SUBMITTED_AT + ) + + assert "schema_url" not in payload + assert "schema_name" not in payload + assert "cir_instrument_id" in payload + assert ( + payload["cir_instrument_id"] + == fake_metadata_v2_cir_instrument_id["cir_instrument_id"] + ) diff --git a/tests/app/submitter/test_submitter.py b/tests/app/submitter/test_submitter.py index f6c857b7ce..4e1feb4aa5 100644 --- a/tests/app/submitter/test_submitter.py +++ b/tests/app/submitter/test_submitter.py @@ -1,6 +1,8 @@ import uuid import pytest +from google.api_core.exceptions import Forbidden +from google.cloud.storage import Blob from pika.exceptions import AMQPError, NackError from app.submitter import GCSFeedbackSubmitter, GCSSubmitter, RabbitMQSubmitter @@ -212,6 +214,47 @@ def test_gcs_submitter_adds_metadata_when_sends_message(patch_gcs_client): } +def test_gcs_submitter_adds_additional_keys_to_metadata_when_set(patch_gcs_client): + gcs_submitter = GCSSubmitter(bucket_name="test_bucket") + + # When + gcs_submitter.send_message( + message={"test_data"}, tx_id="123", case_id="456", **{"qid": "1"} + ) + + # Then + bucket = patch_gcs_client.return_value.get_bucket.return_value + blob = bucket.blob.return_value + + assert blob.metadata == { + "tx_id": "123", + "case_id": "456", + "qid": "1", + } + + +def test_gcs_feedback_submitter_adds_additional_keys_to_metadata_when_set( + patch_gcs_client, +): + gcs_submitter = GCSFeedbackSubmitter(bucket_name="test_bucket") + + # When + gcs_submitter.upload( + payload=json_dumps({"some-data": "some-value"}), + metadata={"tx_id": "123", "case_id": "456", "qid": "1"}, + ) + + # Then + bucket = patch_gcs_client.return_value.get_bucket.return_value + blob = bucket.blob.return_value + + assert blob.metadata == { + "tx_id": "123", + "case_id": "456", + "qid": "1", + } + + @pytest.mark.parametrize( "submitter, entrypoint, data_to_upload", [ @@ -295,3 +338,56 @@ def test_gcs_feedback_submitter_uploads_feedback(patch_gcs_client): b'"form_type": "H", "language_code": "cy", "region_code": "GB-ENG", "tx_id": "12345"}' ) assert feedback_upload is True + + +def test_double_submission_passes_when_delete_operation_error( + patch_gcs_client, gcs_blob_delete_forbidden +): # pylint: disable=redefined-outer-name + # Given + gcs_submitter = GCSSubmitter(bucket_name="test_bucket") + + # When + bucket = patch_gcs_client.return_value.get_bucket.return_value + bucket.blob.return_value = gcs_blob_delete_forbidden + published = gcs_submitter.send_message( + message={"test_data"}, tx_id="123", case_id="456" + ) + # Then + assert published + + +def test_double_submission_is_forbidden_when_not_delete_operation_error( + patch_gcs_client, gcs_blob_create_forbidden +): # pylint: disable=redefined-outer-name + # Given + gcs_submitter = GCSSubmitter(bucket_name="test_bucket") + + # When + bucket = patch_gcs_client.return_value.get_bucket.return_value + bucket.blob.return_value = gcs_blob_create_forbidden + + # Then + with pytest.raises(Forbidden): + gcs_submitter.send_message(message={"test_data"}, tx_id="123", case_id="456") + + +@pytest.fixture +def gcs_blob_create_forbidden(mocker): + blob = Blob(name="test-blob", bucket=mocker.Mock()) + + blob.upload_from_string = mocker.Mock( + side_effect=Forbidden("storage.objects.create") + ) + + return blob + + +@pytest.fixture +def gcs_blob_delete_forbidden(mocker): + blob = Blob(name="test-blob", bucket=mocker.Mock()) + + blob.upload_from_string = mocker.Mock( + side_effect=Forbidden("storage.objects.delete") + ) + + return blob diff --git a/tests/app/test_jinja_filters.py b/tests/app/test_jinja_filters.py index 291c46f233..c2c2b86d29 100644 --- a/tests/app/test_jinja_filters.py +++ b/tests/app/test_jinja_filters.py @@ -1,9 +1,12 @@ # coding: utf-8 +# pylint: disable=too-many-lines +import unicodedata from datetime import datetime, timezone +from decimal import Decimal import pytest import simplejson as json -from jinja2 import Undefined +from flask import g from mock import Mock from app.jinja_filters import ( @@ -17,13 +20,168 @@ format_unit_input_label, get_currency_symbol, get_formatted_address, - get_formatted_currency, get_width_for_number, map_list_collector_config, + map_list_config, map_summary_item_config, should_wrap_with_fieldset, strip_tags, ) +from app.questionnaire.questionnaire_schema import QuestionnaireSchema +from app.utilities.decimal_places import get_formatted_currency +from app.utilities.schema import load_schema_from_name + +TEST_FORMAT_CURRENCY_PARAMS = ( + "value, currency, locale_string, decimal_limit, expected_result", + [ + # This test case assumes that the number of decimal places entered by the + # user has already been validated before reaching the get_formatted_currency method. + # When there is no decimal limit set, providing the user has entered a decimal, then the currency local + # currency precision will take precedence if the number of decimal places entered by the user is less than the + # currency precision value. If the number of decimal places entered by the user is greater than the currency + # precision value then we will display the number of decimal places as entered by the user + # The Jordanian Dinar is used as an example in this test case as the currency precision is set to .000 + (Decimal("2"), "GBP", "en_GB", None, "ÂŖ2"), + (Decimal("2"), "JOD", "en_GB", None, "JOD2"), + (Decimal("2.1"), "GBP", "en_GB", None, "ÂŖ2.10"), + (Decimal("2.1"), "JOD", "en_GB", None, "JOD2.100"), + (Decimal("2"), "GBP", "en_GB", 1, "ÂŖ2"), + (Decimal("2"), "GBP", "en_GB", 2, "ÂŖ2.00"), + (Decimal("2"), "GBP", "en_GB", 0, "ÂŖ2"), + (Decimal("2"), "GBP", "en_GB", 2, "ÂŖ2.00"), + (Decimal("2"), "GBP", "en_GB", 6, "ÂŖ2.00"), + (Decimal("2.1"), "GBP", "en_GB", 0, "ÂŖ2.10"), + (Decimal("2.12"), "GBP", "en_GB", None, "ÂŖ2.12"), + (Decimal("2.12"), "JOD", "en_GB", None, "JOD2.120"), + (Decimal("2.12"), "GBP", "en_GB", 0, "ÂŖ2.12"), + (Decimal("2.123"), "GBP", "en_GB", None, "ÂŖ2.123"), + (Decimal("2.123"), "JOD", "en_GB", None, "JOD2.123"), + (Decimal("123.1234"), "GBP", "en_GB", 0, "ÂŖ123.1234"), + (Decimal("3000.44545"), "GBP", "en_GB", None, "ÂŖ3,000.44545"), + (Decimal("3000"), "GBP", "en_GB", 0, "ÂŖ3,000"), + (Decimal("3000"), "JPY", "en_GB", 0, "JPÂĨ3,000"), + (Decimal("3000"), "JPY", "ja_JP", 0, "ÂĨ3,000"), + (123, "GBP", "en_GB", 1, "ÂŖ123"), + (Decimal("2.1"), "GBP", "en_GB", 1, "ÂŖ2.1"), + (Decimal("123.4"), "HUF", "hu_HU", 1, "123,4 Ft"), + (11, "GBP", "en_GB", 2, "ÂŖ11.00"), + (11000, "USD", "en_GB", 2, "US$11,000.00"), + (11000, "USD", "en_GB", 2, "US$11,000.00"), + (11000, "PLN", "pl_PL", 2, "11 000,00 zł"), + (Decimal("2.1"), "GBP", "en_GB", 2, "ÂŖ2.10"), + (Decimal("11.99"), "GBP", "en_GB", 2, "ÂŖ11.99"), + (Decimal("2.1"), "GBP", "en_GB", 4, "ÂŖ2.10"), + (Decimal("2.1"), "GBP", "en_GB", 5, "ÂŖ2.10"), + (2, "GBP", "en_GB", 6, "ÂŖ2.00"), + (Decimal("1.1"), "GBP", "en_GB", 6, "ÂŖ1.10"), + (Decimal("1.10"), "GBP", "en_GB", 6, "ÂŖ1.10"), + (Decimal("1.100"), "GBP", "en_GB", 6, "ÂŖ1.100"), + (Decimal("1.1000"), "GBP", "en_GB", 6, "ÂŖ1.1000"), + (Decimal("2.14564"), "GBP", "en_GB", 6, "ÂŖ2.14564"), + (Decimal("3000.445"), "GBP", "en_GB", 6, "ÂŖ3,000.445"), + ], +) + +TEST_FORMAT_NUMBER_PARAMS = ( + "value, locale_string, expected_result", + [ + (123, "en_GB", "123"), + (123, "es_ES", "123"), + (Decimal("123.4"), "en_GB", "123.4"), + (Decimal("123.4"), "es_US", "123.4"), + (Decimal("123.40"), "en_GB", "123.40"), + (Decimal("123.400"), "en_GB", "123.400"), + (Decimal("123.4000"), "en_GB", "123.4000"), + (Decimal("123.4000"), "pl_PL", "123,4000"), + (Decimal("123.40000"), "en_GB", "123.40000"), + (Decimal("123.40000"), "ja_JP", "123.40000"), + (Decimal("123434.7678"), "en_GB", "123,434.7678"), + (Decimal("123434.7678"), "hu_HU", "123 434,7678"), + (Decimal("123.45678"), "en_GB", "123.45678"), + (Decimal("2344.6533"), "en_GB", "2,344.6533"), + (1000, "en_GB", "1,000"), + (1000, "en_US", "1,000"), + (10000, "en_GB", "10,000"), + (10000, "es_ES", "10.000"), + (100000000, "en_GB", "100,000,000"), + (0, "en_GB", "0"), + (0, "de_DE", "0"), + (Decimal("0.00"), "en_GB", "0.00"), + (Decimal("0.000"), "en_GB", "0.000"), + (Decimal("0.000"), "es_ES", "0,000"), + (Decimal("0.00000"), "en_GB", "0.00000"), + (Decimal("0.00000"), "es_ES", "0,00000"), + ], +) + +TEST_FORMAT_UNIT_PARAMS = ( + "value, measurement_unit, locale_string, length, expected_result", + [ + (123, "mile", "en_GB", "short", "123 mi"), + ( + Decimal("0.123"), + "millimeter", + "en_GB", + "short", + "0.123 mm", + ), + ( + Decimal("0.123"), + "millimeter", + "es_ES", + "short", + "0,123 mm", + ), + (123, "centimeter", "en_GB", "short", "123 cm"), + (123, "centimeter", "pl_PL", "short", "123 cm"), + (123, "kilometer", "en_GB", "long", "123 kilometres"), + (3, "kilometer", "en_GB", "long", "3 kilometres"), + (3, "kilometer", "hu_HU", "long", "3 kilomÊter"), + (Decimal("1.2"), "kilometer", "en_GB", "long", "1.2 kilometres"), + (Decimal("1.20"), "kilometer", "en_GB", "long", "1.20 kilometres"), + (Decimal("1.200"), "kilometer", "en_GB", "long", "1.200 kilometres"), + (Decimal("1.2000"), "kilometer", "en_GB", "long", "1.2000 kilometres"), + (Decimal("1.20000"), "kilometer", "en_GB", "long", "1.20000 kilometres"), + (Decimal("1.20000"), "kilometer", "de_DE", "long", "1,20000 Kilometer"), + (Decimal("1.2"), "kilometer", "pl_PL", "long", "1,2 kilometra"), + (Decimal("1.2345"), "kilometer", "en_GB", "long", "1.2345 kilometres"), + (Decimal("1.2345"), "kilometer", "es_ES", "long", "1,2345 kilÃŗmetros"), + (123, "mile", "en_GB", "short", "123 mi"), + (123, "mile", "en_GB", "narrow", "123mi"), + (123, "mile", "en_US", "narrow", "123mi"), + (Decimal("0.123"), "millimeter", "en_GB", "short", "0.123 mm"), + (123, "centimeter", "en_GB", "short", "123 cm"), + (100, "length-meter", "en_GB", "short", "100 m"), + (100, "length-centimeter", "en_GB", "short", "100 cm"), + (100, "area-square-meter", "en_GB", "short", "100 m²"), + (100, "area-square-centimeter", "en_GB", "short", "100 cm²"), + (100, "area-square-kilometer", "en_GB", "short", "100 km²"), + (100, "area-square-mile", "en_GB", "short", "100 sq mi"), + (100, "area-hectare", "en_GB", "short", "100 ha"), + (100, "area-acre", "en_GB", "short", "100 ac"), + (100, "volume-cubic-meter", "en_GB", "short", "100 mÂŗ"), + (100, "volume-cubic-centimeter", "en_GB", "short", "100 cmÂŗ"), + (100, "volume-liter", "en_GB", "short", "100 l"), + (100, "volume-hectoliter", "en_GB", "short", "100 hl"), + (100, "volume-megaliter", "en_GB", "short", "100 Ml"), + (100, "duration-hour", "en_GB", "short", "100 hrs"), + (100, "duration-hour", "en_GB", "long", "100 hours"), + (100, "duration-year", "en_GB", "long", "100 years"), + (100, "mass-tonne", "en_GB", "long", "100 tonnes"), + (1, "mass-tonne", "en_GB", "long", "1 tonne"), + (100, "mass-tonne", "en_GB", "short", "100 t"), + ], +) + +TEST_FORMAT_UNIT_LANGUAGE_PARAMS = ( + "unit, value, length, formatted_unit, language", + [ + ("duration-hour", 100, "short", "100 awr", "cy"), + ("duration-year", 100, "short", "100 bl", "cy"), + ("duration-hour", 100, "long", "100 awr", "cy"), + ("duration-year", 100, "long", "100 mlynedd", "cy"), + ], +) @pytest.mark.parametrize( @@ -53,29 +211,33 @@ def test_get_currency_symbol(currency, symbol): assert get_currency_symbol(currency) == symbol -def test_get_formatted_currency_with_no_value(): - assert get_formatted_currency("") == "" +@pytest.mark.parametrize(*TEST_FORMAT_CURRENCY_PARAMS) +def test_get_custom_currency( + mocker, value, currency, locale_string, decimal_limit, expected_result, app +): + with app.app_context(): + mocker.patch( + "app.jinja_filters.flask_babel.get_locale", + mocker.Mock(return_value=locale_string), + ) + result = get_formatted_currency( + value=value, currency=currency, decimal_limit=decimal_limit + ) + assert unicodedata.normalize("NFKD", result) == expected_result -@pytest.mark.usefixtures("gb_locale") -@pytest.mark.parametrize( - "number, formatted_number", - ( - (123, "123"), - ("123.4", "123.4"), - ("123.40", "123.4"), - ("1000", "1,000"), - ("10000", "10,000"), - ("100000000", "100,000,000"), - (0, "0"), - (0.00, "0"), - ("", ""), - (None, ""), - (Undefined(), ""), - ), -) -def test_format_number(number, formatted_number): - assert format_number(number) == formatted_number + +@pytest.mark.parametrize(*TEST_FORMAT_NUMBER_PARAMS) +def test_format_number(mocker, value, locale_string, expected_result, app): + with app.app_context(): + mocker.patch( + "app.jinja_filters.flask_babel.get_locale", + mocker.Mock(return_value=locale_string), + ) + + result = format_number(value) + + assert unicodedata.normalize("NFKD", result) == expected_result def test_format_date_time_in_bst(mock_autoescape_context, app): @@ -128,34 +290,22 @@ def test_format_percentage(percentage, formatted_percentage): @pytest.mark.usefixtures("app") -@pytest.mark.parametrize( - "unit, value, length, formatted_unit, language", - ( - ("length-meter", 100, "short", "100 m", "en_GB"), - ("length-centimeter", 100, "short", "100 cm", "en_GB"), - ("length-mile", 100, "short", "100 mi", "en_GB"), - ("length-kilometer", 100, "short", "100 km", "en_GB"), - ("area-square-meter", 100, "short", "100 m²", "en_GB"), - ("area-square-centimeter", 100, "short", "100 cm²", "en_GB"), - ("area-square-kilometer", 100, "short", "100 km²", "en_GB"), - ("area-square-mile", 100, "short", "100 sq mi", "en_GB"), - ("area-hectare", 100, "short", "100 ha", "en_GB"), - ("area-acre", 100, "short", "100 ac", "en_GB"), - ("volume-cubic-meter", 100, "short", "100 mÂŗ", "en_GB"), - ("volume-cubic-centimeter", 100, "short", "100 cmÂŗ", "en_GB"), - ("volume-liter", 100, "short", "100 l", "en_GB"), - ("volume-hectoliter", 100, "short", "100 hl", "en_GB"), - ("volume-megaliter", 100, "short", "100 Ml", "en_GB"), - ("duration-hour", 100, "short", "100 hrs", "en_GB"), - ("duration-hour", 100, "long", "100 hours", "en_GB"), - ("duration-year", 100, "long", "100 years", "en_GB"), - ("duration-hour", 100, "short", "100 awr", "cy"), - ("duration-year", 100, "short", "100 bl", "cy"), - ("duration-hour", 100, "long", "100 awr", "cy"), - ("duration-year", 100, "long", "100 mlynedd", "cy"), - ), -) -def test_format_unit(unit, value, length, formatted_unit, language, mocker): +@pytest.mark.parametrize(*TEST_FORMAT_UNIT_PARAMS) +def test_format_unit( + value, measurement_unit, locale_string, length, expected_result, mocker +): + mocker.patch( + "app.jinja_filters.flask_babel.get_locale", + mocker.Mock(return_value=locale_string), + ) + assert format_unit(measurement_unit, value, length=length) == expected_result + + +@pytest.mark.usefixtures("app") +@pytest.mark.parametrize(*TEST_FORMAT_UNIT_LANGUAGE_PARAMS) +def test_format_unit_non_gb_locale( + unit, value, length, formatted_unit, language, mocker +): mocker.patch( "app.jinja_filters.flask_babel.get_locale", mocker.Mock(return_value=language) ) @@ -204,6 +354,8 @@ def test_format_unit(unit, value, length, formatted_unit, language, mocker): ("duration-hour", "long", "awr", "cy"), ("duration-year", "short", "bl", "cy"), ("duration-year", "long", "flynedd", "cy"), + ("mass-tonne", "long", "tonnes", "en_GB"), + ("mass-tonne", "short", "t", "en_GB"), ), ) def test_format_unit_input_label(unit, length, formatted_unit, language, mocker): @@ -237,20 +389,56 @@ def test_format_duration(duration, formatted_duration, app): @pytest.mark.parametrize( "answer, width", ( - ({}, 10), + ({}, 15), ({"maximum": {"value": 1}}, 1), ({"maximum": {"value": 123456}}, 6), ({"maximum": {"value": 12345678901}}, 15), ({"minimum": {"value": -123456}, "maximum": {"value": 1234}}, 7), - ({"decimal_places": 2, "maximum": {"value": 123456}}, 8), + ({"decimal_places": 6, "maximum": {"value": 123456}}, 15), + ({"maximum": {"value": 999_999_999_999_999}}, 15), + ({"decimal_places": 5, "maximum": {"value": 999_999_999_999_999}}, 20), + ({"decimal_places": 6, "maximum": {"value": 999_999_999_999_999}}, 30), + ({"minimum": {"value": -99_999_999_999_999}}, 15), + ({"decimal_places": 5, "minimum": {"value": -99_999_999_999_999}}, 20), + ({"decimal_places": 6, "minimum": {"value": -99_999_999_999_999}}, 30), ( {"maximum": {"value": 123456789012345678901123456789012345678901234567890}}, None, ), ), ) -def test_get_width_for_number(answer, width): - assert get_width_for_number(answer) == width +def test_get_width_for_number(answer, width, app): + with app.app_context(): + schema = QuestionnaireSchema({}) + g.schema = schema + assert get_width_for_number(answer) == width + + +@pytest.mark.parametrize( + "answer, width", + ( + ("set-minimum", 7), + ("set-maximum", 7), + ("test-range", 7), + ("test-range-exclusive", 7), + ("test-min", 15), + ("test-max", 4), + ("test-min-exclusive", 15), + ("test-max-exclusive", 4), + ("test-percent", 3), + ("test-decimal", 10), + ("other-answer", 5), + ("first-number-answer", 6), + ("second-number-answer", 6), + ("detail-answer", 15), + ), +) +def test_get_width_for_number_recursive(answer, width, app): + with app.test_request_context(): + schema = load_schema_from_name("test_numbers") + g.schema = schema + answer_to_test = schema.get_answers_by_answer_id(answer)[0] + assert get_width_for_number(answer_to_test) == width @pytest.mark.parametrize( @@ -300,6 +488,34 @@ def test_get_width_for_number(answer, width): }, True, ), + ( + { + "type": "General", + "answers": [ + { + "type": "Currency", + "minimum": { + "value": {"identifier": "set-minimum", "source": "answers"} + }, + } + ], + }, + False, + ), + ( + { + "type": "General", + "answers": [ + { + "type": "Currency", + "maximum": { + "value": {"identifier": "set-maximum", "source": "answers"} + }, + } + ], + }, + False, + ), ), ) def test_should_wrap_with_fieldset(question, expected): @@ -312,20 +528,21 @@ def test_map_list_collector_config_no_actions(): {"item_title": "Joe Bloggs", "list_item_id": "two"}, ] - output = map_list_collector_config(list_items, "icon") + output = map_list_collector_config(list_items, True, False) expected = [ { "rowItems": [ { "actions": [], - "iconType": "icon", - "rowTitle": "Mark Bloggs", + "iconVisuallyHiddenText": None, + "iconType": None, "id": "one", "rowTitleAttributes": { - "data-qa": "list-item-1-label", "data-list-item-id": "one", + "data-qa": "list-item-1-label", }, + "rowTitle": "Mark Bloggs", } ] }, @@ -333,13 +550,14 @@ def test_map_list_collector_config_no_actions(): "rowItems": [ { "actions": [], - "iconType": "icon", - "rowTitle": "Joe Bloggs", + "iconVisuallyHiddenText": None, + "iconType": None, "id": "two", "rowTitleAttributes": { - "data-qa": "list-item-2-label", "data-list-item-id": "two", + "data-qa": "list-item-2-label", }, + "rowTitle": "Joe Bloggs", } ] }, @@ -370,7 +588,8 @@ def test_map_list_collector_config(): output = map_list_collector_config( list_items, - "icon", + True, + False, "edit_link_text", "edit_link_aria_label", "remove_link_text", @@ -383,19 +602,20 @@ def test_map_list_collector_config(): { "actions": [ { - "ariaLabel": "edit_link_aria_label", + "visuallyHiddenText": "edit_link_aria_label", "attributes": {"data-qa": "list-item-change-1-link"}, "text": "edit_link_text", "url": "/primary/change", } ], - "iconType": "icon", - "rowTitle": "Mark Bloggs (You)", + "iconVisuallyHiddenText": None, + "iconType": None, "id": "primary", "rowTitleAttributes": { - "data-qa": "list-item-1-label", "data-list-item-id": "primary", + "data-qa": "list-item-1-label", }, + "rowTitle": "Mark Bloggs (You)", } ] }, @@ -404,25 +624,26 @@ def test_map_list_collector_config(): { "actions": [ { - "ariaLabel": "edit_link_aria_label", + "visuallyHiddenText": "edit_link_aria_label", "attributes": {"data-qa": "list-item-change-2-link"}, "text": "edit_link_text", "url": "/nonprimary/change", }, { - "ariaLabel": "remove_link_aria_label", + "visuallyHiddenText": "remove_link_aria_label", "attributes": {"data-qa": "list-item-remove-2-link"}, "text": "remove_link_text", "url": "/nonprimary/remove", }, ], - "iconType": "icon", - "rowTitle": "Joe Bloggs", + "iconType": None, + "iconVisuallyHiddenText": None, "id": "nonprimary", "rowTitleAttributes": { - "data-qa": "list-item-2-label", "data-list-item-id": "nonprimary", + "data-qa": "list-item-2-label", }, + "rowTitle": "Joe Bloggs", } ] }, @@ -431,6 +652,150 @@ def test_map_list_collector_config(): assert output == expected +@pytest.mark.usefixtures("gb_locale") +def test_map_list_collector_config_with_related_answers_and_answer_title(): + list_items = [ + { + "remove_link": "/nonprimary/remove", + "edit_link": "/nonprimary/change", + "primary_person": False, + "item_title": "Name of UK company or branch", + "id": "nonprimary", + "list_item_id": "VHoiow", + }, + ] + + output = map_list_collector_config( + list_items, + True, + False, + "edit_link_text", + "edit_link_aria_label", + "remove_link_text", + "remove_link_aria_label", + { + "VHoiow": [ + { + "id": "edit-company", + "title": None, + "number": None, + "question": { + "id": "add-question", + "type": "General", + "title": " ", + "number": None, + "answers": [ + { + "id": "registration-number", + "label": "Registration number", + "value": 123, + "type": "number", + "unit": None, + "unit_length": None, + "currency": None, + "link": "registration_edit_link_url", + }, + { + "id": "authorised-insurer-radio", + "label": "Is this UK company or branch an authorised insurer?", + "value": {"label": "Yes", "detail_answer_value": None}, + "type": "radio", + "unit": None, + "unit_length": None, + "currency": None, + "link": "authorised_edit_link_url", + }, + ], + }, + } + ] + }, + "Name of UK company or branch", + "", + ) + + expected = [ + { + "rowItems": [ + { + "actions": [ + { + "visuallyHiddenText": "edit_link_aria_label", + "attributes": {"data-qa": "list-item-change-1-link"}, + "text": "edit_link_text", + "url": "/nonprimary/change", + }, + { + "visuallyHiddenText": "remove_link_aria_label", + "attributes": {"data-qa": "list-item-remove-1-link"}, + "text": "remove_link_text", + "url": "/nonprimary/remove", + }, + ], + "iconVisuallyHiddenText": None, + "iconType": None, + "id": "VHoiow", + "rowTitle": "Name of UK company or branch", + "rowTitleAttributes": { + "data-list-item-id": "VHoiow", + "data-qa": "list-item-1-label", + }, + "valueList": [{"text": "Name of UK company or branch"}], + }, + { + "actions": [ + { + "visuallyHiddenText": "Change answer for Name of UK company or branch: Registration number", + "attributes": { + "data-ga": "click", + "data-ga-action": "Edit", + "data-ga-category": "Link", + "data-ga-label": "Edit", + "data-ga-page": "Summary", + "data-qa": "registration-number-edit", + }, + "text": "edit_link_text", + "url": "registration_edit_link_url", + } + ], + "attributes": {"data-qa": "registration-number"}, + "id": "registration-number", + "rowTitle": "Registration number", + "rowTitleAttributes": {"data-qa": "registration-number-label"}, + "valueList": [{"text": "123"}], + }, + { + "actions": [ + { + "visuallyHiddenText": "Change answer for Name of UK company or branch: Is this UK " + "company or branch an authorised " + "insurer?", + "attributes": { + "data-ga": "click", + "data-ga-action": "Edit", + "data-ga-category": "Link", + "data-ga-label": "Edit", + "data-ga-page": "Summary", + "data-qa": "authorised-insurer-radio-edit", + }, + "text": "edit_link_text", + "url": "authorised_edit_link_url", + } + ], + "attributes": {"data-qa": "authorised-insurer-radio"}, + "id": "authorised-insurer-radio", + "rowTitle": "Is this UK company or branch an authorised " + "insurer?", + "rowTitleAttributes": {"data-qa": "authorised-insurer-radio-label"}, + "valueList": [{"text": "Yes"}], + }, + ] + } + ] + + assert to_dict(output) == to_dict(expected) + + @pytest.mark.parametrize( "address_fields, formatted_address", ( @@ -460,7 +825,7 @@ def test_get_formatted_address(address_fields, formatted_address): @pytest.mark.parametrize( "max_value, expected_width", [ - (None, 10), + (None, 15), (1, 1), (123123123123, 15), ], @@ -531,6 +896,7 @@ def test_calculated_summary_config(): "type": "currency", "currency": "GBP", "link": "/questionnaire/first-number-block/?return_to=final-summary&return_to_answer_id=first-number-answer#first-number-answer", + "decimal_places": 2, } ], }, @@ -538,7 +904,6 @@ def test_calculated_summary_config(): answers_are_editable=True, no_answer_provided="No answer Provided", edit_link_text="Change", - edit_link_aria_label="Change your answer for", ), SummaryRow( question={ @@ -553,6 +918,7 @@ def test_calculated_summary_config(): "type": "currency", "currency": "GBP", "link": "/questionnaire/second-number-block/?return_to=final-summary&return_to_answer_id=second-number-answer#second-number-answer", + "decimal_places": 2, } ], }, @@ -560,7 +926,6 @@ def test_calculated_summary_config(): answers_are_editable=True, no_answer_provided="No answer Provided", edit_link_text="Change", - edit_link_aria_label="Change your answer for", ), SummaryRow( question={ @@ -572,7 +937,6 @@ def test_calculated_summary_config(): answers_are_editable=False, no_answer_provided=None, edit_link_text=None, - edit_link_aria_label=None, ), ] @@ -596,6 +960,7 @@ def test_calculated_summary_config(): "/questionnaire/first-number-block/?return_to=final-summary" "&return_to_answer_id=first-number-answer#first-number-answer" ), + "decimal_places": 2, } ], }, @@ -618,6 +983,7 @@ def test_calculated_summary_config(): "/questionnaire/second-number-block/?return_to=final-summary" "&return_to_answer_id=second-number-answer#second-number-answer" ), + "decimal_places": 2, } ], }, @@ -639,5 +1005,379 @@ def test_calculated_summary_config(): assert to_dict(expected) == to_dict(result) +@pytest.mark.usefixtures("gb_locale") +def test_summary_item_config_with_list_collector(): + expected = [ + { + "rowItems": [ + { + "actions": [ + { + "visuallyHiddenText": "Change details for Company A", + "attributes": {"data-qa": "list-item-change-1-link"}, + "text": "Change", + "url": "change_link_url", + }, + { + "visuallyHiddenText": "Remove details for Company A", + "attributes": {"data-qa": "list-item-remove-1-link"}, + "text": "Remove", + "url": "remove_link_url", + }, + ], + "iconVisuallyHiddenText": None, + "iconType": None, + "id": "vmmPmD", + "rowTitle": "Company A", + "rowTitleAttributes": { + "data-list-item-id": "vmmPmD", + "data-qa": "list-item-1-label", + }, + }, + { + "actions": [ + { + "visuallyHiddenText": "Change answer for Company A: Registration number", + "attributes": { + "data-ga": "click", + "data-ga-action": "Edit", + "data-ga-category": "Link", + "data-ga-label": "Edit", + "data-ga-page": "Summary", + "data-qa": "registration-number-edit", + }, + "text": "Change", + "url": "edit_link_url", + } + ], + "attributes": {"data-qa": "registration-number"}, + "id": "registration-number", + "rowTitle": "Registration number", + "rowTitleAttributes": {"data-qa": "registration-number-label"}, + "valueList": [{"text": "123"}], + }, + { + "actions": [ + { + "visuallyHiddenText": "Change answer for Company A: Is this UK " + "company or branch an authorised " + "insurer?", + "attributes": { + "data-ga": "click", + "data-ga-action": "Edit", + "data-ga-category": "Link", + "data-ga-label": "Edit", + "data-ga-page": "Summary", + "data-qa": "authorised-insurer-radio-edit", + }, + "text": "Change", + "url": "edit_link_url", + } + ], + "attributes": {"data-qa": "authorised-insurer-radio"}, + "id": "authorised-insurer-radio", + "rowTitle": "Is this UK company or branch an authorised " + "insurer?", + "rowTitleAttributes": {"data-qa": "authorised-insurer-radio-label"}, + "valueList": [{"text": "Yes"}], + }, + ] + } + ] + + result = map_summary_item_config( + group={ + "blocks": [ + { + "title": "Companies or UK branches", + "type": "List", + "add_link": "/questionnaire/companies/add-company/?return_to=section-summary", + "add_link_text": "Add another UK company or branch", + "empty_list_text": "No UK company or branch added", + "list_name": "companies", + "related_answers": { + "vmmPmD": [ + { + "id": "edit-company", + "title": None, + "number": None, + "question": { + "id": "add-question", + "type": "General", + "title": " ", + "number": None, + "answers": [ + { + "id": "registration-number", + "label": "Registration number", + "value": 123, + "type": "number", + "unit": None, + "unit_length": None, + "currency": None, + "link": "edit_link_url", + }, + { + "id": "authorised-insurer-radio", + "label": "Is this UK company or branch an authorised insurer?", + "value": { + "label": "Yes", + "detail_answer_value": None, + }, + "type": "radio", + "unit": None, + "unit_length": None, + "currency": None, + "link": "edit_link_url", + }, + ], + }, + } + ] + }, + "answer_title": "Name of UK company or branch", + "list": { + "list_items": [ + { + "item_title": "Company A", + "primary_person": False, + "list_item_id": "vmmPmD", + "edit_link": "change_link_url", + "remove_link": "remove_link_url", + } + ], + "editable": True, + }, + } + ], + }, + summary_type="SectionSummary", + answers_are_editable=True, + no_answer_provided="No answer Provided", + remove_link_aria_label="Remove details for {item_name}", + remove_link_text="Remove", + edit_link_text="Change", + edit_link_aria_label="Change details for {item_name}", + calculated_question={}, + ) + + assert to_dict(expected) == to_dict(result) + + +@pytest.mark.usefixtures("gb_locale") +def test_summary_item_config_with_list_collector_and_one_related_answer(): + expected = [ + { + "rowItems": [ + { + "actions": [ + { + "visuallyHiddenText": "Change details for Company A", + "attributes": {"data-qa": "list-item-change-1-link"}, + "text": "Change", + "url": "change_link_url", + }, + { + "visuallyHiddenText": "Remove details for Company A", + "attributes": {"data-qa": "list-item-remove-1-link"}, + "text": "Remove", + "url": "remove_link_url", + }, + ], + "iconVisuallyHiddenText": None, + "iconType": None, + "id": "vmmPmD", + "rowTitle": "Company A", + "rowTitleAttributes": { + "data-list-item-id": "vmmPmD", + "data-qa": "list-item-1-label", + }, + }, + { + "actions": [ + { + "visuallyHiddenText": "Change answer for Company A: " + "Registration number", + "attributes": { + "data-ga": "click", + "data-ga-action": "Edit", + "data-ga-category": "Link", + "data-ga-label": "Edit", + "data-ga-page": "Summary", + "data-qa": "registration-number-edit", + }, + "text": "Change", + "url": "edit_link_url", + } + ], + "attributes": {"data-qa": "registration-number"}, + "id": "registration-number", + "rowTitle": "Registration number", + "rowTitleAttributes": {"data-qa": "registration-number-label"}, + "valueList": [{"text": "123"}], + }, + ] + } + ] + + result = map_summary_item_config( + group={ + "blocks": [ + { + "title": "Companies or UK branches", + "type": "List", + "add_link": "/questionnaire/companies/add-company/?return_to=section-summary", + "add_link_text": "Add another UK company or branch", + "empty_list_text": "No UK company or branch added", + "list_name": "companies", + "related_answers": { + "vmmPmD": [ + { + "id": "edit-company", + "title": None, + "number": None, + "question": { + "id": "add-question", + "type": "General", + "title": "Give details about the company or branch that undertakes general insurance business", + "number": None, + "answers": [ + { + "id": "registration-number", + "label": "Registration number", + "value": 123, + "type": "number", + "unit": None, + "unit_length": None, + "currency": None, + "link": "edit_link_url", + } + ], + }, + } + ] + }, + "answer_title": "Name of UK company or branch", + "list": { + "list_items": [ + { + "item_title": "Company A", + "primary_person": False, + "list_item_id": "vmmPmD", + "edit_link": "change_link_url", + "remove_link": "remove_link_url", + } + ], + "editable": True, + }, + } + ], + }, + summary_type="SectionSummary", + answers_are_editable=True, + no_answer_provided="No answer Provided", + remove_link_aria_label="Remove details for {item_name}", + remove_link_text="Remove", + edit_link_text="Change", + edit_link_aria_label="Change details for {item_name}", + calculated_question={}, + ) + + assert to_dict(expected) == to_dict(result) + + +def test_map_list_collector_config_render_icon_set(): + list_items = [ + { + "item_title": "Mark Bloggs", + "list_item_id": "one", + "repeating_blocks": True, + "is_complete": True, + }, + { + "item_title": "Joe Bloggs", + "list_item_id": "two", + "repeating_block": True, + "is_complete": False, + }, + ] + + output = map_list_collector_config(list_items, editable=True, render_icon=True) + + expected = [ + { + "rowItems": [ + { + "actions": [], + "iconVisuallyHiddenText": "Completed", + "iconType": "check", + "id": "one", + "rowTitleAttributes": { + "data-list-item-id": "one", + "data-qa": "list-item-1-label", + }, + "rowTitle": "Mark Bloggs", + } + ] + }, + { + "rowItems": [ + { + "actions": [], + "iconVisuallyHiddenText": None, + "iconType": None, + "id": "two", + "rowTitleAttributes": { + "data-list-item-id": "two", + "data-qa": "list-item-2-label", + }, + "rowTitle": "Joe Bloggs", + } + ] + }, + ] + + assert output == expected + + +def test_map_list_config(): + list_values = [ + { + "item_title": "Harry Potter", + "primary_person": False, + "list_item_id": "1", + "is_complete": True, + "repeating_blocks": False, + }, + { + "item_title": "Clark Kent", + "primary_person": False, + "list_item_id": "2", + "is_complete": False, + "repeating_blocks": False, + }, + ] + + output = map_list_config(list_values) + + expected = [ + { + "text": "Harry Potter", + "iconType": "check", + "attributes": { + "data-qa": "list-item-1-label", + }, + }, + { + "text": "Clark Kent", + "attributes": { + "data-qa": "list-item-2-label", + }, + }, + ] + + assert output == expected + + def to_dict(obj): return json.loads(json.dumps(obj, default=lambda o: o.__dict__)) diff --git a/tests/app/test_setup.py b/tests/app/test_setup.py index ffecdfa1b8..695ee19298 100644 --- a/tests/app/test_setup.py +++ b/tests/app/test_setup.py @@ -1,6 +1,8 @@ import pytest +from mock import MagicMock from app.helpers import get_span_and_trace +from app.setup import setup_secure_cookies @pytest.mark.parametrize( @@ -26,3 +28,16 @@ def test_get_span_and_trace(cloud_trace_header, expected_trace, expected_span): span, trace = get_span_and_trace(cloud_trace_header) assert trace == expected_trace assert span == expected_span + + +def test_setup_secure_cookies_missing_secret_key(): + app = MagicMock() + app.eq = {"secret_store": MagicMock()} + app.secret_key = None + + app.eq["secret_store"].get_secret_by_name.return_value = None + + with pytest.raises(ValueError): + setup_secure_cookies(app) + + assert app.secret_key is None diff --git a/tests/app/utilities/conftest.py b/tests/app/utilities/conftest.py new file mode 100644 index 0000000000..3bbbeb7c51 --- /dev/null +++ b/tests/app/utilities/conftest.py @@ -0,0 +1,13 @@ +import pytest + +from app.data_models.metadata_proxy import MetadataProxy + + +@pytest.fixture +def metadata_with_cir_instrument_id(): + return MetadataProxy.from_dict( + { + "cir_instrument_id": "f0519981-426c-8b93-75c0-bfc40c66fe25", + "language_code": "cy", + }, + ) diff --git a/tests/app/utilities/test_decimal_places.py b/tests/app/utilities/test_decimal_places.py new file mode 100644 index 0000000000..5ada02c153 --- /dev/null +++ b/tests/app/utilities/test_decimal_places.py @@ -0,0 +1,44 @@ +import unicodedata + +import pytest + +from app.utilities.decimal_places import ( + custom_format_decimal, + custom_format_unit, + get_formatted_currency, +) +from tests.app.test_jinja_filters import ( + TEST_FORMAT_CURRENCY_PARAMS, + TEST_FORMAT_NUMBER_PARAMS, + TEST_FORMAT_UNIT_PARAMS, +) + + +@pytest.mark.parametrize(*TEST_FORMAT_NUMBER_PARAMS) +def test_custom_format_decimal(value, locale_string, expected_result): + result = custom_format_decimal(value, locale_string) + + assert unicodedata.normalize("NFKD", result) == expected_result + + +@pytest.mark.parametrize(*TEST_FORMAT_UNIT_PARAMS) +def test_custom_format_unit( + value, measurement_unit, locale_string, length, expected_result +): + result = custom_format_unit(value, measurement_unit, locale_string, length) + + assert result == expected_result + + +@pytest.mark.parametrize(*TEST_FORMAT_CURRENCY_PARAMS) +def test_custom_format_currency( + value, currency, locale_string, decimal_limit, expected_result +): + result = get_formatted_currency( + value=value, + currency=currency, + locale=locale_string, + decimal_limit=decimal_limit, + ) + + assert unicodedata.normalize("NFKD", result) == expected_result diff --git a/tests/app/utilities/test_metadata_validators.py b/tests/app/utilities/test_metadata_validators.py new file mode 100644 index 0000000000..ceafe03f51 --- /dev/null +++ b/tests/app/utilities/test_metadata_validators.py @@ -0,0 +1,41 @@ +import pytest + +from app.utilities.metadata_validators import DateString + + +def test_deserialize_defaults_to_iso_format(): + date_string = DateString() + assert ( + date_string._deserialize( # pylint: disable=protected-access + value="2014-12-22T03:12:58.019077+00:00", attr="", data="" + ) + == "2014-12-22T03:12:58.019077+00:00" + ) + + +def test_deserialize_does_not_default_to_iso_format(): + date_string = DateString(format="%Y-%m-%d") + assert ( + date_string._deserialize( # pylint: disable=protected-access + value="2014-12-22", attr="", data="" + ) + == "2014-12-22" + ) + + +def test_deserialize_incorrect_format_errors_with_default_format(): + date_string = DateString() + with pytest.raises(Exception) as e: + date_string._deserialize( # pylint: disable=protected-access + value="2014-12-22", attr="", data="" + ) + assert "Not a valid datetime." in str(e.value) + + +def test_deserialize_given_format_errors_with_wrong_format(): + date_string = DateString("%d-%m-%Y") + with pytest.raises(Exception) as e: + date_string._deserialize( # pylint: disable=protected-access + value="2023-10-22T05:16:58.019477+00:00", attr="", data="" + ) + assert "Not a valid datetime." in str(e.value) diff --git a/tests/app/utilities/test_schema.py b/tests/app/utilities/test_schema.py index 0660643168..8d7bea6ddc 100644 --- a/tests/app/utilities/test_schema.py +++ b/tests/app/utilities/test_schema.py @@ -1,13 +1,20 @@ import os -from unittest.mock import Mock, patch +from http.client import HTTPMessage import pytest import responses -from werkzeug.exceptions import NotFound +from mock import Mock, patch +from requests import RequestException +from urllib3.connectionpool import HTTPConnectionPool +from urllib3.response import HTTPResponse +from app.oidc.gcp_oidc import OIDCCredentialsServiceGCP from app.questionnaire import QuestionnaireSchema from app.setup import create_app from app.utilities.schema import ( + CIR_RETRIEVE_COLLECTION_INSTRUMENT_URL, + SCHEMA_REQUEST_MAX_RETRIES, + SchemaRequestFailed, _load_schema_from_name, cache_questionnaire_schemas, get_allowed_languages, @@ -18,8 +25,10 @@ load_schema_from_name, load_schema_from_url, ) +from tests.app.questionnaire.conftest import get_metadata TEST_SCHEMA_URL = "http://test.domain/schema.json" +TEST_CIR_URL = "http://cir.domain" def test_valid_schema_names_from_params(): @@ -64,7 +73,7 @@ def test_get_schema_list(): assert get_schema_list() == expected_output -# pylint: disable=no-value-for-parameter +# pylint: disable=no-value-for-parameter,missing-kwoa def test_schema_cache_on_function_call(): _load_schema_from_name.cache_clear() @@ -144,7 +153,7 @@ def test_load_schema_from_url_200(): mock_schema = QuestionnaireSchema({}, language_code="cy") responses.add(responses.GET, TEST_SCHEMA_URL, json=mock_schema.json, status=200) - loaded_schema = load_schema_from_url(survey_url=TEST_SCHEMA_URL, language_code="cy") + loaded_schema = load_schema_from_url(url=TEST_SCHEMA_URL, language_code="cy") assert loaded_schema.json == mock_schema.json assert loaded_schema.language_code == mock_schema.language_code @@ -155,21 +164,37 @@ def test_load_schema_from_url_200(): assert cache_info.hits == 0 +@pytest.mark.parametrize( + "status_code", + [401, 403, 404, 501, 511], +) @responses.activate -def test_load_schema_from_url_404(): +def test_load_schema_from_url_non_200(status_code): load_schema_from_url.cache_clear() - mock_schema = QuestionnaireSchema({}) - responses.add(responses.GET, TEST_SCHEMA_URL, json=mock_schema.json, status=404) + responses.add( + responses.GET, TEST_SCHEMA_URL, json=mock_schema.json, status=status_code + ) - with pytest.raises(NotFound): - load_schema_from_url(survey_url=TEST_SCHEMA_URL, language_code="en") + with pytest.raises(SchemaRequestFailed) as exc: + load_schema_from_url(url=TEST_SCHEMA_URL, language_code="en") cache_info = load_schema_from_url.cache_info() assert cache_info.currsize == 0 assert cache_info.misses == 1 assert cache_info.hits == 0 + assert str(exc.value) == "schema request failed" + + +@responses.activate +def test_load_schema_from_url_request_failed(): + responses.add(responses.GET, TEST_SCHEMA_URL, body=RequestException()) + with pytest.raises(SchemaRequestFailed) as exc: + load_schema_from_url(url=TEST_SCHEMA_URL, language_code="en") + + assert str(exc.value) == "schema request failed" + @responses.activate def test_load_schema_from_url_uses_cache(): @@ -179,7 +204,7 @@ def test_load_schema_from_url_uses_cache(): responses.add(responses.GET, TEST_SCHEMA_URL, json=mock_schema.json, status=200) # First load: Add to cache, no hits - load_schema_from_url(survey_url=TEST_SCHEMA_URL, language_code="cy") + load_schema_from_url(url=TEST_SCHEMA_URL, language_code="cy") cache_info = load_schema_from_url.cache_info() assert cache_info.currsize == 1 @@ -187,7 +212,7 @@ def test_load_schema_from_url_uses_cache(): assert cache_info.hits == 0 # Second load: Read from cache, 1 hit - load_schema_from_url(survey_url=TEST_SCHEMA_URL, language_code="cy") + load_schema_from_url(url=TEST_SCHEMA_URL, language_code="cy") cache_info = load_schema_from_url.cache_info() assert cache_info.currsize == 1 @@ -196,13 +221,193 @@ def test_load_schema_from_url_uses_cache(): @responses.activate -def test_load_schema_from_metadata_with_survey_url(): +def test_load_schema_from_metadata_with_schema_url(): load_schema_from_url.cache_clear() - metadata = {"survey_url": TEST_SCHEMA_URL, "language_code": "cy"} + metadata = get_metadata( + extra_metadata={"schema_url": TEST_SCHEMA_URL, "language_code": "cy"}, + ) mock_schema = QuestionnaireSchema({}, language_code="cy") responses.add(responses.GET, TEST_SCHEMA_URL, json=mock_schema.json, status=200) - loaded_schema = load_schema_from_metadata(metadata=metadata) + loaded_schema = load_schema_from_metadata(metadata=metadata, language_code="cy") assert loaded_schema.json == mock_schema.json assert loaded_schema.language_code == mock_schema.language_code + + +@responses.activate +def test_load_schema_from_metadata_with_schema_url_and_override_language_code(): + load_schema_from_url.cache_clear() + language_code = "en" + + metadata = get_metadata( + extra_metadata={"schema_url": TEST_SCHEMA_URL, "language_code": "cy"} + ) + + mock_schema = QuestionnaireSchema({}, language_code="cy") + responses.add(responses.GET, TEST_SCHEMA_URL, json=mock_schema.json, status=200) + + loaded_schema = load_schema_from_metadata( + metadata=metadata, language_code=language_code + ) + + assert loaded_schema.json == mock_schema.json + assert loaded_schema.language_code == language_code + + +@responses.activate +def test_load_schema_from_metadata_with_cir_instrument_id_200( + app, metadata_with_cir_instrument_id +): + load_schema_from_url.cache_clear() + mock_schema = QuestionnaireSchema({}, language_code="cy") + responses.add( + responses.GET, + f"{TEST_CIR_URL}{CIR_RETRIEVE_COLLECTION_INSTRUMENT_URL}", + json=mock_schema.json, + status=200, + ) + + with app.app_context(): + app.config["CIR_API_BASE_URL"] = TEST_CIR_URL + loaded_schema = load_schema_from_metadata( + metadata=metadata_with_cir_instrument_id, language_code="cy" + ) + + assert loaded_schema.json == mock_schema.json + assert loaded_schema.language_code == mock_schema.language_code + + +@responses.activate +def test_load_schema_from_metadata_with_cir_instrument_id_request_failed( + app, metadata_with_cir_instrument_id +): + load_schema_from_url.cache_clear() + responses.add( + responses.GET, + f"{TEST_CIR_URL}{CIR_RETRIEVE_COLLECTION_INSTRUMENT_URL}", + body=RequestException(), + ) + with app.app_context(): + with pytest.raises(SchemaRequestFailed): + app.config["CIR_API_BASE_URL"] = TEST_CIR_URL + load_schema_from_metadata( + metadata=metadata_with_cir_instrument_id, language_code="cy" + ) + + +@pytest.mark.parametrize( + "status_code", + [401, 403, 404, 501, 511], +) +@responses.activate +def test_load_schema_from_metadata_with_cir_instrument_id_non_200( + app, status_code, metadata_with_cir_instrument_id +): + load_schema_from_url.cache_clear() + mock_schema = QuestionnaireSchema({}, language_code="cy") + responses.add( + responses.GET, + f"{TEST_CIR_URL}{CIR_RETRIEVE_COLLECTION_INSTRUMENT_URL}", + json=mock_schema.json, + status=status_code, + ) + with app.app_context(): + with pytest.raises(SchemaRequestFailed) as exc: + app.config["CIR_API_BASE_URL"] = TEST_CIR_URL + load_schema_from_metadata( + metadata=metadata_with_cir_instrument_id, language_code="cy" + ) + assert str(exc.value) == "schema request failed" + + +def get_mocked_make_request(mocker, status_codes): + mocked_responses = [] + for status_code in status_codes: + response = HTTPResponse(status=status_code, headers={}, msg=HTTPMessage()) + response.drain_conn = mocker.Mock(return_value=None) + + mocked_responses.append(response) + + return mocker.patch.object( + HTTPConnectionPool, + "_make_request", + side_effect=mocked_responses, + ) + + +def test_load_schema_from_url_retries_timeout_error(mocked_make_request_with_timeout): + load_schema_from_url.cache_clear() + + try: + schema = load_schema_from_url(url=TEST_SCHEMA_URL, language_code="en") + except SchemaRequestFailed: + return pytest.fail("Schema request unexpectedly failed") + + assert schema.json == QuestionnaireSchema({}).json + + expected_call = SCHEMA_REQUEST_MAX_RETRIES + 1 # Max retries + the initial request + assert mocked_make_request_with_timeout.call_count == expected_call + + +@pytest.mark.usefixtures("mocked_response_content") +def test_load_schema_from_url_retries_transient_error(mocker): + mocked_make_request = get_mocked_make_request(mocker, status_codes=[500, 500, 200]) + load_schema_from_url.cache_clear() + + try: + schema = load_schema_from_url(url=TEST_SCHEMA_URL, language_code="en") + except SchemaRequestFailed: + return pytest.fail("Schema request unexpectedly failed") + + assert schema.json == QuestionnaireSchema({}).json + + expected_call = SCHEMA_REQUEST_MAX_RETRIES + 1 # Max retries + the initial request + assert mocked_make_request.call_count == expected_call + + +def test_load_schema_from_url_max_retries(mocker): + mocked_make_request = get_mocked_make_request( + mocker, status_codes=[500, 500, 500, 500] + ) + load_schema_from_url.cache_clear() + + with pytest.raises(SchemaRequestFailed) as exc: + load_schema_from_url(url=TEST_SCHEMA_URL, language_code="en") + + assert str(exc.value) == "schema request failed" + assert mocked_make_request.call_count == 3 + + +@responses.activate +def test_load_schema_from_metadata_cir_with_gcp_authentication( + app, metadata_with_cir_instrument_id, mocker +): + load_schema_from_url.cache_clear() + mock_schema = QuestionnaireSchema({}, language_code="cy") + + mock_oidc_service = Mock(spec=OIDCCredentialsServiceGCP) + mocker.patch.dict( + "app.services.supplementary_data.current_app.eq", + {"oidc_credentials_service": mock_oidc_service}, + ) + + responses.add( + responses.GET, + f"{TEST_CIR_URL}{CIR_RETRIEVE_COLLECTION_INSTRUMENT_URL}", + json=mock_schema.json, + status=200, + ) + + with app.app_context(): + app.config["CIR_API_BASE_URL"] = TEST_CIR_URL + loaded_schema = load_schema_from_metadata( + metadata=metadata_with_cir_instrument_id, language_code="cy" + ) + + mock_oidc_service.get_credentials.assert_called_once_with( + iap_client_id=app.config["CIR_OAUTH2_CLIENT_ID"] + ) + + assert loaded_schema.json == mock_schema.json + assert loaded_schema.language_code == mock_schema.language_code diff --git a/tests/app/utilities/test_strings.py b/tests/app/utilities/test_strings.py index 326371c306..a07b97640c 100644 --- a/tests/app/utilities/test_strings.py +++ b/tests/app/utilities/test_strings.py @@ -27,3 +27,15 @@ def test_to_bytes(input_str, bytes_str): ) def test_to_str(input_str, bytes_str): assert strings.to_str(input_str) == bytes_str + + +@pytest.mark.parametrize( + "input_str, expected_result", + ( + ("CalculatedSummary", "calculated-summary"), + ("GrandCalculatedSummary", "grand-calculated-summary"), + ("Block", "block"), + ), +) +def test_pascal_case_to_hyphenated_lowercase(input_str, expected_result): + assert strings.pascal_case_to_hyphenated_lowercase(input_str) == expected_result diff --git a/tests/app/views/contexts/__init__.py b/tests/app/views/contexts/__init__.py index e71bf451a3..38e5cfe793 100644 --- a/tests/app/views/contexts/__init__.py +++ b/tests/app/views/contexts/__init__.py @@ -1,18 +1,31 @@ -def assert_summary_context(context): +def assert_summary_context(context, summary_item_type="question"): summary_context = context["summary"] - for key_value in ("groups", "answers_are_editable", "summary_type"): + for key_value in ("sections", "answers_are_editable", "summary_type"): assert ( key_value in summary_context ), f"Key value {key_value} missing from context['summary']" - for group in context["summary"]["groups"]: - assert "id" in group - assert "blocks" in group - for block in group["blocks"]: - assert "question" in block - assert "title" in block["question"] - assert "answers" in block["question"] - for answer in block["question"]["answers"]: - assert "id" in answer - assert "value" in answer - assert "type" in answer + for section in summary_context["sections"]: + for group in section["groups"]: + assert "id" in group + assert "blocks" in group + for block in group["blocks"]: + assert summary_item_type in block + assert "title" in block[summary_item_type] + assert "answers" in block[summary_item_type] + for answer in block[summary_item_type]["answers"]: + assert "id" in answer + assert "value" in answer + assert "type" in answer + + +def assert_preview_context(context): + for key_value in ("blocks", "title", "id"): + assert ( + key_value in context["sections"][0] + ), f"Key value {key_value} missing from context" + + for block in context["sections"][0]["blocks"]: + assert "question" in block + for answers in block["question"]["answers"]: + assert len(answers) != 0 diff --git a/tests/app/views/contexts/conftest.py b/tests/app/views/contexts/conftest.py index 3ba68b9222..6384ba8b35 100644 --- a/tests/app/views/contexts/conftest.py +++ b/tests/app/views/contexts/conftest.py @@ -1,6 +1,7 @@ import pytest from mock import MagicMock +from app.data_models import QuestionnaireStore from app.data_models.answer_store import AnswerStore from app.data_models.list_store import ListStore from app.data_models.progress_store import ProgressStore @@ -184,11 +185,196 @@ def people_answer_store(): ) +@pytest.fixture +def repeating_blocks_answer_store(): + return AnswerStore( + [ + { + "answer_id": "company-or-branch-name", + "value": "CompanyA", + "list_item_id": "PlwgoG", + }, + { + "answer_id": "registration-number", + "value": "123", + "list_item_id": "PlwgoG", + }, + { + "answer_id": "registration-date", + "value": "2023-01-01", + "list_item_id": "PlwgoG", + }, + { + "answer_id": "authorised-trader-uk-radio", + "value": "Yes", + "list_item_id": "PlwgoG", + }, + { + "answer_id": "authorised-trader-eu-radio", + "value": "Yes", + "list_item_id": "PlwgoG", + }, + { + "answer_id": "company-or-branch-name", + "value": "CompanyB", + "list_item_id": "UHPLbX", + }, + { + "answer_id": "registration-number", + "value": "456", + "list_item_id": "UHPLbX", + }, + { + "answer_id": "registration-date", + "value": "2023-01-01", + "list_item_id": "UHPLbX", + }, + { + "answer_id": "authorised-trader-uk-radio", + "value": "No", + "list_item_id": "UHPLbX", + }, + { + "answer_id": "authorised-trader-eu-radio", + "value": "No", + "list_item_id": "UHPLbX", + }, + ] + ) + + +@pytest.fixture +def companies_answer_store(): + return AnswerStore( + [ + { + "answer_id": "company-or-branch-name", + "value": "company a", + "list_item_id": "PlwgoG", + }, + { + "answer_id": "registration-number", + "value": 123, + "list_item_id": "PlwgoG", + }, + { + "answer_id": "authorised-insurer-radio", + "value": "Yes", + "list_item_id": "PlwgoG", + }, + { + "answer_id": "company-or-branch-name", + "value": "company b", + "list_item_id": "UHPLbX", + }, + { + "answer_id": "registration-number", + "value": 456, + "list_item_id": "UHPLbX", + }, + { + "answer_id": "authorised-insurer-radio", + "value": "No", + "list_item_id": "UHPLbX", + }, + ] + ) + + +@pytest.fixture +def companies_variants_answer_store_first_variant(): + return AnswerStore( + [ + { + "answer_id": "uk-based-answer", + "value": "Yes", + }, + { + "answer_id": "company-or-branch-name", + "value": "company a", + "list_item_id": "PlwgoG", + }, + { + "answer_id": "registration-number", + "value": 123, + "list_item_id": "PlwgoG", + }, + { + "answer_id": "authorised-insurer-radio", + "value": "Yes", + "list_item_id": "PlwgoG", + }, + { + "answer_id": "company-or-branch-name", + "value": "company b", + "list_item_id": "UHPLbX", + }, + { + "answer_id": "registration-number", + "value": 456, + "list_item_id": "UHPLbX", + }, + { + "answer_id": "authorised-insurer-radio", + "value": "No", + "list_item_id": "UHPLbX", + }, + ] + ) + + +@pytest.fixture +def companies_variants_answer_store_second_variant(): + return AnswerStore( + [ + { + "answer_id": "uk-based-answer", + "value": "No", + }, + { + "answer_id": "company-or-branch-name", + "value": "company a", + "list_item_id": "PlwgoG", + }, + { + "answer_id": "registration-number", + "value": 123, + "list_item_id": "PlwgoG", + }, + { + "answer_id": "authorised-insurer-radio", + "value": "Yes", + "list_item_id": "PlwgoG", + }, + { + "answer_id": "company-or-branch-name", + "value": "company b", + "list_item_id": "UHPLbX", + }, + { + "answer_id": "registration-number", + "value": 456, + "list_item_id": "UHPLbX", + }, + { + "answer_id": "authorised-insurer-radio", + "value": "No", + "list_item_id": "UHPLbX", + }, + ] + ) + + @pytest.fixture def people_list_store(): return ListStore([{"items": ["PlwgoG", "UHPLbX"], "name": "people"}]) +@pytest.fixture +def repeating_blocks_list_store(): + return ListStore([{"items": ["PlwgoG", "UHPLbX"], "name": "companies"}]) + + @pytest.fixture def response_metadata(): return {"started_at": "2021-01-01T09:00:00.220038+00:00"} @@ -197,16 +383,7 @@ def response_metadata(): @pytest.fixture def fake_session_data(): return SessionData( - tx_id="tx_id", - schema_name="some_schema_name", - period_str="period_str", language_code=None, - launch_language_code=None, - survey_url=None, - ru_name="ru_name", - ru_ref="ru_ref", - response_id="response_id", - case_id="case_id", ) @@ -215,6 +392,23 @@ def test_calculated_summary_schema(): return load_schema_from_name("test_calculated_summary") +@pytest.fixture +def test_grand_calculated_summary_schema(): + return load_schema_from_name("test_grand_calculated_summary") + + +@pytest.fixture +def test_calculated_summary_repeating_and_static_answers_schema(): + return load_schema_from_name( + "test_new_calculated_summary_repeating_and_static_answers" + ) + + +@pytest.fixture +def test_calculated_summary_repeating_blocks(): + return load_schema_from_name("test_new_calculated_summary_repeating_blocks") + + @pytest.fixture def test_calculated_summary_answers(): answers = [ @@ -235,6 +429,69 @@ def test_calculated_summary_answers(): return AnswerStore(answers) +@pytest.fixture +def test_grand_calculated_summary_answers(): + answers = [ + {"value": 10, "answer_id": "q1-a1"}, + {"value": 1, "answer_id": "q1-a2"}, + {"value": 20, "answer_id": "q2-a1"}, + {"value": 2, "answer_id": "q2-a2"}, + {"value": 30, "answer_id": "q3-a1"}, + {"value": 3, "answer_id": "q3-a2"}, + {"value": 40, "answer_id": "q4-a1"}, + {"value": 4, "answer_id": "q4-a2"}, + ] + return AnswerStore(answers) + + +@pytest.fixture +def test_calculated_summary_answers_skipped_fourth(): + answers = [ + {"value": 1, "answer_id": "first-number-answer"}, + {"value": 2, "answer_id": "second-number-answer"}, + {"value": 3, "answer_id": "second-number-answer-unit-total"}, + {"value": 4, "answer_id": "second-number-answer-also-in-total"}, + {"value": 5, "answer_id": "third-number-answer"}, + {"value": 6, "answer_id": "third-and-a-half-number-answer-unit-total"}, + {"value": "Yes", "answer_id": "skip-fourth-block-answer"}, + {"value": 9, "answer_id": "fifth-percent-answer"}, + {"value": 10, "answer_id": "fifth-number-answer"}, + {"value": 11, "answer_id": "sixth-percent-answer"}, + {"value": 12, "answer_id": "sixth-number-answer"}, + ] + return AnswerStore(answers) + + @pytest.fixture def test_section_summary_schema(): return load_schema_from_name("test_section_summary") + + +@pytest.fixture +def test_introduction_preview_linear_schema(): + return load_schema_from_name("test_introduction") + + +@pytest.fixture +def questionnaire_store(): + storage = MagicMock() + storage.get_user_data = MagicMock(return_value=("{}", "ce_sid", 1, None)) + storage.add_or_update = MagicMock() + + store = QuestionnaireStore(storage) + + store.data_stores.answer_store = AnswerStore() + store.data_stores.metadata = { + "ru_name": "ESSENTIAL ENTERPRISE LTD.", + "ref_p_start_date": "2016-02-02", + "ref_p_end_date": "2016-03-03", + "display_address": "68 Abingdon Road, Goathill", + "trad_as": "ESSENTIAL ENTERPRISE LTD.", + "ru_ref": "12345678901A", + } + + store.data_stores.response_metadata = { + "started_at": "2018-07-04T14:49:33.448608+00:00" + } + + return store diff --git a/tests/app/views/contexts/summary/test_answer.py b/tests/app/views/contexts/summary/test_answer.py index c8b8e8740c..4eaa47078e 100644 --- a/tests/app/views/contexts/summary/test_answer.py +++ b/tests/app/views/contexts/summary/test_answer.py @@ -1,17 +1,73 @@ import pytest +from app.questionnaire.return_location import ReturnLocation from app.views.contexts.summary.answer import Answer @pytest.mark.usefixtures("app") -def test_create_answer(): +@pytest.mark.parametrize( + "return_to, return_to_block_id, is_in_repeating_section, return_to_answer_id, query_string", + [ + ( + "section-summary", + None, + False, + "answer-id", + "?return_to=section-summary&return_to_answer_id=answer-id-answer-item-id,answer-id", + ), + (None, None, False, "answer-id", ""), + ( + "calculated-summary", + "total", + False, + "answer-id", + "?return_to=calculated-summary&return_to_answer_id=answer-id-answer-item-id,answer-id&return_to_block_id=total", + ), + ( + "section-summary", + None, + True, + "answer-id-answer-item-id", + "?return_to=section-summary&return_to_answer_id=answer-id,answer-id-answer-item-id", + ), + (None, None, True, "answer-id-answer-item-id", ""), + ( + "calculated-summary", + "total", + True, + "answer-id-answer-item-id", + "?return_to=calculated-summary&return_to_answer_id=answer-id,answer-id-answer-item-id&return_to_block_id=total", + ), + ( + "calculated-summary", + "total", + True, + "calculated-summary-1", + "?return_to=calculated-summary&return_to_answer_id=answer-id,calculated-summary-1&return_to_block_id=total", + ), + ], +) +def test_create_answer( + return_to, + return_to_block_id, + is_in_repeating_section, + return_to_answer_id, + query_string, +): + return_location = ReturnLocation( + return_to=return_to, + return_to_block_id=return_to_block_id, + return_to_answer_id=return_to_answer_id, + ) + answer = Answer( answer_schema={"id": "answer-id", "label": "Answer Label", "type": "date"}, answer_value="An answer", block_id="house-type", list_name="answer-list", list_item_id="answer-item-id", - return_to="section-summary", + return_location=return_location, + is_in_repeating_section=is_in_repeating_section, ) assert answer.id == "answer-id" @@ -21,20 +77,25 @@ def test_create_answer(): assert ( answer.link - == "/questionnaire/answer-list/answer-item-id/house-type/?return_to=section-summary&return_to_answer_id=answer-id#answer-id" + == f"/questionnaire/answer-list/answer-item-id/house-type/{query_string}#{answer.id}" ) @pytest.mark.usefixtures("app") def test_date_answer_type(): # When + return_location = ReturnLocation( + return_to="section-summary", + ) + answer = Answer( answer_schema={"id": "answer-id", "label": "", "type": "date"}, answer_value=None, block_id="house-type", list_name="answer-list", list_item_id="answer-item-id", - return_to="section-summary", + return_location=return_location, + is_in_repeating_section=False, ) # Then diff --git a/tests/app/views/contexts/summary/test_block.py b/tests/app/views/contexts/summary/test_block.py index a955e0fbab..3ae281acd1 100644 --- a/tests/app/views/contexts/summary/test_block.py +++ b/tests/app/views/contexts/summary/test_block.py @@ -1,4 +1,5 @@ from app.questionnaire.location import Location +from app.questionnaire.return_location import ReturnLocation from app.views.contexts.summary.block import Block @@ -12,6 +13,8 @@ def test_create_block(mocker): } location = Location(section_id="a-section") + return_location = ReturnLocation(return_to="final-summary") + question = mocker.MagicMock() question.serialize = mocker.MagicMock(return_value="A Question") @@ -22,13 +25,11 @@ def test_create_block(mocker): ) block = Block( block_schema, - answer_store=mocker.MagicMock(), - list_store=mocker.MagicMock(), - metadata=mocker.MagicMock(), - response_metadata=mocker.MagicMock(), + data_stores=mocker.MagicMock(), schema=mocker.MagicMock(), location=location, - return_to="final-summary", + return_location=return_location, + language="en", ) # Then diff --git a/tests/app/views/contexts/summary/test_question.py b/tests/app/views/contexts/summary/test_question.py index 47507bed85..00216fee12 100644 --- a/tests/app/views/contexts/summary/test_question.py +++ b/tests/app/views/contexts/summary/test_question.py @@ -1,40 +1,14 @@ -# pylint: disable=too-many-lines import pytest -from mock import MagicMock -from app.data_models import Answer +from app.data_models import Answer, ListStore from app.data_models.answer_store import AnswerStore +from app.data_models.data_stores import DataStores from app.questionnaire import QuestionnaireSchema -from app.questionnaire.rules.rule_evaluator import RuleEvaluator -from app.questionnaire.value_source_resolver import ValueSourceResolver +from app.questionnaire.return_location import ReturnLocation +from app.utilities.schema import load_schema_from_name from app.views.contexts.summary.question import Question -def get_rule_evaluator(answer_store, list_store, schema, response_metadata=None): - return RuleEvaluator( - schema=schema, - answer_store=answer_store, - list_store=list_store, - metadata={}, - response_metadata=response_metadata or {}, - location=None, - ) - - -def get_value_source_resolver(answer_store, list_store, schema, response_metadata=None): - return ValueSourceResolver( - answer_store=answer_store, - list_store=list_store, - metadata={}, - response_metadata=response_metadata or {}, - schema=schema, - location=None, - list_item_id=None, - routing_path_block_ids=None, - use_default_answer=True, - ) - - def get_question_schema(answer_schema): return { "id": "question_id", @@ -126,19 +100,19 @@ def address_questionnaire_schema(concatenation_type): ) -def address_question(answer_store, list_store, schema): +def address_question( + schema, + data_stores, +): question_schema = schema.get_questions("what-is-your-address-question")[0] return Question( question_schema, - answer_store=answer_store, + data_stores=data_stores, schema=schema, - rule_evaluator=get_rule_evaluator(answer_store, list_store, schema), location=None, block_id="address-block", - return_to=None, - value_source_resolver=get_value_source_resolver( - answer_store, list_store, schema - ), + return_location=ReturnLocation(), + language="en", ) @@ -146,7 +120,12 @@ def address_question(answer_store, list_store, schema): @pytest.mark.parametrize( "question_title, answers, expected_title, expected_len", ( - ("Question title", [MagicMock()], "Question title", 1), + ( + "Question title", + [{"type": "Number", "id": "age-answer", "mandatory": True, "label": "Age"}], + "Question title", + 1, + ), ("Question title", [], "Question title", 0), ( "", @@ -161,9 +140,8 @@ def test_create_question( answers, expected_title, expected_len, - answer_store, - list_store, mock_schema, + data_stores, ): # Given question_schema = { @@ -175,16 +153,13 @@ def test_create_question( # When question = Question( - question_schema, - answer_store=answer_store, - schema=mock_schema, - rule_evaluator=get_rule_evaluator(answer_store, list_store, mock_schema), - value_source_resolver=get_value_source_resolver( - answer_store, list_store, mock_schema - ), + question_schema=question_schema, + data_stores=data_stores, location=None, block_id="house-type", - return_to=None, + return_location=ReturnLocation(), + language="en", + schema=mock_schema, ) # Then @@ -202,7 +177,9 @@ def test_create_question( ), ) def test_concatenate_textfield_answers( - concatenation_type, concatenation_character, list_store, answer_store + concatenation_type, + concatenation_character, + answer_store, ): # Given schema = address_questionnaire_schema(concatenation_type) @@ -214,7 +191,10 @@ def test_concatenate_textfield_answers( ): answer_store.add_or_update(answer) - question = address_question(answer_store, list_store, schema) + question = address_question( + schema, + DataStores(answer_store=answer_store), + ) # Then assert ( question.answers[0]["value"] @@ -232,7 +212,7 @@ def test_concatenate_textfield_answers( ), ) def test_concatenate_textfield_answers_default( - concatenation_type, concatenation_character, list_store, answer_store + concatenation_type, concatenation_character, answer_store ): # Given schema = address_questionnaire_schema(concatenation_type) @@ -244,7 +224,10 @@ def test_concatenate_textfield_answers_default( answer_store.add_or_update(answer) # When - question = address_question(answer_store, list_store, schema) + question = address_question( + schema, + data_stores=DataStores(answer_store=answer_store), + ) # Then assert ( @@ -263,7 +246,10 @@ def test_concatenate_textfield_answers_default( ), ) def test_concatenate_number_and_checkbox_answers( - concatenation_type, concatenation_character, list_store, answer_store, mock_schema + concatenation_type, + concatenation_character, + answer_store, + mock_schema, ): # Given answer_store.add_or_update(Answer(answer_id="age", value=7)) @@ -300,15 +286,12 @@ def test_concatenate_number_and_checkbox_answers( # When question = Question( question_schema, - answer_store=answer_store, schema=mock_schema, - rule_evaluator=get_rule_evaluator(answer_store, list_store, mock_schema), - value_source_resolver=get_value_source_resolver( - answer_store, list_store, mock_schema - ), + data_stores=DataStores(answer_store=answer_store), location=None, block_id="house-type", - return_to=None, + return_location=ReturnLocation(), + language="en", ) # Then @@ -320,7 +303,10 @@ def test_concatenate_number_and_checkbox_answers( @pytest.mark.usefixtures("app") -def test_merge_date_range_answers(answer_store, list_store, mock_schema): +def test_merge_date_range_answers( + answer_store, + mock_schema, +): # Given answer_store.add_or_update(Answer(answer_id="answer_1", value="13/02/2016")) answer_store.add_or_update(Answer(answer_id="answer_2", value="13/09/2016")) @@ -338,15 +324,12 @@ def test_merge_date_range_answers(answer_store, list_store, mock_schema): # When question = Question( question_schema, - answer_store=answer_store, + data_stores=DataStores(answer_store=answer_store), schema=mock_schema, - rule_evaluator=get_rule_evaluator(answer_store, list_store, mock_schema), - value_source_resolver=get_value_source_resolver( - answer_store, list_store, mock_schema - ), location=None, block_id="house-type", - return_to=None, + return_location=ReturnLocation(), + language="en", ) # Then @@ -356,7 +339,10 @@ def test_merge_date_range_answers(answer_store, list_store, mock_schema): @pytest.mark.usefixtures("app") -def test_merge_multiple_date_range_answers(answer_store, list_store, mock_schema): +def test_merge_multiple_date_range_answers( + answer_store, + mock_schema, +): # Given for answer in ( Answer(answer_id="answer_1", value="13/02/2016"), @@ -381,15 +367,12 @@ def test_merge_multiple_date_range_answers(answer_store, list_store, mock_schema # When question = Question( question_schema, - answer_store=answer_store, + data_stores=DataStores(answer_store=answer_store), schema=mock_schema, - rule_evaluator=get_rule_evaluator(answer_store, list_store, mock_schema), - value_source_resolver=get_value_source_resolver( - answer_store, list_store, mock_schema - ), location=None, block_id="house-type", - return_to=None, + return_location=ReturnLocation(), + language="en", ) # Then @@ -401,7 +384,10 @@ def test_merge_multiple_date_range_answers(answer_store, list_store, mock_schema @pytest.mark.usefixtures("app") -def test_create_question_with_multiple_answers(answer_store, list_store, mock_schema): +def test_create_question_with_multiple_answers( + answer_store, + mock_schema, +): # Given for answer in ( Answer(answer_id="answer_1", value="Han"), @@ -422,15 +408,12 @@ def test_create_question_with_multiple_answers(answer_store, list_store, mock_sc # When question = Question( question_schema, - answer_store=answer_store, + data_stores=DataStores(answer_store=answer_store), schema=mock_schema, - rule_evaluator=get_rule_evaluator(answer_store, list_store, mock_schema), - value_source_resolver=get_value_source_resolver( - answer_store, list_store, mock_schema - ), location=None, block_id="house-type", - return_to=None, + return_location=ReturnLocation(), + language="en", ) # Then @@ -440,7 +423,10 @@ def test_create_question_with_multiple_answers(answer_store, list_store, mock_sc @pytest.mark.usefixtures("app") -def test_checkbox_button_options(answer_store, list_store, mock_schema): +def test_checkbox_button_options( + answer_store, + mock_schema, +): # Given answer_store.add_or_update( Answer(answer_id="answer_1", value=["Light Side", "Dark Side"]) @@ -466,15 +452,12 @@ def test_checkbox_button_options(answer_store, list_store, mock_schema): # When question = Question( question_schema, - answer_store=answer_store, + data_stores=DataStores(answer_store=answer_store), schema=mock_schema, - rule_evaluator=get_rule_evaluator(answer_store, list_store, mock_schema), - value_source_resolver=get_value_source_resolver( - answer_store, list_store, mock_schema - ), location=None, block_id="house-type", - return_to=None, + return_location=ReturnLocation(), + language="en", ) # Then @@ -484,7 +467,10 @@ def test_checkbox_button_options(answer_store, list_store, mock_schema): @pytest.mark.usefixtures("app") -def test_checkbox_button_detail_answer_empty(answer_store, list_store, mock_schema): +def test_checkbox_button_detail_answer_empty( + answer_store, + mock_schema, +): # Given answer_store.add_or_update(Answer(answer_id="answer_1", value=["other", ""])) @@ -512,15 +498,12 @@ def test_checkbox_button_detail_answer_empty(answer_store, list_store, mock_sche # When question = Question( question_schema, - answer_store=answer_store, + data_stores=DataStores(answer_store=answer_store), schema=mock_schema, - rule_evaluator=get_rule_evaluator(answer_store, list_store, mock_schema), - value_source_resolver=get_value_source_resolver( - answer_store, list_store, mock_schema - ), location=None, block_id="house-type", - return_to=None, + return_location=ReturnLocation(), + language="en", ) # Then @@ -573,7 +556,6 @@ def test_checkbox_answer_with_detail_answer_returns_the_value( expected_len, expected_value, answer_store, - list_store, mock_schema, ): # Given @@ -597,15 +579,12 @@ def test_checkbox_answer_with_detail_answer_returns_the_value( # When question = Question( question_schema, - answer_store=answer_store, + data_stores=DataStores(answer_store=answer_store), schema=mock_schema, - rule_evaluator=get_rule_evaluator(answer_store, list_store, mock_schema), - value_source_resolver=get_value_source_resolver( - answer_store, list_store, mock_schema - ), location=None, block_id="house-type", - return_to=None, + return_location=ReturnLocation(), + language="en", ) # Then @@ -614,7 +593,10 @@ def test_checkbox_answer_with_detail_answer_returns_the_value( @pytest.mark.usefixtures("app") -def test_checkbox_button_other_option_text(answer_store, list_store, mock_schema): +def test_checkbox_button_other_option_text( + answer_store, + mock_schema, +): # Given answer_store.add_or_update( Answer(answer_id="answer_1", value=["Light Side", "other"]) @@ -645,15 +627,12 @@ def test_checkbox_button_other_option_text(answer_store, list_store, mock_schema # When question = Question( question_schema, - answer_store=answer_store, + data_stores=DataStores(answer_store=answer_store), schema=mock_schema, - rule_evaluator=get_rule_evaluator(answer_store, list_store, mock_schema), - value_source_resolver=get_value_source_resolver( - answer_store, list_store, mock_schema - ), location=None, block_id="house-type", - return_to=None, + return_location=ReturnLocation(), + language="en", ) # Then @@ -700,7 +679,12 @@ def test_checkbox_button_other_option_text(answer_store, list_store, mock_schema ), ) def test_radio_answer_with_detail_answers_returns_correct_value( - answer_type, options, answers, expected, answer_store, list_store, mock_schema + answer_type, + options, + answers, + expected, + answer_store, + mock_schema, ): # Given for answer in answers: @@ -723,15 +707,12 @@ def test_radio_answer_with_detail_answers_returns_correct_value( # When question = Question( question_schema, - answer_store=answer_store, + data_stores=DataStores(answer_store=answer_store), schema=mock_schema, - rule_evaluator=get_rule_evaluator(answer_store, list_store, mock_schema), - value_source_resolver=get_value_source_resolver( - answer_store, list_store, mock_schema - ), location=None, block_id="house-type", - return_to=None, + return_location=ReturnLocation(), + language="en", ) # Then @@ -772,9 +753,13 @@ def test_radio_answer_with_detail_answers_returns_correct_value( ), ) def test_answer_types_selected_option_label( - answer_type, options, answers, expected, answer_store, list_store, mock_schema + answer_type, + options, + answers, + expected, + answer_store, + mock_schema, ): - for answer in answers: answer_store.add_or_update(answer) @@ -795,15 +780,12 @@ def test_answer_types_selected_option_label( # When question = Question( question_schema, - answer_store=answer_store, + data_stores=DataStores(answer_store=answer_store), schema=mock_schema, - rule_evaluator=get_rule_evaluator(answer_store, list_store, mock_schema), - value_source_resolver=get_value_source_resolver( - answer_store, list_store, mock_schema - ), location=None, block_id="house-type", - return_to=None, + return_location=ReturnLocation(), + language="en", ) # Then @@ -812,7 +794,9 @@ def test_answer_types_selected_option_label( @pytest.mark.usefixtures("app") def test_dynamic_checkbox_answer_options( - answer_store, list_store, mock_schema, dynamic_answer_options_schema + answer_store, + mock_schema, + dynamic_answer_options_schema, ): # Given answer_schema = { @@ -837,17 +821,14 @@ def test_dynamic_checkbox_answer_options( # When question = Question( question_schema, - answer_store=answer_store, - schema=mock_schema, - rule_evaluator=get_rule_evaluator( - answer_store, list_store, mock_schema, response_metadata - ), - value_source_resolver=get_value_source_resolver( - answer_store, list_store, mock_schema, response_metadata + data_stores=DataStores( + answer_store=answer_store, response_metadata=response_metadata ), + schema=mock_schema, location=None, block_id="house-type", - return_to=None, + return_location=ReturnLocation(), + language="en", ) # Then @@ -888,14 +869,13 @@ def test_dynamic_checkbox_answer_options( ) def test_dynamic_answer_options( answer_type, - list_store, answer_store_value, expected, dynamic_answer_options_schema, mock_schema, ): # Given - answer_id = (f"dynamic-{answer_type.lower()}-answer",) + answer_id = f"dynamic-{answer_type.lower()}-answer" answer_schema = { "id": answer_id, "label": "Some label", @@ -909,17 +889,14 @@ def test_dynamic_answer_options( # When question = Question( question_schema, - answer_store=answer_store, - schema=mock_schema, - rule_evaluator=get_rule_evaluator( - answer_store, list_store, mock_schema, response_metadata - ), - value_source_resolver=get_value_source_resolver( - answer_store, list_store, mock_schema, response_metadata + data_stores=DataStores( + answer_store=answer_store, response_metadata=response_metadata ), + schema=mock_schema, location=None, block_id="house-type", - return_to=None, + return_location=ReturnLocation(), + language="en", ) # Then @@ -962,7 +939,11 @@ def test_dynamic_answer_options( ), ), ) -def test_get_answer(answer_schema, answer_store, expected, list_store): +def test_get_answer( + answer_schema, + answer_store, + expected, +): schema = address_questionnaire_schema("Newline") # Given @@ -971,16 +952,160 @@ def test_get_answer(answer_schema, answer_store, expected, list_store): # When question = Question( question_schema, - answer_store=answer_store, + data_stores=DataStores(answer_store=answer_store), schema=schema, - rule_evaluator=get_rule_evaluator(answer_store, list_store, schema), - value_source_resolver=get_value_source_resolver( - answer_store, list_store, schema - ), location=None, block_id="address-group", - return_to=None, + return_location=ReturnLocation(), + language="en", ) # Then assert question.get_answer(answer_store, "building") == expected + + +@pytest.mark.usefixtures("app") +@pytest.mark.parametrize( + "answer_store, list_store, expected", + ( + ( + AnswerStore( + [ + {"answer_id": "any-supermarket-answer", "value": "Yes"}, + { + "answer_id": "supermarket-name", + "value": "Tesco", + "list_item_id": "awTNTI", + }, + { + "answer_id": "supermarket-name", + "value": "Aldi", + "list_item_id": "FMOByU", + }, + {"answer_id": "list-collector-answer", "value": "No"}, + { + "answer_id": "based-checkbox-answer", + "value": ["Non UK based supermarkets"], + }, + { + "answer_id": "percentage-of-shopping", + "value": 12, + "list_item_id": "awTNTI", + }, + { + "answer_id": "percentage-of-shopping", + "value": 21, + "list_item_id": "FMOByU", + }, + ], + ), + ListStore([{"items": ["awTNTI", "FMOByU"], "name": "supermarkets"}]), + [ + { + "currency": None, + "id": "percentage-of-shopping-awTNTI", + "label": "Percentage of shopping at Tesco", + "link": "/questionnaire/group/?list_item_id=awTNTI#percentage-of-shopping-awTNTI", + "type": "percentage", + "unit": None, + "unit_length": None, + "value": 12, + "decimal_places": 0, + }, + { + "currency": None, + "id": "percentage-of-shopping-FMOByU", + "label": "Percentage of shopping at Aldi", + "link": "/questionnaire/group/?list_item_id=FMOByU#percentage-of-shopping-FMOByU", + "type": "percentage", + "unit": None, + "unit_length": None, + "value": 21, + "decimal_places": 0, + }, + { + "currency": None, + "id": "based-checkbox-answer", + "label": "Are supermarkets UK or non UK based?", + "link": "/questionnaire/group/#based-checkbox-answer", + "type": "checkbox", + "unit": None, + "unit_length": None, + "value": [ + { + "detail_answer_value": None, + "label": "Non UK based supermarkets", + } + ], + "decimal_places": None, + }, + ], + ), + ), +) +def test_dynamic_answers(expected, list_store, answer_store): + schema = load_schema_from_name("test_dynamic_answers_list_source", "en") + + # Given + question_schema = { + "dynamic_answers": { + "values": {"source": "list", "identifier": "supermarkets"}, + "answers": [ + { + "label": { + "text": "Percentage of shopping at {transformed_value}", + "placeholders": [ + { + "placeholder": "transformed_value", + "value": { + "source": "answers", + "identifier": "supermarket-name", + }, + } + ], + }, + "id": "percentage-of-shopping", + "mandatory": False, + "type": "Percentage", + "maximum": {"value": 100}, + "decimal_places": 0, + } + ], + }, + "answers": [ + { + "id": "based-checkbox-answer", + "label": "Are supermarkets UK or non UK based?", + "instruction": "Select any answers that apply", + "mandatory": False, + "options": [ + { + "label": "UK based supermarkets", + "value": "UK based supermarkets", + }, + { + "label": "Non UK based supermarkets", + "value": "Non UK based supermarkets", + }, + ], + "type": "Checkbox", + } + ], + "id": "dynamic-answer-question", + "title": "What percent of your shopping do you do at each of the following supermarket?", + "type": "General", + } + + # When + question = Question( + question_schema, + data_stores=DataStores(answer_store=answer_store, list_store=list_store), + schema=schema, + location=None, + block_id="group", + return_location=ReturnLocation(), + language="en", + ) + + # Then + assert question.answers == expected diff --git a/tests/app/views/contexts/test_calculated_summary_context.py b/tests/app/views/contexts/test_calculated_summary_context.py index 43eec72b9c..7d8e37ca6b 100644 --- a/tests/app/views/contexts/test_calculated_summary_context.py +++ b/tests/app/views/contexts/test_calculated_summary_context.py @@ -1,37 +1,39 @@ import pytest -from app.data_models.answer_store import AnswerStore -from app.data_models.list_store import ListStore -from app.data_models.progress_store import ProgressStore -from app.questionnaire.location import Location -from app.questionnaire.questionnaire_schema import DEFAULT_LANGUAGE_CODE +from app.data_models import AnswerStore, ListStore +from app.data_models.data_stores import DataStores +from app.questionnaire import Location +from app.questionnaire.placeholder_renderer import PlaceholderRenderer +from app.questionnaire.return_location import ReturnLocation from app.questionnaire.routing_path import RoutingPath -from app.utilities.schema import load_schema_from_name from app.views.contexts.calculated_summary_context import CalculatedSummaryContext -from app.views.contexts.section_summary_context import SectionSummaryContext from tests.app.views.contexts import assert_summary_context # pylint: disable=too-many-locals @pytest.mark.usefixtures("app") @pytest.mark.parametrize( - "block_id, locale, language, title, value, total_blocks", + "block_id, locale, language, title, value, total_blocks, return_to_answer_id, skip_fourth", ( ( - "currency-total-playback-with-fourth", + "currency-total-playback", "en_GB", "en", - "We calculate the total of currency values entered to be ÂŖ27.00. Is this correct? (With Fourth)", + "We calculate the total of currency values entered to be ÂŖ27.00. Is this correct?", "ÂŖ27.00", 5, + "first-number-answer", + False, ), ( - "currency-total-playback-skipped-fourth", + "currency-total-playback", "en_GB", "en", - "We calculate the total of currency values entered to be ÂŖ12.00. Is this correct? (Skipped Fourth)", + "We calculate the total of currency values entered to be ÂŖ12.00. Is this correct?", "ÂŖ12.00", 3, + "first-number-answer", + True, ), ( "unit-total-playback", @@ -40,6 +42,8 @@ "We calculate the total of unit values entered to be 9 cm. Is this correct?", "9 cm", 2, + "second-number-answer-unit-total", + False, ), ( "percentage-total-playback", @@ -48,6 +52,8 @@ "We calculate the total of percentage values entered to be 20%. Is this correct?", "20%", 2, + "fifth-percent-answer", + False, ), ( "number-total-playback", @@ -56,6 +62,8 @@ "We calculate the total of number values entered to be 22. Is this correct?", "22", 2, + "fifth-number-answer", + False, ), ), ) @@ -68,31 +76,73 @@ def test_build_view_context_for_currency_calculated_summary( total_blocks, test_calculated_summary_schema, test_calculated_summary_answers, - list_store, - progress_store, + test_calculated_summary_answers_skipped_fourth, mocker, + return_to_answer_id, + skip_fourth, ): mocker.patch( "app.jinja_filters.flask_babel.get_locale", mocker.MagicMock(return_value=locale), ) + block_ids = ( + [ + "first-number-block", + "second-number-block", + "third-number-block", + "third-and-a-half-number-block", + "skip-fourth-block", + "fifth-number-block", + "sixth-number-block", + ] + if skip_fourth + else [ + "first-number-block", + "second-number-block", + "third-number-block", + "third-and-a-half-number-block", + "skip-fourth-block", + "fourth-number-block", + "fourth-and-a-half-number-block", + "fifth-number-block", + "sixth-number-block", + ] + ) + current_location = Location(section_id="default-section", block_id=block_id) + data_stores = DataStores( + answer_store=( + test_calculated_summary_answers_skipped_fourth + if skip_fourth + else test_calculated_summary_answers + ) + ) + block = test_calculated_summary_schema.get_block(block_id) - calculated_summary_context = CalculatedSummaryContext( - language, - test_calculated_summary_schema, - test_calculated_summary_answers, - list_store, - progress_store, - metadata={}, - response_metadata={}, + placeholder_renderer = PlaceholderRenderer( + language=language, + data_stores=data_stores, + schema=test_calculated_summary_schema, + location=current_location, ) - context = calculated_summary_context.build_view_context_for_calculated_summary( - current_location + rendered_block = placeholder_renderer.render( + data_to_render=block, list_item_id=current_location.list_item_id ) + calculated_summary_context = CalculatedSummaryContext( + language=language, + schema=test_calculated_summary_schema, + data_stores=data_stores, + routing_path=RoutingPath(section_id="default-section", block_ids=block_ids), + current_location=current_location, + return_location=ReturnLocation(return_to_answer_id=return_to_answer_id), + rendered_block=rendered_block, + ) + + context = calculated_summary_context.build_view_context() + assert "summary" in context assert_summary_context(context) assert len(context["summary"]) == 6 @@ -101,329 +151,349 @@ def test_build_view_context_for_currency_calculated_summary( assert context_summary["title"] == title assert "calculated_question" in context_summary - assert len(context_summary["groups"][0]["blocks"]) == total_blocks + assert len(context_summary["sections"][0]["groups"][0]["blocks"]) == total_blocks assert ( context_summary["calculated_question"]["title"] == "Grand total of previous values" ) assert context_summary["calculated_question"]["answers"][0]["value"] == value + answer_change_link = context_summary["sections"][0]["groups"][0]["blocks"][0][ + "question" + ]["answers"][0]["link"] + assert "return_to=calculated-summary" in answer_change_link + assert f"return_to_answer_id={return_to_answer_id}" in answer_change_link + assert f"return_to_block_id={block_id}" in answer_change_link + @pytest.mark.usefixtures("app") -def test_context_for_section_list_summary(people_answer_store): - schema = load_schema_from_name("test_list_collector_section_summary") - - summary_context = SectionSummaryContext( - language=DEFAULT_LANGUAGE_CODE, - schema=schema, - answer_store=people_answer_store, - list_store=ListStore( - [ - {"items": ["PlwgoG", "UHPLbX"], "name": "people"}, - {"items": ["gTrlio"], "name": "visitors"}, - ] +@pytest.mark.parametrize( + "block_id, return_to_answer_id, return_to, return_to_block_id", + ( + ( + "distance-calculated-summary-1", + "q1-a1", + "grand-calculated-summary", + "distance-grand-calculated-summary", ), - progress_store=ProgressStore(), - metadata={"display_address": "70 Abingdon Road, Goathill"}, - response_metadata={}, - current_location=Location(section_id="section"), - routing_path=RoutingPath( - [ - "primary-person-list-collector", - "list-collector", - "visitor-list-collector", - ], - section_id="section", + ( + "number-calculated-summary-1", + "q1-a2", + "grand-calculated-summary", + "number-grand-calculated-summary", + ), + ( + "distance-calculated-summary-2", + "q3-a1", + "grand-calculated-summary", + "distance-grand-calculated-summary", + ), + ( + "number-calculated-summary-2", + "q3-a2", + "grand-calculated-summary", + "number-grand-calculated-summary", ), + ), +) +def test_build_view_context_for_return_to_calculated_summary( + test_grand_calculated_summary_schema, + test_grand_calculated_summary_answers, + mocker, + block_id, + return_to_answer_id, + return_to, + return_to_block_id, +): + """ + Tests the change answer links for a calculated summary that has been reached by a change link on a grand calculated summary + """ + mocker.patch( + "app.jinja_filters.flask_babel.get_locale", + mocker.MagicMock(return_value="en_GB"), ) - context = summary_context() - expected = { - "summary": { - "answers_are_editable": True, - "collapsible": False, - "custom_summary": [ - { - "add_link": "/questionnaire/people/add-person/?return_to=section-summary", - "add_link_text": "Add someone to this household", - "empty_list_text": "There are no householders", - "list": { - "editable": True, - "list_items": [ - { - "edit_link": "/questionnaire/people/PlwgoG/edit-person/?return_to=section-summary", - "item_title": "Toni Morrison", - "primary_person": False, - "remove_link": "/questionnaire/people/PlwgoG/remove-person/?return_to=section-summary", - "list_item_id": "PlwgoG", - }, - { - "edit_link": "/questionnaire/people/UHPLbX/edit-person/?return_to=section-summary", - "item_title": "Barry Pheloung", - "primary_person": False, - "remove_link": "/questionnaire/people/UHPLbX/remove-person/?return_to=section-summary", - "list_item_id": "UHPLbX", - }, - ], - }, - "list_name": "people", - "title": "Household members staying overnight on 13 October 2019 at 70 Abingdon Road, Goathill", - "type": "List", - }, - { - "add_link": "/questionnaire/visitors/add-visitor/?return_to=section-summary", - "add_link_text": "Add another visitor to this household", - "empty_list_text": "There are no visitors", - "list": { - "editable": True, - "list_items": [ - { - "edit_link": "/questionnaire/visitors/gTrlio/edit-visitor-person/?return_to=section-summary", - "item_title": "", - "primary_person": False, - "remove_link": "/questionnaire/visitors/gTrlio/remove-visitor/?return_to=section-summary", - "list_item_id": "gTrlio", - } - ], - }, - "list_name": "visitors", - "title": "Visitors staying overnight on 13 October 2019 at 70 Abingdon Road, Goathill", - "type": "List", - }, - ], - "page_title": "People who live here and overnight visitors", - "summary_type": "SectionSummary", - "title": "People who live here and overnight visitors", - } - } - assert context == expected + block_ids = [ + "first-number-block", + "second-number-block", + "distance-calculated-summary-1", + "number-calculated-summary-1", + "third-number-block", + "fourth-number-block", + "distance-calculated-summary-2", + "number-calculated-summary-2", + ] + current_location = Location(section_id="default-section", block_id=block_id) + data_stores = DataStores(answer_store=test_grand_calculated_summary_answers) + block = test_grand_calculated_summary_schema.get_block(block_id) + language = "en" + + placeholder_renderer = PlaceholderRenderer( + language=language, + data_stores=data_stores, + schema=test_grand_calculated_summary_schema, + location=current_location, + ) -@pytest.mark.usefixtures("app") -def test_context_for_driving_question_summary_empty_list(): - schema = load_schema_from_name("test_list_collector_driving_question") - - summary_context = SectionSummaryContext( - DEFAULT_LANGUAGE_CODE, - schema, - AnswerStore([{"answer_id": "anyone-usually-live-at-answer", "value": "No"}]), - ListStore(), - ProgressStore(), - metadata={}, - response_metadata={}, - current_location=Location(section_id="section"), - routing_path=RoutingPath(["anyone-usually-live-at"], section_id="section"), + rendered_block = placeholder_renderer.render( + data_to_render=block, list_item_id=current_location.list_item_id ) - context = summary_context() - expected = { - "summary": { - "answers_are_editable": True, - "collapsible": False, - "custom_summary": [ - { - "add_link": "/questionnaire/anyone-usually-live-at/?return_to=section-summary", - "add_link_text": "Add someone to this household", - "empty_list_text": "There are no householders", - "list": {"editable": False, "list_items": []}, - "list_name": "people", - "title": "Household members", - "type": "List", - } - ], - "page_title": "List Collector Driving Question Summary", - "summary_type": "SectionSummary", - "title": "List Collector Driving Question Summary", - } - } + calculated_summary_context = CalculatedSummaryContext( + language=language, + schema=test_grand_calculated_summary_schema, + data_stores=data_stores, + routing_path=RoutingPath(section_id="default-section", block_ids=block_ids), + current_location=current_location, + return_location=ReturnLocation( + return_to=return_to, return_to_block_id=return_to_block_id + ), + rendered_block=rendered_block, + ) - assert context == expected + context = calculated_summary_context.build_view_context() + assert "summary" in context + assert_summary_context(context) + context_summary = context["summary"] + answer_change_link = context_summary["sections"][0]["groups"][0]["blocks"][0][ + "question" + ]["answers"][0]["link"] + assert f"return_to=calculated-summary,{return_to}" in answer_change_link + assert f"return_to_answer_id={return_to_answer_id}" in answer_change_link + assert f"return_to_block_id={block_id},{return_to_block_id}" in answer_change_link -@pytest.mark.usefixtures("app") -def test_context_for_driving_question_summary(): - schema = load_schema_from_name("test_list_collector_driving_question") - summary_context = SectionSummaryContext( - DEFAULT_LANGUAGE_CODE, - schema, - AnswerStore( +@pytest.mark.usefixtures("app") +@pytest.mark.parametrize( + "block_id,expected_answer_ids,expected_block_ids", + ( + ( + "calculated-summary-spending", [ - {"answer_id": "anyone-usually-live-at-answer", "value": "Yes"}, - {"answer_id": "first-name", "value": "Toni", "list_item_id": "PlwgoG"}, - { - "answer_id": "last-name", - "value": "Morrison", - "list_item_id": "PlwgoG", - }, - ] + "cost-of-shopping-CHKtQS", + "cost-of-shopping-laFWcs", + "cost-of-other-CHKtQS", + "cost-of-other-laFWcs", + "extra-static-answer", + ], + ["dynamic-answer", "extra-spending-block"], ), - ListStore([{"items": ["PlwgoG"], "name": "people"}]), - ProgressStore(), - metadata={}, - response_metadata={}, - current_location=Location(section_id="section"), - routing_path=RoutingPath( - ["anyone-usually-live-at", "anyone-else-live-at"], section_id="section" + ( + "calculated-summary-visits", + [ + "days-a-week-CHKtQS", + "days-a-week-laFWcs", + ], + ["dynamic-answer"], ), + ), +) +def test_build_view_context_for_calculated_summary_with_dynamic_answers( + test_calculated_summary_repeating_and_static_answers_schema, + mocker, + block_id, + expected_answer_ids, + expected_block_ids, +): + """ + Tests that when different dynamic answers for the same list are used in different calculated summaries + that the calculated summary context filters the answers to keep correctly. + """ + mocker.patch( + "app.jinja_filters.flask_babel.get_locale", + mocker.MagicMock(return_value="en_GB"), ) - context = summary_context() + block_ids = [ + "any-supermarket", + "list-collector", + "dynamic-answer", + "extra-spending-block", + ] - expected = { - "summary": { - "answers_are_editable": True, - "collapsible": False, - "custom_summary": [ - { - "add_link": "/questionnaire/people/add-person/?return_to=section-summary", - "add_link_text": "Add someone to this household", - "empty_list_text": "There are no householders", - "list": { - "editable": True, - "list_items": [ - { - "item_title": "Toni Morrison", - "primary_person": False, - "edit_link": "/questionnaire/people/PlwgoG/edit-person/?return_to=section-summary", - "remove_link": "/questionnaire/people/PlwgoG/remove-person/?return_to=section-summary", - "list_item_id": "PlwgoG", - } - ], - }, - "list_name": "people", - "title": "Household members", - "type": "List", - } - ], - "page_title": "List Collector Driving Question Summary", - "summary_type": "SectionSummary", - "title": "List Collector Driving Question Summary", - } - } + current_location = Location(section_id="section-1", block_id=block_id) + data_stores = DataStores( + list_store=ListStore([{"items": ["CHKtQS", "laFWcs"], "name": "supermarkets"}]) + ) + block = test_calculated_summary_repeating_and_static_answers_schema.get_block( + block_id + ) + language = "en" - assert context == expected + placeholder_renderer = PlaceholderRenderer( + language=language, + data_stores=data_stores, + schema=test_calculated_summary_repeating_and_static_answers_schema, + location=current_location, + ) + rendered_block = placeholder_renderer.render( + data_to_render=block, list_item_id=current_location.list_item_id + ) -@pytest.mark.usefixtures("app") -def test_titles_for_repeating_section_summary(people_answer_store, mocker): - schema = load_schema_from_name("test_repeating_sections_with_hub_and_spoke") - - section_summary_context = SectionSummaryContext( - DEFAULT_LANGUAGE_CODE, - schema, - people_answer_store, - ListStore( - [ - {"items": ["PlwgoG", "UHPLbX"], "name": "people"}, - {"items": ["gTrlio"], "name": "visitors"}, - ] - ), - ProgressStore(), - metadata={}, - response_metadata={}, - current_location=Location( - section_id="personal-details-section", - list_name="people", - list_item_id="PlwgoG", - ), - routing_path=mocker.MagicMock(), + calculated_summary_context = CalculatedSummaryContext( + language=language, + schema=test_calculated_summary_repeating_and_static_answers_schema, + data_stores=data_stores, + routing_path=RoutingPath(section_id="section-1", block_ids=block_ids), + current_location=current_location, + return_location=ReturnLocation(), + rendered_block=rendered_block, ) - context = section_summary_context() + context = calculated_summary_context.build_view_context() + assert "summary" in context + assert_summary_context(context) + context_summary = context["summary"] + calculation_blocks = context_summary["sections"][0]["groups"][0]["blocks"] + + block_ids = [block["id"] for block in calculation_blocks] + assert block_ids == expected_block_ids - assert context["summary"]["title"] == "Toni Morrison" + answers_to_keep = calculation_blocks[0]["question"]["answers"] + answer_ids = [answer["id"] for answer in answers_to_keep] + assert answer_ids == expected_answer_ids - section_summary_context = SectionSummaryContext( - DEFAULT_LANGUAGE_CODE, - schema, - people_answer_store, - ListStore( - [ - {"items": ["PlwgoG", "UHPLbX"], "name": "people"}, - {"items": ["gTrlio"], "name": "visitors"}, - ] - ), - ProgressStore(), - metadata={}, - response_metadata={}, - current_location=Location( - block_id="personal-summary", - section_id="personal-details-section", - list_name="people", - list_item_id="UHPLbX", - ), - routing_path=mocker.MagicMock(), + # blocks with dynamic answers show each answer suffixed with the list item id, so the anchor needs to also include it + assert all( + answer["link"].endswith( + f"return_to=calculated-summary&return_to_answer_id={answer['id']}&return_to_block_id={block_id}#{answer['id']}" + ) + for answer in answers_to_keep ) - context = section_summary_context() - assert context["summary"]["title"] == "Barry Pheloung" - @pytest.mark.usefixtures("app") -def test_primary_only_links_for_section_summary(people_answer_store): - schema = load_schema_from_name("test_list_collector_section_summary") - - summary_context = SectionSummaryContext( - language=DEFAULT_LANGUAGE_CODE, - schema=schema, - answer_store=people_answer_store, - list_store=ListStore( - [{"items": ["PlwgoG"], "name": "people", "primary_person": "PlwgoG"}] +@pytest.mark.parametrize( + "block_id,expected_answer_ids,expected_answer_labels,expected_block_ids,anchors", + ( + ( + "calculated-summary-spending", + [ + "answer-car", + "transport-cost-CHKtQS", + "transport-additional-cost-CHKtQS", + "transport-cost-laFWcs", + "transport-additional-cost-laFWcs", + ], + [ + "Monthly expenditure travelling by car", + "Monthly season ticket expenditure for travel by Train", + "Additional monthly expenditure for travel by Train", + "Monthly season ticket expenditure for travel by Bus", + "Additional monthly expenditure for travel by Bus", + ], + [ + "block-car", + "transport-repeating-block-1-CHKtQS", + "transport-repeating-block-1-laFWcs", + ], + [ + "answer-car", + "transport-cost", + "transport-additional-cost", + "transport-cost", + "transport-additional-cost", + ], ), - progress_store=ProgressStore(), - metadata={"display_address": "70 Abingdon Road, Goathill"}, - response_metadata={}, - current_location=Location(section_id="section"), - routing_path=RoutingPath( + ( + "calculated-summary-count", + ["transport-count-CHKtQS", "transport-count-laFWcs"], + ["Monthly journeys by Train", "Monthly journeys by Bus"], [ - "primary-person-list-collector", - "list-collector", - "visitor-list-collector", + "transport-repeating-block-2-CHKtQS", + "transport-repeating-block-2-laFWcs", ], - section_id="section", + ["transport-count", "transport-count"], ), + ), +) +def test_build_view_context_for_calculated_summary_with_answers_from_repeating_blocks( + test_calculated_summary_repeating_blocks, + mocker, + block_id, + expected_answer_ids, + expected_answer_labels, + expected_block_ids, + anchors, +): + """ + Tests that when different dynamic answers for the same list are used in different calculated summaries + that the calculated summary context filters the answers to keep correctly. + """ + mocker.patch( + "app.jinja_filters.flask_babel.get_locale", + mocker.MagicMock(return_value="en_GB"), ) - context = summary_context() - list_items = context["summary"]["custom_summary"][0]["list"]["list_items"] + block_ids = ["block-car", "list-collector"] - assert "/add-or-edit-primary-person/" in list_items[0]["edit_link"] - - -@pytest.mark.usefixtures("app") -def test_primary_links_for_section_summary(people_answer_store): - schema = load_schema_from_name("test_list_collector_section_summary") - - summary_context = SectionSummaryContext( - language=DEFAULT_LANGUAGE_CODE, - schema=schema, - answer_store=people_answer_store, - list_store=ListStore( + current_location = Location(section_id="section-1", block_id=block_id) + data_stores = DataStores( + answer_store=AnswerStore( [ { - "items": ["PlwgoG", "fg0sPd"], - "name": "people", - "primary_person": "PlwgoG", - } + "answer_id": "transport-name", + "value": "Train", + "list_item_id": "CHKtQS", + }, + { + "answer_id": "transport-name", + "value": "Bus", + "list_item_id": "laFWcs", + }, ] ), - progress_store=ProgressStore(), - metadata={"display_address": "70 Abingdon Road, Goathill"}, - response_metadata={}, - current_location=Location(section_id="section"), - routing_path=RoutingPath( - [ - "primary-person-list-collector", - "list-collector", - "visitor-list-collector", - ], - section_id="section", - ), + list_store=ListStore([{"items": ["CHKtQS", "laFWcs"], "name": "transport"}]), + ) + block = test_calculated_summary_repeating_blocks.get_block(block_id) + language = "en" + + placeholder_renderer = PlaceholderRenderer( + language=language, + data_stores=data_stores, + schema=test_calculated_summary_repeating_blocks, + location=current_location, ) - context = summary_context() - list_items = context["summary"]["custom_summary"][0]["list"]["list_items"] + rendered_block = placeholder_renderer.render( + data_to_render=block, list_item_id=current_location.list_item_id + ) - assert "/edit-person/" in list_items[0]["edit_link"] - assert "/edit-person/" in list_items[1]["edit_link"] + calculated_summary_context = CalculatedSummaryContext( + language=language, + schema=test_calculated_summary_repeating_blocks, + data_stores=data_stores, + routing_path=RoutingPath(section_id="section-1", block_ids=block_ids), + current_location=current_location, + return_location=ReturnLocation(), + rendered_block=rendered_block, + ) + + context = calculated_summary_context.build_view_context() + assert "summary" in context + assert_summary_context(context) + context_summary = context["summary"] + calculation_blocks = context_summary["sections"][0]["groups"][0]["blocks"] + + block_ids = [block["id"] for block in calculation_blocks] + assert block_ids == expected_block_ids + + questions = [block["question"] for block in calculation_blocks] + answers = [answer for question in questions for answer in question["answers"]] + answer_ids = [answer["id"] for answer in answers] + assert answer_ids == expected_answer_ids + + answer_labels = [answer["label"] for answer in answers] + assert answer_labels == expected_answer_labels + + # on summary pages, repeating block answer ids are suffixed with list item ids, + # but the anchor on the change links needs to not have them, because the repeating block itself doesn't do that + assert all( + answer["link"].endswith( + f"return_to=calculated-summary&return_to_answer_id={answer['id']}&return_to_block_id={block_id}#{anchor}" + ) + for anchor, answer in zip(anchors, answers) + ) diff --git a/tests/app/views/contexts/test_grand_calculated_summary_context.py b/tests/app/views/contexts/test_grand_calculated_summary_context.py new file mode 100644 index 0000000000..ee9fe856cd --- /dev/null +++ b/tests/app/views/contexts/test_grand_calculated_summary_context.py @@ -0,0 +1,130 @@ +import pytest + +from app.data_models import CompletionStatus +from app.data_models.data_stores import DataStores +from app.data_models.progress_store import ProgressStore +from app.questionnaire import Location +from app.questionnaire.placeholder_renderer import PlaceholderRenderer +from app.questionnaire.return_location import ReturnLocation +from app.questionnaire.routing_path import RoutingPath +from app.views.contexts.grand_calculated_summary_context import ( + GrandCalculatedSummaryContext, +) +from tests.app.views.contexts import assert_summary_context + + +@pytest.mark.usefixtures("app") +@pytest.mark.parametrize( + "block_id, title, value, return_to_answer_id", + ( + ( + "distance-grand-calculated-summary", + "We calculate the grand total weekly distance travelled to be 100 mi. Is this correct?", + "100 mi", + "distance-calculated-summary-1", + ), + ( + "number-grand-calculated-summary", + "We calculate the grand total journeys per week to be 10. Is this correct?", + "10", + "number-calculated-summary-1", + ), + ), +) +# pylint: disable=too-many-locals +def test_build_view_context_for_grand_calculated_summary( + block_id, + title, + value, + test_grand_calculated_summary_schema, + test_grand_calculated_summary_answers, + mocker, + return_to_answer_id, +): + mocker.patch( + "app.jinja_filters.flask_babel.get_locale", + mocker.MagicMock(return_value="en_GB"), + ) + + block_ids = [ + "first-number-block", + "second-number-block", + "distance-calculated-summary-1", + "number-calculated-summary-1", + "third-number-block", + "fourth-number-block", + "distance-calculated-summary-2", + "number-calculated-summary-2", + ] + + current_location = Location(section_id="default-section", block_id=block_id) + data_stores = DataStores( + answer_store=test_grand_calculated_summary_answers, + progress_store=ProgressStore( + progress=[ + { + "section_id": "section-1", + "status": CompletionStatus.COMPLETED, + "block_ids": [ + "first-number-block", + "second-number-block", + "distance-calculated-summary-1", + "number-calculated-summary-1", + ], + }, + { + "section_id": "section-2", + "status": CompletionStatus.COMPLETED, + "block_ids": [ + "third-number-block", + "fourth-number-block", + "distance-calculated-summary-2", + "number-calculated-summary-2", + ], + }, + ] + ), + ) + block = test_grand_calculated_summary_schema.get_block(block_id) + language = "en" + + placeholder_renderer = PlaceholderRenderer( + language=language, + data_stores=data_stores, + schema=test_grand_calculated_summary_schema, + location=current_location, + ) + + rendered_block = placeholder_renderer.render( + data_to_render=block, list_item_id=current_location.list_item_id + ) + + grand_calculated_summary_context = GrandCalculatedSummaryContext( + language=language, + schema=test_grand_calculated_summary_schema, + data_stores=data_stores, + routing_path=RoutingPath(section_id="default-section", block_ids=block_ids), + current_location=current_location, + return_location=ReturnLocation(), + rendered_block=rendered_block, + ) + + context = grand_calculated_summary_context.build_view_context() + + assert "summary" in context + assert_summary_context(context, "calculated_summary") + assert len(context["summary"]) == 6 + context_summary = context["summary"] + assert context_summary.get("title") == title + + assert "calculated_question" in context_summary + assert context_summary["calculated_question"]["answers"][0]["value"] == value + + calculated_summary_change_link = context_summary["sections"][0]["groups"][0][ + "blocks" + ][0]["calculated_summary"]["answers"][0]["link"] + assert "return_to=grand-calculated-summary" in calculated_summary_change_link + assert ( + f"return_to_answer_id={return_to_answer_id}" in calculated_summary_change_link + ) + assert f"return_to_block_id={block_id}" in calculated_summary_change_link diff --git a/tests/app/views/contexts/test_hub_context.py b/tests/app/views/contexts/test_hub_context.py index 3fe5da2755..c92db34d87 100644 --- a/tests/app/views/contexts/test_hub_context.py +++ b/tests/app/views/contexts/test_hub_context.py @@ -1,27 +1,18 @@ # pylint: disable=redefined-outer-name import pytest -from app.data_models.progress_store import CompletionStatus +from app.data_models import CompletionStatus from app.questionnaire.router import Router from app.utilities.schema import load_schema_from_name from app.views.contexts import HubContext @pytest.fixture -def router(schema, answer_store, list_store, progress_store): - return Router( - schema, - answer_store, - list_store, - progress_store, - metadata={}, - response_metadata={}, - ) +def router(schema, data_stores): + return Router(schema, data_stores) -def test_get_not_started_row_for_section( - schema, progress_store, answer_store, list_store -): +def test_get_not_started_row_for_section(schema, data_stores): expected = { "rowItems": [ { @@ -32,7 +23,7 @@ def test_get_not_started_row_for_section( "actions": [ { "text": "Start section", - "ariaLabel": "Start Breakfast section", + "visuallyHiddenText": "Start section: Breakfast", "url": "http://some/url", "attributes": {"data-qa": "hub-row-section-1-link"}, } @@ -41,15 +32,7 @@ def test_get_not_started_row_for_section( ] } - hub = HubContext( - language=None, - progress_store=progress_store, - list_store=list_store, - schema=schema, - answer_store=answer_store, - metadata={}, - response_metadata={}, - ) + hub = HubContext(language=None, schema=schema, data_stores=data_stores) actual = hub.get_row_context_for_section( section_name="Breakfast", @@ -61,9 +44,39 @@ def test_get_not_started_row_for_section( assert expected == actual -def test_get_completed_row_for_section( - schema, progress_store, answer_store, list_store -): +def test_get_in_progress_row_for_section(schema, data_stores): + expected = { + "rowItems": [ + { + "rowTitle": "Breakfast", + "rowTitleAttributes": {"data-qa": "hub-row-section-1-title"}, + "attributes": {"data-qa": "hub-row-section-1-state"}, + "valueList": [{"text": "Partially completed"}], + "actions": [ + { + "text": "Continue with section", + "visuallyHiddenText": "Continue with section: Breakfast", + "url": "http://some/url", + "attributes": {"data-qa": "hub-row-section-1-link"}, + } + ], + } + ] + } + + hub = HubContext(language=None, schema=schema, data_stores=data_stores) + + actual = hub.get_row_context_for_section( + section_name="Breakfast", + section_status=CompletionStatus.IN_PROGRESS, + section_url="http://some/url", + row_id="section-1", + ) + + assert expected == actual + + +def test_get_completed_row_for_section(schema, data_stores): expected = { "rowItems": [ { @@ -75,7 +88,7 @@ def test_get_completed_row_for_section( "actions": [ { "text": "View answers", - "ariaLabel": "View answers for Breakfast", + "visuallyHiddenText": "View answers: Breakfast", "url": "http://some/url", "attributes": {"data-qa": "hub-row-section-1-link"}, } @@ -84,15 +97,7 @@ def test_get_completed_row_for_section( ] } - hub = HubContext( - language=None, - progress_store=progress_store, - list_store=list_store, - schema=schema, - answer_store=answer_store, - metadata={}, - response_metadata={}, - ) + hub = HubContext(language=None, schema=schema, data_stores=data_stores) actual = hub.get_row_context_for_section( section_name="Breakfast", @@ -104,17 +109,9 @@ def test_get_completed_row_for_section( assert expected == actual -def test_get_context(progress_store, answer_store, list_store, router): +def test_get_context(router, data_stores): schema = load_schema_from_name("test_hub_and_spoke") - hub = HubContext( - language="en", - progress_store=progress_store, - list_store=list_store, - schema=schema, - answer_store=answer_store, - metadata={}, - response_metadata={}, - ) + hub = HubContext(language="en", schema=schema, data_stores=data_stores) expected_context = { "individual_response_enabled": False, @@ -131,19 +128,9 @@ def test_get_context(progress_store, answer_store, list_store, router): ) -def test_get_context_custom_content_incomplete( - progress_store, answer_store, list_store, router -): +def test_get_context_custom_content_incomplete(router, data_stores): schema = load_schema_from_name("test_hub_and_spoke_custom_content") - hub_context = HubContext( - language="en", - progress_store=progress_store, - list_store=list_store, - schema=schema, - answer_store=answer_store, - metadata={}, - response_metadata={}, - ) + hub_context = HubContext(language="en", schema=schema, data_stores=data_stores) expected_context = { "individual_response_enabled": False, @@ -160,19 +147,9 @@ def test_get_context_custom_content_incomplete( ) -def test_get_context_custom_content_complete( - progress_store, answer_store, list_store, router -): +def test_get_context_custom_content_complete(data_stores, router): schema = load_schema_from_name("test_hub_and_spoke_custom_content") - hub_context = HubContext( - language="en", - progress_store=progress_store, - list_store=list_store, - schema=schema, - answer_store=answer_store, - metadata={}, - response_metadata={}, - ) + hub_context = HubContext(language="en", schema=schema, data_stores=data_stores) expected_context = { "individual_response_enabled": False, @@ -190,18 +167,11 @@ def test_get_context_custom_content_complete( def test_get_context_no_list_items_survey_incomplete_individual_response_disabled( - progress_store, answer_store, list_store, router + data_stores, + router, ): schema = load_schema_from_name("test_individual_response") - hub_context = HubContext( - language="en", - progress_store=progress_store, - list_store=list_store, - schema=schema, - answer_store=answer_store, - metadata={}, - response_metadata={}, - ) + hub_context = HubContext(language="en", schema=schema, data_stores=data_stores) assert not ( hub_context( diff --git a/tests/app/views/contexts/test_list_context.py b/tests/app/views/contexts/test_list_context.py index 6e2119d7bc..36f9aa0dca 100644 --- a/tests/app/views/contexts/test_list_context.py +++ b/tests/app/views/contexts/test_list_context.py @@ -1,6 +1,8 @@ import pytest -from app.data_models.progress_store import ProgressStore +from app.data_models import CompletionStatus, ProgressStore +from app.data_models.data_stores import DataStores +from app.data_models.progress import ProgressDict from app.questionnaire.questionnaire_schema import DEFAULT_LANGUAGE_CODE from app.utilities.schema import load_schema_from_name from app.views.contexts import ListContext @@ -8,44 +10,53 @@ @pytest.mark.usefixtures("app") def test_build_list_collector_context( - list_collector_block, schema, people_answer_store, people_list_store + list_collector_block, + schema, + people_answer_store, + people_list_store, ): list_context = ListContext( DEFAULT_LANGUAGE_CODE, schema, - people_answer_store, - people_list_store, - None, - metadata={}, - response_metadata={}, + DataStores(answer_store=people_answer_store, list_store=people_list_store), ) - list_context = list_context(list_collector_block["summary"], for_list="people") + list_context = list_context( + list_collector_block["summary"], + for_list="people", + section_id="section-id", + has_repeating_blocks=False, + ) assert all(keys in list_context["list"] for keys in ["list_items", "editable"]) @pytest.mark.usefixtures("app") def test_build_list_summary_context_no_summary_block( - schema, people_answer_store, people_list_store + schema, + people_answer_store, + people_list_store, ): list_context = ListContext( DEFAULT_LANGUAGE_CODE, schema, - people_answer_store, - people_list_store, - None, - metadata={}, - response_metadata={}, + DataStores(answer_store=people_answer_store, list_store=people_list_store), ) - list_context = list_context(None, for_list="people") + list_context = list_context( + summary_definition=None, + for_list="people", + section_id="section-id", + has_repeating_blocks=False, + ) assert list_context == {"list": {"editable": False, "list_items": []}} @pytest.mark.usefixtures("app") def test_build_list_summary_context( - list_collector_block, people_answer_store, people_list_store + list_collector_block, + people_answer_store, + people_list_store, ): schema = load_schema_from_name("test_list_collector_primary_person") expected = [ @@ -55,6 +66,8 @@ def test_build_list_summary_context( "remove_link": "/questionnaire/people/PlwgoG/remove-person/", "primary_person": False, "list_item_id": "PlwgoG", + "is_complete": False, + "repeating_blocks": False, }, { "item_title": "Barry Pheloung", @@ -62,22 +75,22 @@ def test_build_list_summary_context( "remove_link": "/questionnaire/people/UHPLbX/remove-person/", "primary_person": False, "list_item_id": "UHPLbX", + "is_complete": False, + "repeating_blocks": False, }, ] list_context = ListContext( DEFAULT_LANGUAGE_CODE, schema, - people_answer_store, - people_list_store, - None, - metadata={}, - response_metadata={}, + DataStores(answer_store=people_answer_store, list_store=people_list_store), ) list_context = list_context( - list_collector_block["summary"], - "people", + summary_definition=list_collector_block["summary"], + for_list="people", + section_id="section-id", + has_repeating_blocks=False, edit_block_id=list_collector_block["edit_block"]["id"], remove_block_id=list_collector_block["remove_block"]["id"], ) @@ -87,22 +100,25 @@ def test_build_list_summary_context( @pytest.mark.usefixtures("app") def test_assert_primary_person_string_appended( - list_collector_block, people_answer_store, people_list_store + list_collector_block, + people_answer_store, + people_list_store, ): schema = load_schema_from_name("test_list_collector_primary_person") people_list_store["people"].primary_person = "PlwgoG" list_context = ListContext( language=DEFAULT_LANGUAGE_CODE, - progress_store=ProgressStore(), - list_store=people_list_store, schema=schema, - answer_store=people_answer_store, - metadata=None, - response_metadata={}, + data_stores=DataStores( + answer_store=people_answer_store, list_store=people_list_store + ), ) list_context = list_context( - list_collector_block["summary"], list_collector_block["for_list"] + summary_definition=list_collector_block["summary"], + for_list=list_collector_block["for_list"], + section_id="section-id", + has_repeating_blocks=False, ) assert list_context["list"]["list_items"][0]["primary_person"] is True @@ -112,31 +128,222 @@ def test_assert_primary_person_string_appended( @pytest.mark.usefixtures("app") def test_for_list_item_ids( - list_collector_block, people_answer_store, people_list_store + list_collector_block, + people_answer_store, + people_list_store, ): schema = load_schema_from_name("test_list_collector_primary_person") list_context = ListContext( language=DEFAULT_LANGUAGE_CODE, - progress_store=ProgressStore(), - list_store=people_list_store, schema=schema, - answer_store=people_answer_store, - metadata=None, - response_metadata={}, + data_stores=DataStores( + answer_store=people_answer_store, list_store=people_list_store + ), ) list_context = list_context( - list_collector_block["summary"], - list_collector_block["for_list"], + summary_definition=list_collector_block["summary"], + for_list=list_collector_block["for_list"], for_list_item_ids=["UHPLbX"], + section_id="section-id", + has_repeating_blocks=False, ) expected = [ { "item_title": "Barry Pheloung", "primary_person": False, + "is_complete": False, + "repeating_blocks": False, "list_item_id": "UHPLbX", } ] assert expected == list_context["list"]["list_items"] + + +@pytest.mark.usefixtures("app") +def test_list_context_items_complete_without_repeating_blocks( + people_answer_store, + people_list_store, + list_collector_block, +): + schema = load_schema_from_name("test_list_collector_primary_person") + expected = [ + { + "item_title": "Toni Morrison", + "edit_link": "/questionnaire/people/PlwgoG/edit-person/", + "remove_link": "/questionnaire/people/PlwgoG/remove-person/", + "primary_person": False, + "list_item_id": "PlwgoG", + "is_complete": True, + "repeating_blocks": False, + }, + { + "item_title": "Barry Pheloung", + "edit_link": "/questionnaire/people/UHPLbX/edit-person/", + "remove_link": "/questionnaire/people/UHPLbX/remove-person/", + "primary_person": False, + "list_item_id": "UHPLbX", + "is_complete": True, + "repeating_blocks": False, + }, + ] + + progress_store = ProgressStore( + [ + ProgressDict( + section_id="section-id", + list_item_id="PlwgoG", + status=CompletionStatus.COMPLETED, + block_ids=[], + ), + ProgressDict( + section_id="section-id", + list_item_id="UHPLbX", + status=CompletionStatus.COMPLETED, + block_ids=[], + ), + ] + ) + + list_context = ListContext( + DEFAULT_LANGUAGE_CODE, + schema, + data_stores=DataStores( + answer_store=people_answer_store, + list_store=people_list_store, + progress_store=progress_store, + ), + ) + + list_context = list_context( + summary_definition=list_collector_block["summary"], + for_list="people", + section_id="section-id", + has_repeating_blocks=False, + edit_block_id=list_collector_block["edit_block"]["id"], + remove_block_id=list_collector_block["remove_block"]["id"], + ) + + assert expected == list_context["list"]["list_items"] + + +@pytest.mark.usefixtures("app") +def test_list_context_items_incomplete_with_repeating_blocks( + repeating_blocks_answer_store, + repeating_blocks_list_store, +): + schema = load_schema_from_name( + "test_list_collector_repeating_blocks_section_summary" + ) + list_collector_block = schema.get_block("any-other-companies-or-branches") + expected = [ + { + "item_title": "CompanyA", + "edit_link": "/questionnaire/companies/PlwgoG/edit-company/", + "remove_link": "/questionnaire/companies/PlwgoG/remove-company/", + "primary_person": False, + "list_item_id": "PlwgoG", + "is_complete": False, + "repeating_blocks": True, + }, + { + "item_title": "CompanyB", + "edit_link": "/questionnaire/companies/UHPLbX/edit-company/", + "remove_link": "/questionnaire/companies/UHPLbX/remove-company/", + "primary_person": False, + "list_item_id": "UHPLbX", + "is_complete": False, + "repeating_blocks": True, + }, + ] + + list_context = ListContext( + DEFAULT_LANGUAGE_CODE, + schema, + DataStores( + answer_store=repeating_blocks_answer_store, + list_store=repeating_blocks_list_store, + ), + ) + + list_context = list_context( + summary_definition=list_collector_block["summary"], + for_list=list_collector_block["for_list"], + section_id="section-companies", + has_repeating_blocks=True, + edit_block_id=list_collector_block["edit_block"]["id"], + remove_block_id=list_collector_block["remove_block"]["id"], + ) + + assert expected == list_context["list"]["list_items"] + + +@pytest.mark.usefixtures("app") +def test_list_context_items_complete_with_repeating_blocks( + repeating_blocks_answer_store, repeating_blocks_list_store, supplementary_data_store +): + schema = load_schema_from_name( + "test_list_collector_repeating_blocks_section_summary" + ) + list_collector_block = schema.get_block("any-other-companies-or-branches") + expected = [ + { + "item_title": "CompanyA", + "edit_link": "/questionnaire/companies/PlwgoG/edit-company/", + "remove_link": "/questionnaire/companies/PlwgoG/remove-company/", + "primary_person": False, + "list_item_id": "PlwgoG", + "is_complete": True, + "repeating_blocks": True, + }, + { + "item_title": "CompanyB", + "edit_link": "/questionnaire/companies/UHPLbX/edit-company/", + "remove_link": "/questionnaire/companies/UHPLbX/remove-company/", + "primary_person": False, + "list_item_id": "UHPLbX", + "is_complete": True, + "repeating_blocks": True, + }, + ] + + progress_store = ProgressStore( + [ + ProgressDict( + section_id="section-companies", + list_item_id="PlwgoG", + status=CompletionStatus.COMPLETED, + block_ids=[], + ), + ProgressDict( + section_id="section-companies", + list_item_id="UHPLbX", + status=CompletionStatus.COMPLETED, + block_ids=[], + ), + ] + ) + + list_context = ListContext( + DEFAULT_LANGUAGE_CODE, + schema, + DataStores( + answer_store=repeating_blocks_answer_store, + list_store=repeating_blocks_list_store, + supplementary_data_store=supplementary_data_store, + progress_store=progress_store, + ), + ) + + list_context = list_context( + summary_definition=list_collector_block["summary"], + for_list=list_collector_block["for_list"], + section_id="section-companies", + has_repeating_blocks=True, + edit_block_id=list_collector_block["edit_block"]["id"], + remove_block_id=list_collector_block["remove_block"]["id"], + ) + + assert expected == list_context["list"]["list_items"] diff --git a/tests/app/views/contexts/test_preview_context.py b/tests/app/views/contexts/test_preview_context.py new file mode 100644 index 0000000000..d1a3de3849 --- /dev/null +++ b/tests/app/views/contexts/test_preview_context.py @@ -0,0 +1,277 @@ +import pytest +from flask_babel import lazy_gettext + +from app.data_models.data_stores import DataStores +from app.questionnaire import QuestionnaireSchema +from app.views.contexts.preview_context import ( + PreviewContext, + PreviewNotEnabledException, +) +from tests.app.views.contexts import assert_preview_context + + +def test_build_preview_rendering_context( + test_introduction_preview_linear_schema, + questionnaire_store, +): + preview_context = PreviewContext( + "en", + test_introduction_preview_linear_schema, + data_stores=DataStores( + supplementary_data_store=questionnaire_store.data_stores.supplementary_data_store, + metadata=questionnaire_store.data_stores.metadata, + response_metadata=questionnaire_store.data_stores.response_metadata, + ), + ) + + preview_context = preview_context() + + assert_preview_context(preview_context) + + +def test_build_preview_context( + test_introduction_preview_linear_schema, + questionnaire_store, +): + preview_context = PreviewContext( + "en", + test_introduction_preview_linear_schema, + data_stores=DataStores( + supplementary_data_store=questionnaire_store.data_stores.supplementary_data_store, + metadata=questionnaire_store.data_stores.metadata, + response_metadata=questionnaire_store.data_stores.response_metadata, + ), + ) + context = preview_context() + + expected_context = { + "sections": [ + { + "blocks": [ + { + "question": { + "answers": [ + { + "description": "Select your answer", + "guidance": { + "contents": [ + { + "description": "For example select `yes` if you can report for this period" + } + ], + "hide_guidance": "Additional guidance", + "show_guidance": "Additional guidance", + }, + "options": ["Yes", "No"], + "options_text": lazy_gettext( + "You can answer with one of the following options:" + ), + } + ], + "descriptions": [ + "

Your return should relate to the calendar year 2021.

" + ], + "guidance": { + "contents": [ + { + "description": "Please provide figures for the period in which you were trading." + } + ] + }, + "id": "report-radio", + "title": "Are you able to report for the calendar month 2 February 2016 to 3 March 2016?", + "type": "General", + } + }, + { + "question": { + "answers": [ + {"label": "Period from"}, + {"label": "Period to"}, + ], + "descriptions": [ + "

If figures are not available for the calendar year 2021, your return should " + "relate to a 12 month business year that ends between 6 April 2021 and 5 April 2022.

" + ], + "guidance": { + "contents": [ + { + "description": "

Only traded for a part of the year?

" + }, + { + "description": "

Please provide figures for the period in which you were trading.

" + }, + { + "description": "

Only commenced trading during 2021?

" + }, + { + "description": "

Your return should cover the period from the commencement of " + "your business until 31 December 2021 or, alternatively, any date up to 5 April 2022.

" + }, + { + "description": "

Ceased trading during 2021?

" + }, + { + "description": "

Your return should cover the period 1 January 2021 to the date " + "you ceased to trade or, alternatively, from the beginning of your last business year up to the cessation date.

" + }, + ] + }, + "id": "reporting-date", + "title": "What dates will you be reporting for?", + "type": "DateRange", + } + }, + { + "question": { + "answers": [ + { + "options": ["Yes", "No"], + "options_text": lazy_gettext( + "You can answer with one of the following options:" + ), + } + ], + "descriptions": [ + "

Your return should relate to the calendar year 2021.

" + ], + "guidance": None, + "id": "report-radio-second", + "title": "Are you sure you are able to report for the calendar month {calendar_start_date} to {calendar_end_date}?", + "type": "General", + } + }, + { + "question": { + "answers": [ + { + "options": [ + "Public sector projects", + "Private sector projects", + ], + "options_text": lazy_gettext( + "You can answer with the following options:" + ), + } + ], + "descriptions": None, + "guidance": { + "contents": [ + {"description": "Include:"}, + { + "list": [ + "Local public authorities and agencies", + "Regional and national authorities and agencies", + ] + }, + ] + }, + "id": "projects-checkbox", + "title": "Which sector did ESSENTIAL " + "ENTERPRISE LTD. carry out work " + "for?", + "type": "General", + } + }, + { + "question": { + "answers": [{"label": "Total turnover"}], + "descriptions": None, + "guidance": { + "contents": [ + {"description": "Include:"}, + { + "list": [ + "exports", + "payments for work in progress", + "costs incurred and passed on to customers", + "income from sub-contracted activities", + "commission", + "sales of goods purchased for resale", + "revenue earned from other parts of the business not named, please supply at fair value", + ] + }, + {"description": "Exclude:"}, + { + "list": [ + "VAT", + "income from the sale of fixed capital assets", + "grants and subsidies", + "insurance claims", + "interest received", + ] + }, + ] + }, + "id": "turnover-variants-block", + "title": "What was your total turnover", + "type": "General", + } + }, + { + "question": { + "answers": [ + { + "options": [ + "68 Abingdon Road, Goathill", + "7 Evelyn Street, Barry", + "251 Argae Lane, Barry", + ], + "options_text": lazy_gettext( + "You can answer with the following options:" + ), + }, + { + "options": ["I prefer not to say"], + "options_text": lazy_gettext( + "You can answer with the following options:" + ), + }, + ], + "descriptions": None, + "guidance": None, + "id": "address-mutually-exclusive-checkbox", + "title": "Were your company based at any of the following addresses?", + "type": "MutuallyExclusive", + } + }, + { + "question": { + "answers": [ + { + "label": "Comments", + "max_length": 2000, + } + ], + "descriptions": [ + "

Answer for ESSENTIAL ENTERPRISE LTD.

" + ], + "guidance": None, + "id": "further-details-text-area", + "title": "Please provide any further details", + "type": "General", + } + }, + ], + "title": "Main section", + "id": "introduction-section", + } + ] + } + assert "sections" in context + assert_preview_context(context) + assert len(context["sections"][0]) == 3 + assert "blocks" in context["sections"][0] + assert context == expected_context + + +def test_preview_questions_disabled_raises_exception( + data_stores, +): + schema = QuestionnaireSchema({"preview_questions": False}) + with pytest.raises(PreviewNotEnabledException): + PreviewContext( + "en", + schema, + data_stores, + ) diff --git a/tests/app/views/contexts/test_section_summary_context.py b/tests/app/views/contexts/test_section_summary_context.py index d2c99b6cee..fdafd15b47 100644 --- a/tests/app/views/contexts/test_section_summary_context.py +++ b/tests/app/views/contexts/test_section_summary_context.py @@ -1,29 +1,28 @@ -from unittest.mock import MagicMock - import pytest +from mock import MagicMock from app.data_models.answer_store import Answer, AnswerStore +from app.data_models.data_stores import DataStores from app.data_models.list_store import ListStore -from app.data_models.progress_store import ProgressStore from app.questionnaire.location import Location from app.questionnaire.questionnaire_schema import DEFAULT_LANGUAGE_CODE from app.questionnaire.routing_path import RoutingPath +from app.survey_config.link import Link from app.utilities.schema import load_schema_from_name from app.views.contexts import SectionSummaryContext +from tests.app.questionnaire.conftest import get_metadata from tests.app.views.contexts import assert_summary_context def test_build_summary_rendering_context( - test_section_summary_schema, answer_store, list_store, progress_store, mocker + test_section_summary_schema, + data_stores, + mocker, ): section_summary_context = SectionSummaryContext( "en", test_section_summary_schema, - answer_store, - list_store, - progress_store, - metadata={}, - response_metadata={}, + data_stores, current_location=Location(section_id="property-details-section"), routing_path=mocker.MagicMock(), ) @@ -34,16 +33,14 @@ def test_build_summary_rendering_context( def test_build_view_context_for_section_summary( - test_section_summary_schema, answer_store, list_store, progress_store, mocker + test_section_summary_schema, + data_stores, + mocker, ): summary_context = SectionSummaryContext( "en", test_section_summary_schema, - answer_store, - list_store, - progress_store, - metadata={}, - response_metadata={}, + data_stores, current_location=Location( section_id="property-details-section", block_id="property-details-summary", @@ -100,8 +97,6 @@ def test_custom_section( expected_title, test_section_summary_schema, answer_store, - list_store, - progress_store, mocker, ): for answer in answers: @@ -110,11 +105,7 @@ def test_custom_section( summary_context = SectionSummaryContext( "en", test_section_summary_schema, - answer_store, - list_store, - progress_store, - metadata={}, - response_metadata={}, + data_stores=DataStores(answer_store=answer_store), current_location=location, routing_path=mocker.MagicMock(), ) @@ -124,24 +115,26 @@ def test_custom_section( @pytest.mark.usefixtures("app") def test_context_for_section_list_summary(people_answer_store): - schema = load_schema_from_name("test_list_collector_section_summary") + schema = load_schema_from_name("test_list_collector_list_summary") summary_context = SectionSummaryContext( language=DEFAULT_LANGUAGE_CODE, schema=schema, - answer_store=people_answer_store, - list_store=ListStore( - [ - {"items": ["PlwgoG", "UHPLbX"], "name": "people"}, - {"items": ["gTrlio"], "name": "visitors"}, - ] + data_stores=DataStores( + answer_store=people_answer_store, + list_store=ListStore( + [ + {"items": ["PlwgoG", "UHPLbX"], "name": "people"}, + {"items": ["gTrlio"], "name": "visitors"}, + ] + ), + metadata=get_metadata( + extra_metadata={"display_address": "70 Abingdon Road, Goathill"} + ), ), - progress_store=ProgressStore(), - metadata={"display_address": "70 Abingdon Road, Goathill"}, - response_metadata={}, current_location=Location(section_id="section"), routing_path=RoutingPath( - [ + block_ids=[ "primary-person-list-collector", "list-collector", "visitor-list-collector", @@ -149,6 +142,7 @@ def test_context_for_section_list_summary(people_answer_store): section_id="section", ), ) + context = summary_context() expected = { "summary": { @@ -158,47 +152,59 @@ def test_context_for_section_list_summary(people_answer_store): { "add_link": "/questionnaire/people/add-person/?return_to=section-summary", "add_link_text": "Add someone to this household", + "item_anchor": None, + "item_label": None, "empty_list_text": "There are no householders", "list": { "editable": True, "list_items": [ { - "edit_link": "/questionnaire/people/PlwgoG/edit-person/?return_to=section-summary", + "edit_link": "/questionnaire/people/PlwgoG/edit-person/?return_to=section-summary&return_to_answer_id=PlwgoG", "item_title": "Toni Morrison", + "list_item_id": "PlwgoG", "primary_person": False, "remove_link": "/questionnaire/people/PlwgoG/remove-person/?return_to=section-summary", - "list_item_id": "PlwgoG", + "is_complete": False, + "repeating_blocks": False, }, { - "edit_link": "/questionnaire/people/UHPLbX/edit-person/?return_to=section-summary", + "edit_link": "/questionnaire/people/UHPLbX/edit-person/?return_to=section-summary&return_to_answer_id=UHPLbX", "item_title": "Barry Pheloung", + "list_item_id": "UHPLbX", "primary_person": False, "remove_link": "/questionnaire/people/UHPLbX/remove-person/?return_to=section-summary", - "list_item_id": "UHPLbX", + "is_complete": False, + "repeating_blocks": False, }, ], }, "list_name": "people", + "related_answers": None, "title": "Household members staying overnight on 13 October 2019 at 70 Abingdon Road, Goathill", "type": "List", }, { "add_link": "/questionnaire/visitors/add-visitor/?return_to=section-summary", "add_link_text": "Add another visitor to this household", + "item_anchor": None, + "item_label": None, "empty_list_text": "There are no visitors", "list": { "editable": True, "list_items": [ { - "edit_link": "/questionnaire/visitors/gTrlio/edit-visitor-person/?return_to=section-summary", + "edit_link": "/questionnaire/visitors/gTrlio/edit-visitor-person/?return_to=section-summary&return_to_answer_id=gTrlio", "item_title": "", + "list_item_id": "gTrlio", "primary_person": False, "remove_link": "/questionnaire/visitors/gTrlio/remove-visitor/?return_to=section-summary", - "list_item_id": "gTrlio", + "is_complete": False, + "repeating_blocks": False, } ], }, "list_name": "visitors", + "related_answers": None, "title": "Visitors staying overnight on 13 October 2019 at 70 Abingdon Road, Goathill", "type": "List", }, @@ -212,6 +218,228 @@ def test_context_for_section_list_summary(people_answer_store): assert context == expected +# pylint: disable=line-too-long +@pytest.mark.parametrize( + "test_schema, answer_store_fixture, item_label, answer_1_label, answer_2_label", + [ + ( + "test_list_collector_section_summary", + "companies_answer_store", + "Name of UK company or branch", + "Registration number", + "Is this UK company or branch an authorised insurer?", + ), + ( + "test_list_collector_variants_section_summary", + "companies_variants_answer_store_first_variant", + "Name of UK or non-UK company or branch", + "UK Registration number", + "Is this UK company or branch an authorised insurer?", + ), + ( + "test_list_collector_variants_section_summary", + "companies_variants_answer_store_second_variant", + "Name of UK or non-UK company or branch", + "Non-UK Registration number", + "Is this non-UK company or branch an authorised insurer?", + ), + ], +) +@pytest.mark.usefixtures("app") +def test_context_for_section_summary_with_list_summary_and_first_variant( + test_schema, + answer_store_fixture, + item_label, + answer_1_label, + answer_2_label, + request, +): + schema = load_schema_from_name(test_schema) + answer_store = request.getfixturevalue(answer_store_fixture) + summary_context = SectionSummaryContext( + language=DEFAULT_LANGUAGE_CODE, + schema=schema, + data_stores=DataStores( + answer_store=answer_store, + list_store=ListStore( + [ + {"items": ["PlwgoG", "UHPLbX"], "name": "companies"}, + ] + ), + ), + current_location=Location(section_id="section-companies"), + routing_path=RoutingPath( + block_ids=[ + "any-other-companies-or-branches", + ], + section_id="section-companies", + ), + ) + context = summary_context() + expected = { + "summary": { + "answers_are_editable": True, + "collapsible": False, + "sections": [ + { + "groups": [ + { + "blocks": [], + "id": "group-companies-0", + "links": {}, + "placeholder_text": None, + "title": None, + }, + { + "blocks": [ + { + "add_link": "/questionnaire/companies/add-company/?return_to=section-summary", + "add_link_text": "Add another UK company or branch", + "empty_list_text": "No UK company or branch added", + "item_anchor": "#company-or-branch-name", + "item_label": item_label, + "list": { + "editable": True, + "list_items": [ + { + "edit_link": "/questionnaire/companies/PlwgoG/edit-company/?return_to=section-summary&return_to_answer_id=PlwgoG", + "item_title": "company a", + "list_item_id": "PlwgoG", + "primary_person": False, + "remove_link": "/questionnaire/companies/PlwgoG/remove-company/?return_to=section-summary", + "is_complete": False, + "repeating_blocks": False, + }, + { + "edit_link": "/questionnaire/companies/UHPLbX/edit-company/?return_to=section-summary&return_to_answer_id=UHPLbX", + "item_title": "company b", + "list_item_id": "UHPLbX", + "primary_person": False, + "remove_link": "/questionnaire/companies/UHPLbX/remove-company/?return_to=section-summary", + "is_complete": False, + "repeating_blocks": False, + }, + ], + }, + "list_name": "companies", + "related_answers": { + "PlwgoG": [ + { + "id": "edit-company-PlwgoG", + "number": None, + "question": { + "answers": [ + { + "currency": None, + "id": "registration-number-PlwgoG", + "label": answer_1_label, + "link": "/questionnaire/companies/PlwgoG/edit-company/?return_to=section-summary&return_to_answer_id=registration-number-PlwgoG#registration-number", + "type": "number", + "unit": None, + "unit_length": None, + "value": 123, + "decimal_places": None, + }, + { + "currency": None, + "id": "authorised-insurer-radio-PlwgoG", + "label": answer_2_label, + "link": "/questionnaire/companies/PlwgoG/edit-company/?return_to=section-summary&return_to_answer_id=authorised-insurer-radio-PlwgoG#authorised-insurer-radio", + "type": "radio", + "unit": None, + "unit_length": None, + "value": { + "detail_answer_value": None, + "label": "Yes", + }, + "decimal_places": None, + }, + ], + "id": "edit-question-companies-PlwgoG", + "number": None, + "title": "What is the name of the company?", + "type": "General", + }, + "title": None, + } + ], + "UHPLbX": [ + { + "id": "edit-company-UHPLbX", + "number": None, + "question": { + "answers": [ + { + "currency": None, + "id": "registration-number-UHPLbX", + "label": answer_1_label, + "link": "/questionnaire/companies/UHPLbX/edit-company/?return_to=section-summary&return_to_answer_id=registration-number-UHPLbX#registration-number", + "type": "number", + "unit": None, + "unit_length": None, + "value": 456, + "decimal_places": None, + }, + { + "currency": None, + "id": "authorised-insurer-radio-UHPLbX", + "label": answer_2_label, + "link": "/questionnaire/companies/UHPLbX/edit-company/?return_to=section-summary&return_to_answer_id=authorised-insurer-radio-UHPLbX#authorised-insurer-radio", + "type": "radio", + "unit": None, + "unit_length": None, + "value": { + "detail_answer_value": None, + "label": "No", + }, + "decimal_places": None, + }, + ], + "id": "edit-question-companies-UHPLbX", + "number": None, + "title": "What is the name of the company?", + "type": "General", + }, + "title": None, + } + ], + }, + "title": "Companies or UK branches", + "type": "List", + } + ], + "id": "group-companies-1", + "links": { + "add_link": Link( + text="Add another UK company or branch", + url="/questionnaire/companies/add-company/?return_to=section-summary", + target="_self", + attributes={"data-qa": "add-item-link"}, + ) + }, + "placeholder_text": "No UK company or branch added", + "title": None, + }, + { + "blocks": [], + "id": "group-companies-2", + "links": {}, + "placeholder_text": None, + "title": None, + }, + ], + "title": "General insurance business", + } + ], + "page_title": "General insurance business", + "summary_type": "SectionSummary", + "title": "General insurance business", + } + } + + assert context == expected + + @pytest.mark.usefixtures("app") def test_context_for_driving_question_summary_empty_list(): schema = load_schema_from_name("test_list_collector_driving_question") @@ -219,13 +447,15 @@ def test_context_for_driving_question_summary_empty_list(): summary_context = SectionSummaryContext( DEFAULT_LANGUAGE_CODE, schema, - AnswerStore([{"answer_id": "anyone-usually-live-at-answer", "value": "No"}]), - ListStore(), - ProgressStore(), - metadata={}, - response_metadata={}, + DataStores( + answer_store=AnswerStore( + [{"answer_id": "anyone-usually-live-at-answer", "value": "No"}] + ) + ), current_location=Location(section_id="section"), - routing_path=RoutingPath(["anyone-usually-live-at"], section_id="section"), + routing_path=RoutingPath( + block_ids=["anyone-usually-live-at"], section_id="section" + ), ) context = summary_context() @@ -237,9 +467,12 @@ def test_context_for_driving_question_summary_empty_list(): { "add_link": "/questionnaire/anyone-usually-live-at/?return_to=section-summary", "add_link_text": "Add someone to this household", + "item_anchor": None, + "item_label": None, "empty_list_text": "There are no householders", "list": {"editable": False, "list_items": []}, "list_name": "people", + "related_answers": None, "title": "Household members", "type": "List", } @@ -249,18 +482,14 @@ def test_context_for_driving_question_summary_empty_list(): "title": "List Collector Driving Question Summary", } } - assert context == expected @pytest.mark.usefixtures("app") def test_context_for_driving_question_summary(): schema = load_schema_from_name("test_list_collector_driving_question") - - summary_context = SectionSummaryContext( - DEFAULT_LANGUAGE_CODE, - schema, - AnswerStore( + data_stores = DataStores( + answer_store=AnswerStore( [ {"answer_id": "anyone-usually-live-at-answer", "value": "Yes"}, {"answer_id": "first-name", "value": "Toni", "list_item_id": "PlwgoG"}, @@ -271,13 +500,17 @@ def test_context_for_driving_question_summary(): }, ] ), - ListStore([{"items": ["PlwgoG"], "name": "people"}]), - ProgressStore(), - metadata={}, - response_metadata={}, + list_store=ListStore([{"items": ["PlwgoG"], "name": "people"}]), + ) + + summary_context = SectionSummaryContext( + DEFAULT_LANGUAGE_CODE, + schema, + data_stores=data_stores, current_location=Location(section_id="section"), routing_path=RoutingPath( - ["anyone-usually-live-at", "anyone-else-live-at"], section_id="section" + block_ids=["anyone-usually-live-at", "anyone-else-live-at"], + section_id="section", ), ) @@ -291,20 +524,25 @@ def test_context_for_driving_question_summary(): { "add_link": "/questionnaire/people/add-person/?return_to=section-summary", "add_link_text": "Add someone to this household", + "item_anchor": None, + "item_label": None, "empty_list_text": "There are no householders", "list": { "editable": True, "list_items": [ { + "edit_link": "/questionnaire/people/PlwgoG/edit-person/?return_to=section-summary&return_to_answer_id=PlwgoG", "item_title": "Toni Morrison", + "list_item_id": "PlwgoG", "primary_person": False, - "edit_link": "/questionnaire/people/PlwgoG/edit-person/?return_to=section-summary", "remove_link": "/questionnaire/people/PlwgoG/remove-person/?return_to=section-summary", - "list_item_id": "PlwgoG", + "is_complete": False, + "repeating_blocks": False, } ], }, "list_name": "people", + "related_answers": None, "title": "Household members", "type": "List", } @@ -314,7 +552,6 @@ def test_context_for_driving_question_summary(): "title": "List Collector Driving Question Summary", } } - assert context == expected @@ -325,16 +562,15 @@ def test_titles_for_repeating_section_summary(people_answer_store): section_summary_context = SectionSummaryContext( DEFAULT_LANGUAGE_CODE, schema, - people_answer_store, - ListStore( - [ - {"items": ["PlwgoG", "UHPLbX"], "name": "people"}, - {"items": ["gTrlio"], "name": "visitors"}, - ] + data_stores=DataStores( + list_store=ListStore( + [ + {"items": ["PlwgoG", "UHPLbX"], "name": "people"}, + {"items": ["gTrlio"], "name": "visitors"}, + ] + ), + answer_store=people_answer_store, ), - ProgressStore(), - metadata={}, - response_metadata={}, current_location=Location( section_id="personal-details-section", list_name="people", @@ -350,16 +586,15 @@ def test_titles_for_repeating_section_summary(people_answer_store): section_summary_context = SectionSummaryContext( DEFAULT_LANGUAGE_CODE, schema, - people_answer_store, - ListStore( - [ - {"items": ["PlwgoG", "UHPLbX"], "name": "people"}, - {"items": ["gTrlio"], "name": "visitors"}, - ] + data_stores=DataStores( + answer_store=people_answer_store, + list_store=ListStore( + [ + {"items": ["PlwgoG", "UHPLbX"], "name": "people"}, + {"items": ["gTrlio"], "name": "visitors"}, + ] + ), ), - ProgressStore(), - metadata={}, - response_metadata={}, current_location=Location( block_id="personal-summary", section_id="personal-details-section", @@ -375,21 +610,31 @@ def test_titles_for_repeating_section_summary(people_answer_store): @pytest.mark.usefixtures("app") def test_primary_only_links_for_section_summary(people_answer_store): - schema = load_schema_from_name("test_list_collector_section_summary") - - summary_context = SectionSummaryContext( - language=DEFAULT_LANGUAGE_CODE, - schema=schema, + schema = load_schema_from_name("test_list_collector_list_summary") + data_stores = DataStores( answer_store=people_answer_store, list_store=ListStore( - [{"items": ["PlwgoG"], "name": "people", "primary_person": "PlwgoG"}] + [ + { + "items": ["PlwgoG"], + "name": "people", + "primary_person": "PlwgoG", + } + ] + ), + metadata=get_metadata( + extra_metadata={"display_address": "70 Abingdon Road, Goathill"} ), - progress_store=ProgressStore(), - metadata={"display_address": "70 Abingdon Road, Goathill"}, response_metadata={}, + ) + + summary_context = SectionSummaryContext( + language=DEFAULT_LANGUAGE_CODE, + schema=schema, + data_stores=data_stores, current_location=Location(section_id="section"), routing_path=RoutingPath( - [ + block_ids=[ "primary-person-list-collector", "list-collector", "visitor-list-collector", @@ -397,6 +642,7 @@ def test_primary_only_links_for_section_summary(people_answer_store): section_id="section", ), ) + context = summary_context() list_items = context["summary"]["custom_summary"][0]["list"]["list_items"] @@ -406,11 +652,9 @@ def test_primary_only_links_for_section_summary(people_answer_store): @pytest.mark.usefixtures("app") def test_primary_links_for_section_summary(people_answer_store): - schema = load_schema_from_name("test_list_collector_section_summary") + schema = load_schema_from_name("test_list_collector_list_summary") - summary_context = SectionSummaryContext( - language=DEFAULT_LANGUAGE_CODE, - schema=schema, + data_stores = DataStores( answer_store=people_answer_store, list_store=ListStore( [ @@ -421,12 +665,18 @@ def test_primary_links_for_section_summary(people_answer_store): } ] ), - progress_store=ProgressStore(), - metadata={"display_address": "70 Abingdon Road, Goathill"}, - response_metadata={}, + metadata=get_metadata( + extra_metadata={"display_address": "70 Abingdon Road, Goathill"} + ), + ) + + summary_context = SectionSummaryContext( + language=DEFAULT_LANGUAGE_CODE, + schema=schema, + data_stores=data_stores, current_location=Location(section_id="section"), routing_path=RoutingPath( - [ + block_ids=[ "primary-person-list-collector", "list-collector", "visitor-list-collector", @@ -434,6 +684,7 @@ def test_primary_links_for_section_summary(people_answer_store): section_id="section", ), ) + context = summary_context() list_items = context["summary"]["custom_summary"][0]["list"]["list_items"] diff --git a/tests/app/views/contexts/test_submission_metatdata_context.py b/tests/app/views/contexts/test_submission_metatdata_context.py index b99e4f2863..f507b8a90d 100644 --- a/tests/app/views/contexts/test_submission_metatdata_context.py +++ b/tests/app/views/contexts/test_submission_metatdata_context.py @@ -1,22 +1,30 @@ from datetime import datetime, timezone +import pytest from flask import Flask +from app.survey_config.survey_type import SurveyType from app.views.contexts.submission_metadata_context import ( build_submission_metadata_context, ) -SURVEY_TYPE_DEFAULT = "default" -SURVEY_TYPE_SOCIAL = "social" +SURVEY_TYPE_DEFAULT = SurveyType.DEFAULT +SURVEY_TYPE_SOCIAL = SurveyType.SOCIAL +SURVEY_TYPE_HEALTH = SurveyType.HEALTH SUBMITTED_AT = datetime(2021, 8, 17, 10, 10, 0, tzinfo=timezone.utc) TX_ID = "6b6f90e6-6c27-4c76-8295-7a14e2c4a399" -def test_metadata_survey_type_social(app: Flask): +@pytest.mark.parametrize( + "survey_type", + ( + (SURVEY_TYPE_SOCIAL), + (SURVEY_TYPE_HEALTH), + ), +) +def test_metadata_survey_types_without_ru_name(app: Flask, survey_type): with app.app_context(): - metadata = build_submission_metadata_context( - SURVEY_TYPE_SOCIAL, SUBMITTED_AT, TX_ID - ) + metadata = build_submission_metadata_context(survey_type, SUBMITTED_AT, TX_ID) assert len(metadata["itemsList"]) == 1 assert metadata["data-qa"] == "metadata" assert metadata["termCol"] == 3 diff --git a/tests/app/views/contexts/test_submit_context.py b/tests/app/views/contexts/test_submit_context.py index 0bd749c53a..6a695002b5 100644 --- a/tests/app/views/contexts/test_submit_context.py +++ b/tests/app/views/contexts/test_submit_context.py @@ -29,17 +29,15 @@ ), ) def test_custom_submission_content( - schema_name, expected, answer_store, list_store, progress_store + schema_name, + expected, + data_stores, ): schema = load_schema_from_name(schema_name) submit_questionnaire_context = SubmitQuestionnaireContext( "en", schema, - answer_store, - list_store, - progress_store, - metadata={}, - response_metadata={}, + data_stores, ) context = submit_questionnaire_context() @@ -52,17 +50,9 @@ def test_custom_submission_content( @pytest.mark.usefixtures("app") -def test_questionnaire_context(answer_store, list_store, progress_store): +def test_questionnaire_context(data_stores): schema = load_schema_from_name("test_submit_with_summary") - submit_questionnaire_context = SubmitQuestionnaireContext( - "en", - schema, - answer_store, - list_store, - progress_store, - metadata={}, - response_metadata={}, - ) + submit_questionnaire_context = SubmitQuestionnaireContext("en", schema, data_stores) context = submit_questionnaire_context() assert_summary_context(context) diff --git a/tests/app/views/contexts/test_submitted_response_context.py b/tests/app/views/contexts/test_submitted_response_context.py index 1fe762e016..fb885d85f5 100644 --- a/tests/app/views/contexts/test_submitted_response_context.py +++ b/tests/app/views/contexts/test_submitted_response_context.py @@ -1,18 +1,21 @@ from datetime import datetime, timedelta, timezone -from unittest.mock import Mock import pytest from flask import Flask from flask_babel import format_datetime +from mock import Mock from app.data_models import QuestionnaireStore from app.data_models.answer import Answer from app.data_models.answer_store import AnswerStore from app.settings import VIEW_SUBMITTED_RESPONSE_EXPIRATION_IN_SECONDS +from app.submitter.converter_v2 import NoMetadataException +from app.survey_config.survey_type import SurveyType from app.utilities.schema import load_schema_from_name from app.views.contexts.view_submitted_response_context import ( build_view_submitted_response_context, ) +from tests.app.questionnaire.conftest import get_metadata SUBMITTED_AT = datetime.now(timezone.utc) SCHEMA = load_schema_from_name("test_view_submitted_response", "en") @@ -20,52 +23,68 @@ def test_build_view_submitted_response_context_summary(app: Flask): with app.app_context(): - questionnaire_store = fake_questionnaire_store() + questionnaire_store = fake_questionnaire_store( + {"tx_id": "tx_id", "ru_name": "Apple"}, SUBMITTED_AT + ) context = build_view_submitted_response_context( - "en", SCHEMA, questionnaire_store, "default" + "en", SCHEMA, questionnaire_store, SurveyType.DEFAULT ) assert context["summary"]["answers_are_editable"] is False assert context["summary"]["collapsible"] is False assert context["view_submitted_response"]["expired"] is False assert ( - context["summary"]["groups"][0]["blocks"][0]["question"]["answers"][0][ - "value" - ] + context["summary"]["sections"][0]["groups"][0]["blocks"][0]["question"][ + "answers" + ][0]["value"] == "John Smith" ) assert ( - context["summary"]["groups"][0]["blocks"][0]["question"]["title"] + context["summary"]["sections"][0]["groups"][0]["blocks"][0]["question"][ + "title" + ] == "What is your name?" ) assert ( - context["summary"]["groups"][1]["blocks"][0]["question"]["answers"][0][ - "value" - ] + context["summary"]["sections"][0]["groups"][1]["blocks"][0]["question"][ + "answers" + ][0]["value"] == "NP10 8XG" ) assert ( - context["summary"]["groups"][1]["blocks"][0]["question"]["title"] + context["summary"]["sections"][0]["groups"][1]["blocks"][0]["question"][ + "title" + ] == "What is your address?" ) -def test_build_view_submitted_response_context_submitted_text(app: Flask): +def test_view_submitted_response_context_submitted_text_with_ru_name(app: Flask): with app.app_context(): - questionnaire_store = fake_questionnaire_store() + questionnaire_store = fake_questionnaire_store( + {"tx_id": "tx_id", "ru_name": "Apple"}, SUBMITTED_AT + ) context = build_view_submitted_response_context( - "en", SCHEMA, questionnaire_store, "default" + "en", SCHEMA, questionnaire_store, SurveyType.DEFAULT ) assert context["submitted_text"] == "Answers submitted for Apple" -def test_build_view_submitted_response_context_submitted_text_social(app: Flask): +@pytest.mark.parametrize( + "survey_type", + ( + (SurveyType.SOCIAL), + (SurveyType.HEALTH), + ), +) +def test_view_submitted_response_context_submitted_text_without_ru_name( + app: Flask, survey_type +): with app.app_context(): - questionnaire_store = fake_questionnaire_store() - questionnaire_store.metadata["trad_as"] = "Apple Inc" + questionnaire_store = fake_questionnaire_store({"tx_id": "tx_id"}, SUBMITTED_AT) context = build_view_submitted_response_context( - "en", SCHEMA, questionnaire_store, "social" + "en", SCHEMA, questionnaire_store, survey_type ) assert context["submitted_text"] == "Answers submitted." @@ -73,10 +92,11 @@ def test_build_view_submitted_response_context_submitted_text_social(app: Flask) def test_build_view_submitted_response_context_submitted_text_with_trad_as(app: Flask): with app.app_context(): - questionnaire_store = fake_questionnaire_store() - questionnaire_store.metadata["trad_as"] = "Apple Inc" + questionnaire_store = fake_questionnaire_store( + {"tx_id": "tx_id", "ru_name": "Apple", "trad_as": "Apple Inc"}, SUBMITTED_AT + ) context = build_view_submitted_response_context( - "en", SCHEMA, questionnaire_store, "default" + "en", SCHEMA, questionnaire_store, SurveyType.DEFAULT ) assert ( @@ -89,12 +109,14 @@ def test_view_submitted_response_expired( app: Flask, ): with app.app_context(): - questionnaire_store = fake_questionnaire_store() + questionnaire_store = fake_questionnaire_store( + {"tx_id": "tx_id", "ru_name": "Apple"}, SUBMITTED_AT + ) questionnaire_store.submitted_at = datetime.now(timezone.utc) - timedelta( seconds=VIEW_SUBMITTED_RESPONSE_EXPIRATION_IN_SECONDS ) context = build_view_submitted_response_context( - "en", SCHEMA, questionnaire_store, "default" + "en", SCHEMA, questionnaire_store, SurveyType.DEFAULT ) assert context["view_submitted_response"]["expired"] is True @@ -103,20 +125,34 @@ def test_view_submitted_response_expired( def test_build_view_submitted_response_no_submitted_at(app: Flask): with app.app_context(): - questionnaire_store = fake_questionnaire_store_no_submitted_at() + questionnaire_store = fake_questionnaire_store({}, None) with pytest.raises(Exception): build_view_submitted_response_context( - "en", SCHEMA, questionnaire_store, "default" + "en", SCHEMA, questionnaire_store, SurveyType.DEFAULT ) -def fake_questionnaire_store(): +def test_no_metadata_raises_error( + app: Flask, +): + with app.app_context(): + questionnaire_store = fake_questionnaire_store({}, SUBMITTED_AT) + + questionnaire_store.data_stores.metadata = None + + with pytest.raises(NoMetadataException): + build_view_submitted_response_context( + "en", SCHEMA, questionnaire_store, SurveyType.DEFAULT + ) + + +def fake_questionnaire_store(metadata, submitted_at): storage = Mock() storage.get_user_data = Mock(return_value=("{}", "ce_sid", 1, None)) questionnaire_store = QuestionnaireStore(storage) - questionnaire_store.submitted_at = SUBMITTED_AT - questionnaire_store.metadata = {"tx_id": "123456789", "ru_name": "Apple"} - questionnaire_store.answer_store = AnswerStore( + questionnaire_store.data_stores.metadata = get_metadata(metadata) + questionnaire_store.submitted_at = submitted_at + questionnaire_store.data_stores.answer_store = AnswerStore( [ Answer("name-answer", "John Smith", None).to_dict(), Answer("address-answer", "NP10 8XG", None).to_dict(), @@ -129,13 +165,3 @@ def format_submitted_on_description(): date = format_datetime(SUBMITTED_AT, format="dd LLLL yyyy") time = format_datetime(SUBMITTED_AT, format="HH:mm") return f"{date} at {time}" - - -def fake_questionnaire_store_no_submitted_at(): - storage = Mock() - storage.get_user_data = Mock(return_value=("{}", "ce_sid", 1, None)) - questionnaire_store = QuestionnaireStore(storage) - questionnaire_store.submitted_at = None - questionnaire_store.metadata = {} - questionnaire_store.answer_store = AnswerStore() - return questionnaire_store diff --git a/tests/app/views/contexts/test_summary_context.py b/tests/app/views/contexts/test_summary_context.py new file mode 100644 index 0000000000..30409f91ae --- /dev/null +++ b/tests/app/views/contexts/test_summary_context.py @@ -0,0 +1,465 @@ +import pytest +from markupsafe import Markup + +from app.data_models import ( + AnswerStore, + ListStore, + ProgressStore, + SupplementaryDataStore, +) +from app.data_models.data_stores import DataStores +from app.data_models.progress import CompletionStatus, ProgressDict +from app.questionnaire.questionnaire_schema import DEFAULT_LANGUAGE_CODE +from app.utilities.schema import load_schema_from_name +from app.views.contexts.summary_context import SummaryContext + + +@pytest.mark.usefixtures("app") +def test_context_for_summary(): + schema = load_schema_from_name("test_view_submitted_response_repeating_sections") + + list_store = ListStore( + [{"items": ["jufPpX", "fjWZET"], "name": "people", "primary_person": "jufPpX"}] + ) + + answer_store = AnswerStore( + [ + {"answer_id": "name-answer", "value": "John"}, + {"answer_id": "address-answer", "value": "1 Street"}, + {"answer_id": "you-live-here", "value": "Yes"}, + {"answer_id": "first-name", "value": "James", "list_item_id": "jufPpX"}, + {"answer_id": "last-name", "value": "Bond", "list_item_id": "jufPpX"}, + {"answer_id": "first-name", "value": "Jane", "list_item_id": "fjWZET"}, + {"answer_id": "last-name", "value": "Doe", "list_item_id": "fjWZET"}, + {"answer_id": "anyone-else", "value": "No"}, + {"answer_id": "skip-first-block-answer", "value": "Yes"}, + {"answer_id": "second-number-answer-also-in-total", "value": 1}, + {"answer_id": "third-number-answer", "value": 2, "list_item_id": "jufPpX"}, + { + "answer_id": "third-number-answer-also-in-total", + "value": 2, + "list_item_id": "jufPpX", + }, + { + "answer_id": "checkbox-answer", + "value": ["{calc_value_2}"], + "list_item_id": "jufPpX", + }, + {"answer_id": "third-number-answer", "value": 1, "list_item_id": "fjWZET"}, + { + "answer_id": "third-number-answer-also-in-total", + "value": 1, + "list_item_id": "fjWZET", + }, + { + "answer_id": "checkbox-answer", + "value": ["{calc_value_1}"], + "list_item_id": "fjWZET", + }, + ] + ) + + progress_store = ProgressStore( + [ + ProgressDict( + section_id="name-section", + block_ids=["name", "address"], + status=CompletionStatus.COMPLETED, + ), + ProgressDict( + section_id="section", + block_ids=["primary-person-list-collector", "list-collector"], + status=CompletionStatus.COMPLETED, + ), + ProgressDict( + section_id="questions-section", + block_ids=[ + "skip-first-block", + "second-number-block", + "currency-total-playback-1", + ], + status=CompletionStatus.COMPLETED, + ), + ProgressDict( + section_id="calculated-summary-section", + block_ids=[ + "third-number-block", + "currency-total-playback-2", + "mutually-exclusive-checkbox", + ], + status=CompletionStatus.COMPLETED, + list_item_id="jufPpX", + ), + ProgressDict( + section_id="calculated-summary-section", + block_ids=[ + "third-number-block", + "currency-total-playback-2", + "mutually-exclusive-checkbox", + ], + status=CompletionStatus.COMPLETED, + list_item_id="fjWZET", + ), + ] + ) + + data_stores = DataStores( + answer_store=answer_store, + list_store=list_store, + progress_store=progress_store, + metadata=None, + response_metadata={}, + supplementary_data_store=SupplementaryDataStore(), + ) + + summary_context = SummaryContext( + language=DEFAULT_LANGUAGE_CODE, + schema=schema, + data_stores=data_stores, + view_submitted_response=False, + ) + context = summary_context() + expected = { + "answers_are_editable": False, + "collapsible": False, + "sections": [ + { + "groups": [ + { + "blocks": [ + { + "id": "name", + "number": None, + "question": { + "answers": [ + { + "currency": None, + "id": "name-answer", + "label": "Full name", + "link": "/questionnaire/name/#name-answer", + "type": "textfield", + "unit": None, + "unit_length": None, + "value": Markup("John"), + "decimal_places": None, + } + ], + "id": "name-question", + "number": None, + "title": "What is your name?", + "type": "General", + }, + "title": None, + } + ], + "id": "personal-details-group-0", + "links": {}, + "placeholder_text": None, + "title": "Personal Details", + }, + { + "blocks": [ + { + "id": "address", + "number": None, + "question": { + "answers": [ + { + "currency": None, + "id": "address-answer", + "label": "Postcode", + "link": "/questionnaire/address/#address-answer", + "type": "textfield", + "unit": None, + "unit_length": None, + "value": Markup("1 Street"), + "decimal_places": None, + } + ], + "id": "address-question", + "number": None, + "title": "What is your address?", + "type": "General", + }, + "title": None, + } + ], + "id": "address-details-group-0", + "links": {}, + "placeholder_text": None, + "title": "Address Details", + }, + ], + "title": "Personal Details Section", + }, + { + "groups": [ + { + "blocks": [ + { + "id": "skip-first-block", + "number": None, + "question": { + "answers": [ + { + "currency": None, + "id": "skip-first-block-answer", + "label": None, + "link": "/questionnaire/skip-first-block/#skip-first-block-answer", + "type": "radio", + "unit": None, + "unit_length": None, + "value": { + "detail_answer_value": None, + "label": "Yes", + }, + "decimal_places": None, + } + ], + "id": "skip-first-block-question", + "number": None, + "title": "Skip First Block so it doesn’t appear in Total?", + "type": "General", + }, + "title": None, + }, + { + "id": "second-number-block", + "number": None, + "question": { + "answers": [ + { + "currency": "GBP", + "id": "second-number-answer-also-in-total", + "label": "Second answer label also in total (optional)", + "link": "/questionnaire/second-number-block/#second-number-answer-also-in-total", + "type": "currency", + "unit": None, + "unit_length": None, + "value": 1, + "decimal_places": 2, + } + ], + "id": "second-number-question-also-in-total", + "number": None, + "title": "Second Number Additional Question Title", + "type": "General", + }, + "title": None, + }, + ], + "id": "radio-0", + "links": {}, + "placeholder_text": None, + "title": "Questions Group", + } + ], + "title": "Questions Section", + }, + { + "groups": [ + { + "blocks": [ + { + "id": "third-number-block", + "number": None, + "question": { + "answers": [ + { + "currency": "GBP", + "id": "third-number-answer", + "label": "Third answer in currency label", + "link": "/questionnaire/people/jufPpX/third-number-block/#third-number-answer", + "type": "currency", + "unit": None, + "unit_length": None, + "value": 2, + "decimal_places": 2, + }, + { + "currency": "GBP", + "id": "third-number-answer-also-in-total", + "label": "Third answer label also in currency total (optional)", + "link": "/questionnaire/people/jufPpX/third-number-block/#third-number-answer-also-in-total", + "type": "currency", + "unit": None, + "unit_length": None, + "value": 2, + "decimal_places": 2, + }, + ], + "id": "third-number-question", + "number": None, + "title": "Third Number Question Title", + "type": "General", + }, + "title": None, + }, + { + "id": "mutually-exclusive-checkbox", + "number": None, + "question": { + "answers": [ + { + "currency": None, + "id": "checkbox-answer", + "label": None, + "link": "/questionnaire/people/jufPpX/mutually-exclusive-checkbox/#checkbox-answer", + "type": "checkbox", + "unit": None, + "unit_length": None, + "value": [ + { + "detail_answer_value": None, + "label": "4 - calculated summary answer (current section)", + } + ], + "decimal_places": None, + } + ], + "id": "mutually-exclusive-checkbox-question", + "number": None, + "title": "Which answer did you give to question 4 and a half?", + "type": "MutuallyExclusive", + }, + "title": None, + }, + { + "id": "skippable-block", + "number": None, + "question": { + "answers": [ + { + "currency": "GBP", + "id": "skippable-answer", + "label": "Capital expenditure", + "link": "/questionnaire/people/jufPpX/skippable-block/#skippable-answer", + "type": "currency", + "unit": None, + "unit_length": None, + "value": None, + "decimal_places": 0, + } + ], + "id": "skippable-question", + "number": None, + "title": "How much did James Bond spend on fruit?", + "type": "General", + }, + "title": None, + }, + ], + "id": "calculated-summary-0", + "links": {}, + "placeholder_text": None, + "title": "Calculated Summary Group", + } + ], + "title": "James Bond", + }, + { + "groups": [ + { + "blocks": [ + { + "id": "third-number-block", + "number": None, + "question": { + "answers": [ + { + "currency": "GBP", + "id": "third-number-answer", + "label": "Third answer in currency label", + "link": "/questionnaire/people/fjWZET/third-number-block/#third-number-answer", + "type": "currency", + "unit": None, + "unit_length": None, + "value": 1, + "decimal_places": 2, + }, + { + "currency": "GBP", + "id": "third-number-answer-also-in-total", + "label": "Third answer label also in currency total (optional)", + "link": "/questionnaire/people/fjWZET/third-number-block/#third-number-answer-also-in-total", + "type": "currency", + "unit": None, + "unit_length": None, + "value": 1, + "decimal_places": 2, + }, + ], + "id": "third-number-question", + "number": None, + "title": "Third Number Question Title", + "type": "General", + }, + "title": None, + }, + { + "id": "mutually-exclusive-checkbox", + "number": None, + "question": { + "answers": [ + { + "currency": None, + "id": "checkbox-answer", + "label": None, + "link": "/questionnaire/people/fjWZET/mutually-exclusive-checkbox/#checkbox-answer", + "type": "checkbox", + "unit": None, + "unit_length": None, + "value": [ + { + "detail_answer_value": None, + "label": "1 - calculated summary answer (previous section)", + } + ], + "decimal_places": None, + } + ], + "id": "mutually-exclusive-checkbox-question", + "number": None, + "title": "Which answer did you give to question 4 and a half?", + "type": "MutuallyExclusive", + }, + "title": None, + }, + { + "id": "skippable-block", + "number": None, + "question": { + "answers": [ + { + "currency": "GBP", + "id": "skippable-answer", + "label": "Capital expenditure", + "link": "/questionnaire/people/fjWZET/skippable-block/#skippable-answer", + "type": "currency", + "unit": None, + "unit_length": None, + "value": None, + "decimal_places": 0, + } + ], + "id": "skippable-question", + "number": None, + "title": "How much did Jane Doe spend on fruit?", + "type": "General", + }, + "title": None, + }, + ], + "id": "calculated-summary-0-1", + "links": {}, + "placeholder_text": None, + "title": "Calculated Summary Group", + } + ], + "title": "Jane Doe", + }, + ], + "summary_type": "Summary", + "view_submitted_response": False, + } + assert context == expected diff --git a/tests/app/views/contexts/test_thank_you_context.py b/tests/app/views/contexts/test_thank_you_context.py index 8273cfb5e2..ae478deb45 100644 --- a/tests/app/views/contexts/test_thank_you_context.py +++ b/tests/app/views/contexts/test_thank_you_context.py @@ -1,46 +1,62 @@ from datetime import datetime, timedelta, timezone +import pytest from flask import Flask +from app.survey_config.survey_type import SurveyType from app.utilities.schema import load_schema_from_name from app.views.contexts.thank_you_context import build_thank_you_context +from tests.app.questionnaire.conftest import get_metadata -SURVEY_TYPE_DEFAULT = "default" -SURVEY_TYPE_SOCIAL = "social" +SURVEY_TYPE_DEFAULT = SurveyType.DEFAULT +SURVEY_TYPE_SOCIAL = SurveyType.SOCIAL +SURVEY_TYPE_HEALTH = SurveyType.HEALTH SUBMITTED_AT = datetime.now(timezone.utc) SCHEMA = load_schema_from_name("test_view_submitted_response", "en") -def test_social_survey_context(fake_session_data, app: Flask): +def test_default_survey_context(app: Flask): with app.app_context(): context = build_thank_you_context( - SCHEMA, fake_session_data, SUBMITTED_AT, SURVEY_TYPE_SOCIAL + SCHEMA, + get_metadata(extra_metadata={"ru_name": "ESSENTIAL ENTERPRISE LTD"}), + SUBMITTED_AT, + SURVEY_TYPE_DEFAULT, ) - assert context["submission_text"] == "Your answers have been submitted." - assert len(context["metadata"]["itemsList"]) == 1 + assert ( + context["submission_text"] + == "Your answers have been submitted for ESSENTIAL ENTERPRISE LTD" + ) + assert len(context["metadata"]["itemsList"]) == 2 -def test_default_survey_context(fake_session_data, app: Flask): +@pytest.mark.parametrize( + "survey_type", + ( + (SURVEY_TYPE_SOCIAL), + (SURVEY_TYPE_HEALTH), + ), +) +def test_survey_context_without_ru_name(app: Flask, survey_type): with app.app_context(): - fake_session_data.ru_name = "ESSENTIAL ENTERPRISE LTD" context = build_thank_you_context( - SCHEMA, fake_session_data, SUBMITTED_AT, SURVEY_TYPE_DEFAULT + SCHEMA, get_metadata(), SUBMITTED_AT, survey_type ) - assert ( - context["submission_text"] - == "Your answers have been submitted for ESSENTIAL ENTERPRISE LTD" - ) - assert len(context["metadata"]["itemsList"]) == 2 + assert context["submission_text"] == "Your answers have been submitted." + assert len(context["metadata"]["itemsList"]) == 1 -def test_default_survey_context_with_trad_as(fake_session_data, app: Flask): +def test_default_survey_context_with_trad_as(app: Flask): with app.app_context(): - fake_session_data.ru_name = "ESSENTIAL ENTERPRISE LTD" - fake_session_data.trad_as = "EE" context = build_thank_you_context( - SCHEMA, fake_session_data, SUBMITTED_AT, SURVEY_TYPE_DEFAULT + SCHEMA, + get_metadata( + extra_metadata={"ru_name": "ESSENTIAL ENTERPRISE LTD", "trad_as": "EE"} + ), + SUBMITTED_AT, + SURVEY_TYPE_DEFAULT, ) assert ( @@ -50,53 +66,53 @@ def test_default_survey_context_with_trad_as(fake_session_data, app: Flask): assert len(context["metadata"]["itemsList"]) == 2 -def test_view_submitted_response_enabled(fake_session_data, app: Flask): +def test_view_submitted_response_enabled(app: Flask): with app.app_context(): context = build_thank_you_context( - SCHEMA, fake_session_data, SUBMITTED_AT, SURVEY_TYPE_DEFAULT + SCHEMA, get_metadata(), SUBMITTED_AT, SURVEY_TYPE_DEFAULT ) assert context["view_submitted_response"]["enabled"] is True -def test_view_submitted_response_not_enabled(fake_session_data, app: Flask): +def test_view_submitted_response_not_enabled(app: Flask): with app.app_context(): schema = load_schema_from_name("test_title", "en") context = build_thank_you_context( - schema, fake_session_data, SUBMITTED_AT, SURVEY_TYPE_DEFAULT + schema, get_metadata(), SUBMITTED_AT, SURVEY_TYPE_DEFAULT ) assert context["view_submitted_response"]["enabled"] is False -def test_view_submitted_response_not_expired(fake_session_data, app: Flask): +def test_view_submitted_response_not_expired(app: Flask): with app.app_context(): context = build_thank_you_context( - SCHEMA, fake_session_data, SUBMITTED_AT, SURVEY_TYPE_DEFAULT + SCHEMA, get_metadata(), SUBMITTED_AT, SURVEY_TYPE_DEFAULT ) assert context["view_submitted_response"]["expired"] is False assert context["view_submitted_response"]["url"] == "/submitted/view-response/" -def test_view_submitted_response_expired(fake_session_data, app: Flask): +def test_view_submitted_response_expired(app: Flask): submitted_at = SUBMITTED_AT - timedelta(minutes=46) with app.app_context(): context = build_thank_you_context( - SCHEMA, fake_session_data, submitted_at, SURVEY_TYPE_DEFAULT + SCHEMA, get_metadata(), submitted_at, SURVEY_TYPE_DEFAULT ) assert context["view_submitted_response"]["expired"] is True -def test_custom_guidance(fake_session_data, app: Flask): +def test_custom_guidance(app: Flask): with app.app_context(): custom_guidance = {"contents": [{"description": "Custom guidance"}]} context = build_thank_you_context( SCHEMA, - fake_session_data, + get_metadata(), SUBMITTED_AT, SURVEY_TYPE_DEFAULT, custom_guidance, diff --git a/tests/app/views/handlers/conftest.py b/tests/app/views/handlers/conftest.py index e51fe677de..97ada5adcd 100644 --- a/tests/app/views/handlers/conftest.py +++ b/tests/app/views/handlers/conftest.py @@ -1,14 +1,18 @@ import uuid from datetime import datetime, timedelta, timezone -from unittest.mock import Mock import pytest from freezegun import freeze_time +from mock import Mock +from app.authentication.auth_payload_versions import AuthPayloadVersion from app.data_models import QuestionnaireStore +from app.data_models.data_stores import DataStores +from app.data_models.metadata_proxy import MetadataProxy from app.data_models.session_data import SessionData from app.data_models.session_store import SessionStore from app.questionnaire import QuestionnaireSchema +from tests.app.parser.conftest import get_response_expires_at time_to_freeze = datetime.now(timezone.utc).replace(second=0, microsecond=0) tx_id = str(uuid.uuid4()) @@ -17,7 +21,7 @@ period_id = "2016-02-01" ref_p_start_date = "2016-02-02" ref_p_end_date = "2016-03-03" -ru_ref = "432423423423" +ru_ref = "12345678901A" ru_name = "ru_name" user_id = "789473423" schema_name = "1_0000" @@ -37,24 +41,14 @@ channel = "H" case_ref = "1000000000000001" region_code = "GB_WLS" +response_expires_at = get_response_expires_at() @pytest.fixture @freeze_time(time_to_freeze) def session_data(): return SessionData( - tx_id="123", - schema_name="some_schema_name", - display_address="68 Abingdon Road, Goathill", - period_str=None, language_code="cy", - launch_language_code="en", - survey_url=None, - ru_name=None, - ru_ref=None, - submitted_at=datetime.now(timezone.utc).isoformat(), - response_id="321", - case_id="789", ) @@ -76,7 +70,12 @@ def language(): @pytest.fixture def schema(): - return QuestionnaireSchema({"post_submission": {"view_response": True}}) + return QuestionnaireSchema( + { + "post_submission": {"view_response": True}, + "title": "Test schema - View Submitted Response", + } + ) @pytest.fixture @@ -98,16 +97,7 @@ def set_storage_data( @pytest.fixture def session_data_feedback(): return SessionData( - tx_id=tx_id, - schema_name=schema_name, - response_id=response_id, - period_str=period_str, language_code=language_code, - launch_language_code=None, - survey_url=None, - ru_name=ru_name, - ru_ref=ru_ref, - case_id=case_id, feedback_count=feedback_count, ) @@ -119,24 +109,62 @@ def schema_feedback(): @pytest.fixture def metadata(): - return { - "tx_id": tx_id, - "user_id": user_id, - "schema_name": schema_name, - "collection_exercise_sid": collection_exercise_sid, - "period_id": period_id, - "period_str": period_str, - "ref_p_start_date": ref_p_start_date, - "ref_p_end_date": ref_p_end_date, - "ru_ref": ru_ref, - "response_id": response_id, - "form_type": form_type, - "display_address": display_address, - "case_type": case_type, - "channel": channel, - "case_ref": case_ref, - "region_code": region_code, - } + return MetadataProxy.from_dict( + { + "tx_id": tx_id, + "user_id": user_id, + "schema_name": schema_name, + "collection_exercise_sid": collection_exercise_sid, + "period_id": period_id, + "period_str": period_str, + "ref_p_start_date": ref_p_start_date, + "ref_p_end_date": ref_p_end_date, + "ru_ref": ru_ref, + "response_id": response_id, + "form_type": form_type, + "display_address": display_address, + "case_type": case_type, + "channel": channel, + "case_ref": case_ref, + "region_code": region_code, + "case_id": case_id, + "language_code": language_code, + "response_expires_at": response_expires_at, + } + ) + + +@pytest.fixture +def metadata_v2(): + return MetadataProxy.from_dict( + { + "version": AuthPayloadVersion.V2, + "tx_id": tx_id, + "case_id": case_id, + "schema_name": schema_name, + "collection_exercise_sid": collection_exercise_sid, + "response_id": response_id, + "channel": channel, + "region_code": region_code, + "account_service_url": "account_service_url", + "response_expires_at": get_response_expires_at(), + "survey_metadata": { + "data": { + "period_id": period_id, + "period_str": period_str, + "ref_p_start_date": ref_p_start_date, + "ref_p_end_date": ref_p_end_date, + "ru_ref": ru_ref, + "ru_name": ru_name, + "case_type": case_type, + "form_type": form_type, + "case_ref": case_ref, + "display_address": display_address, + "user_id": user_id, + } + }, + } + ) @pytest.fixture @@ -154,16 +182,7 @@ def submission_payload_expires_at(): @pytest.fixture def submission_payload_session_data(): return SessionData( - tx_id="tx_id", - schema_name="schema_name", - response_id="response_id", - period_str="period_str", language_code="cy", - launch_language_code="en", - survey_url=None, - ru_name="ru_name", - ru_ref="ru_ref", - case_id="0123456789000000", ) @@ -185,5 +204,59 @@ def mock_questionnaire_store(mocker): storage_ = mocker.Mock() storage_.get_user_data = mocker.Mock(return_value=("{}", "ce_id", 1, None)) questionnaire_store = QuestionnaireStore(storage_) - questionnaire_store.metadata = {"tx_id": "tx_id", "case_id": "case_id"} + questionnaire_store.data_stores = DataStores( + metadata=MetadataProxy.from_dict( + { + "tx_id": "tx_id", + "case_id": "case_id", + "ru_ref": ru_ref, + "user_id": user_id, + "collection_exercise_sid": collection_exercise_sid, + "period_id": period_id, + "schema_name": schema_name, + "account_service_url": "account_service_url", + "response_id": "response_id", + "response_expires_at": get_response_expires_at(), + } + ) + ) + return questionnaire_store + + +@pytest.fixture +def mock_questionnaire_store_v2(mocker): + storage_ = mocker.Mock() + storage_.get_user_data = mocker.Mock(return_value=("{}", "ce_id", 1, None)) + questionnaire_store = QuestionnaireStore(storage_) + questionnaire_store.data_stores = DataStores( + metadata=MetadataProxy.from_dict( + { + "version": AuthPayloadVersion.V2, + "tx_id": "tx_id", + "case_id": case_id, + "schema_name": schema_name, + "collection_exercise_sid": collection_exercise_sid, + "response_id": response_id, + "channel": channel, + "region_code": region_code, + "account_service_url": "account_service_url", + "response_expires_at": get_response_expires_at(), + "survey_metadata": { + "data": { + "period_id": period_id, + "period_str": period_str, + "ref_p_start_date": ref_p_start_date, + "ref_p_end_date": ref_p_end_date, + "ru_ref": ru_ref, + "ru_name": ru_name, + "case_type": case_type, + "form_type": form_type, + "case_ref": case_ref, + "display_address": display_address, + "user_id": user_id, + } + }, + } + ) + ) return questionnaire_store diff --git a/tests/app/views/handlers/test_confirmation_email_fulfilment_request.py b/tests/app/views/handlers/test_confirmation_email_fulfilment_request.py index 5316f72e40..da3f157214 100644 --- a/tests/app/views/handlers/test_confirmation_email_fulfilment_request.py +++ b/tests/app/views/handlers/test_confirmation_email_fulfilment_request.py @@ -2,28 +2,31 @@ from app.utilities.json import json_loads from app.views.handlers.confirm_email import ConfirmationEmailFulfilmentRequest - -from .conftest import time_to_freeze +from tests.app.views.handlers.conftest import time_to_freeze @freeze_time(time_to_freeze) def test_confirmation_email_fulfilment_request_message( - session_data, confirmation_email_fulfilment_schema + session_data, metadata, confirmation_email_fulfilment_schema ): email_address = "name@example.com" + fulfilment_request = ConfirmationEmailFulfilmentRequest( - email_address, session_data, confirmation_email_fulfilment_schema + email_address, + session_data, + metadata, + confirmation_email_fulfilment_schema, ) confirmation_email_json_message = json_loads(fulfilment_request.message) expected_payload = { "email_address": "name@example.com", - "display_address": "68 Abingdon Road, Goathill", + "display_address": metadata["display_address"], "form_type": confirmation_email_fulfilment_schema.form_type, "language_code": session_data.language_code, "region_code": confirmation_email_fulfilment_schema.region_code, - "tx_id": session_data.tx_id, + "tx_id": metadata.tx_id, } assert ( diff --git a/tests/app/views/handlers/test_feedback_upload.py b/tests/app/views/handlers/test_feedback_upload.py index b1c0aba0bf..7b4927d5e4 100644 --- a/tests/app/views/handlers/test_feedback_upload.py +++ b/tests/app/views/handlers/test_feedback_upload.py @@ -2,10 +2,10 @@ from freezegun import freeze_time +from app.authentication.auth_payload_versions import AuthPayloadVersion from app.questionnaire.questionnaire_schema import DEFAULT_LANGUAGE_CODE -from app.views.handlers.feedback import FeedbackMetadata, FeedbackPayload - -from .conftest import ( +from app.views.handlers.feedback import FeedbackMetadata, FeedbackPayloadV2 +from tests.app.views.handlers.conftest import ( case_id, case_ref, case_type, @@ -19,9 +19,11 @@ form_type, language_code, period_id, + period_str, ref_p_end_date, ref_p_start_date, region_code, + ru_name, ru_ref, schema_name, started_at, @@ -32,11 +34,11 @@ @freeze_time(datetime.now(tz=timezone.utc).isoformat()) -def test_feedback_payload( - session_data_feedback, schema_feedback, metadata, response_metadata +def test_feedback_payload_v2( + session_data_feedback, schema_feedback, metadata_v2, response_metadata ): - feedback_payload = FeedbackPayload( - metadata=metadata, + feedback_payload = FeedbackPayloadV2( + metadata=metadata_v2, response_metadata=response_metadata, schema=schema_feedback, case_id=case_id, @@ -47,40 +49,40 @@ def test_feedback_payload( ) expected_payload = { - "collection": { - "exercise_sid": collection_exercise_sid, - "instrument_id": "I", - "period": period_id, - "schema_name": schema_name, - }, + "case_id": case_id, + "channel": channel, + "collection_exercise_sid": collection_exercise_sid, "data": { "feedback_count": str(feedback_count), "feedback_text": feedback_text, "feedback_type": feedback_type, }, - "form_type": form_type, + "data_version": data_version, + "flushed": False, "launch_language_code": "en", - "metadata": { + "origin": "uk.gov.ons.edc.eq", + "region_code": region_code, + "schema_name": schema_name, + "started_at": started_at, + "submission_language_code": language_code, + "submitted_at": datetime.now(tz=timezone.utc).isoformat(), + "survey_metadata": { + "survey_id": survey_id, + "case_ref": case_ref, + "case_type": case_type, "display_address": display_address, - "ref_period_end_date": ref_p_end_date, - "ref_period_start_date": ref_p_start_date, + "form_type": form_type, + "period_id": period_id, + "period_str": period_str, + "ref_p_end_date": ref_p_end_date, + "ref_p_start_date": ref_p_start_date, + "ru_name": ru_name, "ru_ref": ru_ref, "user_id": user_id, }, - "origin": "uk.gov.ons.edc.eq", - "case_id": case_id, - "started_at": started_at, - "submitted_at": datetime.now(tz=timezone.utc).isoformat(), - "flushed": False, - "survey_id": survey_id, - "submission_language_code": language_code, "tx_id": tx_id, "type": "uk.gov.ons.edc.eq:feedback", - "version": data_version, - "case_type": case_type, - "channel": channel, - "region_code": region_code, - "case_ref": case_ref, + "version": AuthPayloadVersion.V2.value, } assert expected_payload == feedback_payload() @@ -89,7 +91,7 @@ def test_feedback_payload( def test_submission_language_code_uses_default_language_when_session_language_none( session_data_feedback, schema_feedback, metadata, response_metadata ): - feedback_payload = FeedbackPayload( + feedback_payload = FeedbackPayloadV2( metadata=metadata, response_metadata=response_metadata, schema=schema_feedback, @@ -112,3 +114,13 @@ def test_feedback_metadata(): } assert feedback_metadata() == expected_metadata + + +def test_feedback_metadata_with_receipting_keys(): + receipting_keys = {"qid": "1"} + + feedback_metadata = FeedbackMetadata(case_id, tx_id, **receipting_keys) + + expected_metadata = {"case_id": case_id, "tx_id": tx_id, "qid": "1"} + + assert feedback_metadata() == expected_metadata diff --git a/tests/app/views/handlers/test_individual_response_fulfilment_request.py b/tests/app/views/handlers/test_individual_response_fulfilment_request.py index 40add84768..7ce505655b 100644 --- a/tests/app/views/handlers/test_individual_response_fulfilment_request.py +++ b/tests/app/views/handlers/test_individual_response_fulfilment_request.py @@ -4,6 +4,7 @@ import pytest from freezegun import freeze_time +from app.data_models.metadata_proxy import MetadataProxy from app.forms.validators import sanitise_mobile_number from app.helpers.uuid_helper import is_valid_uuid4 from app.utilities.json import json_loads @@ -13,13 +14,23 @@ GB_WLS_REGION_CODE, IndividualResponseFulfilmentRequest, ) +from tests.app.parser.conftest import get_response_expires_at DUMMY_MOBILE_NUMBER = "07700900258" @freeze_time(datetime.now(tz=timezone.utc).isoformat()) def test_sms_fulfilment_request_payload(): - metadata = {"region_code": "GB-ENG", "case_id": str(uuid4())} + metadata = MetadataProxy( + region_code="GB-ENG", + case_id=str(uuid4()), + tx_id="tx_id", + response_id="response_id", + account_service_url="account_service_url", + collection_exercise_sid="collection_exercise_sid", + response_expires_at=get_response_expires_at(), + ) + fulfilment_request = IndividualResponseFulfilmentRequest( metadata, DUMMY_MOBILE_NUMBER ) @@ -39,7 +50,17 @@ def test_sms_fulfilment_request_payload(): @freeze_time(datetime.now(tz=timezone.utc).isoformat()) def test_postal_fulfilment_request_message(): - metadata = {"region_code": "GB-ENG", "case_id": str(uuid4())} + metadata = { + "region_code": "GB-ENG", + "case_id": str(uuid4()), + "tx_id": "tx_id", + "response_id": "response_id", + "account_service_url": "account_service_url", + "collection_exercise_sid": "collection_exercise_sid", + } + + metadata = MetadataProxy.from_dict(metadata) + fulfilment_request = IndividualResponseFulfilmentRequest(metadata) postal_json_message = json_loads(fulfilment_request.message) @@ -65,7 +86,22 @@ def validate_uuids_in_payload(payload): @freeze_time(datetime.now(tz=timezone.utc).isoformat()) def test_individual_case_id_not_present_when_case_type_spg(): - metadata = {"region_code": "GB-ENG", "case_id": str(uuid4()), "case_type": "SPG"} + metadata = { + "region_code": "GB-ENG", + "case_id": str(uuid4()), + "tx_id": "tx_id", + "response_id": "response_id", + "account_service_url": "account_service_url", + "collection_exercise_sid": "collection_exercise_sid", + "survey_metadata": { + "data": { + "case_type": "SPG", + } + }, + } + + metadata = MetadataProxy.from_dict(metadata) + fulfilment_request = IndividualResponseFulfilmentRequest(metadata) json_message = json_loads(fulfilment_request.message) @@ -74,7 +110,22 @@ def test_individual_case_id_not_present_when_case_type_spg(): @freeze_time(datetime.now(tz=timezone.utc).isoformat()) def test_individual_case_id_not_present_when_case_type_ce(): - metadata = {"region_code": "GB-ENG", "case_id": str(uuid4()), "case_type": "CE"} + metadata = { + "region_code": "GB-ENG", + "case_id": str(uuid4()), + "tx_id": "tx_id", + "response_id": "response_id", + "account_service_url": "account_service_url", + "collection_exercise_sid": "collection_exercise_sid", + "survey_metadata": { + "data": { + "case_type": "CE", + } + }, + } + + metadata = MetadataProxy.from_dict(metadata) + fulfilment_request = IndividualResponseFulfilmentRequest(metadata) json_message = json_loads(fulfilment_request.message) @@ -90,7 +141,18 @@ def test_individual_case_id_not_present_when_case_type_ce(): ], ) def test_fulfilment_code_for_sms(region_code, expected_fulfilment_code): - metadata = {"region_code": region_code, "case_id": str(uuid4()), "case_type": "SPG"} + metadata = { + "region_code": region_code, + "case_id": str(uuid4()), + "case_type": "SPG", + "tx_id": "tx_id", + "response_id": "response_id", + "account_service_url": "account_service_url", + "collection_exercise_sid": "collection_exercise_sid", + } + + metadata = MetadataProxy.from_dict(metadata) + fulfilment_request = IndividualResponseFulfilmentRequest( metadata, DUMMY_MOBILE_NUMBER ) @@ -110,8 +172,20 @@ def test_fulfilment_code_for_sms(region_code, expected_fulfilment_code): ], ) def test_fulfilment_code_for_postal(region_code, expected_fulfilment_code): - metadata = {"region_code": region_code, "case_id": str(uuid4()), "case_type": "SPG"} + metadata = { + "region_code": region_code, + "case_id": str(uuid4()), + "case_type": "SPG", + "tx_id": "tx_id", + "response_id": "response_id", + "account_service_url": "account_service_url", + "collection_exercise_sid": "collection_exercise_sid", + } + + metadata = MetadataProxy.from_dict(metadata) + fulfilment_request = IndividualResponseFulfilmentRequest(metadata) + json_message = json_loads(fulfilment_request.message) assert ( diff --git a/tests/app/views/handlers/test_preview_questions_pdf.py b/tests/app/views/handlers/test_preview_questions_pdf.py new file mode 100644 index 0000000000..bc8e4a21c4 --- /dev/null +++ b/tests/app/views/handlers/test_preview_questions_pdf.py @@ -0,0 +1,16 @@ +import pytest + +from app.data_models import QuestionnaireStore +from app.views.contexts.preview_context import PreviewNotEnabledException +from app.views.handlers.preview_questions_pdf import PreviewQuestionsPDF +from tests.app.views.handlers.conftest import set_storage_data + + +@pytest.mark.usefixtures("app") +def test_preview_questions_disabled_raises_exception(storage, schema, language): + set_storage_data(storage) + + questionnaire_store = QuestionnaireStore(storage) + questionnaire_store.set_metadata({"schema_name": "test_checkbox"}) + with pytest.raises(PreviewNotEnabledException): + PreviewQuestionsPDF(schema, questionnaire_store, language).get_context() diff --git a/tests/app/views/handlers/test_question_with_dynamic_answers.py b/tests/app/views/handlers/test_question_with_dynamic_answers.py new file mode 100644 index 0000000000..16d57e0107 --- /dev/null +++ b/tests/app/views/handlers/test_question_with_dynamic_answers.py @@ -0,0 +1,114 @@ +from datetime import datetime, timezone + +import pytest +from freezegun import freeze_time + +from app.data_models import AnswerStore, ListStore, QuestionnaireStore +from app.questionnaire import Location +from app.utilities.schema import load_schema_from_name +from app.views.handlers.question import Question +from tests.app.parser.conftest import get_response_expires_at +from tests.app.views.handlers.conftest import set_storage_data + + +@pytest.mark.usefixtures("app") +@freeze_time("2022-06-01T15:34:54+00:00") +def test_question_with_dynamic_answers(storage, language, mocker): + submitted_at = datetime.now(timezone.utc) + set_storage_data(storage, submitted_at=submitted_at) + + questionnaire_store = QuestionnaireStore(storage) + questionnaire_store.data_stores.answer_store = AnswerStore( + [ + { + "answer_id": "supermarket-name", + "value": "Tesco", + "list_item_id": "tUJzGV", + }, + { + "answer_id": "supermarket-name", + "value": "Aldi", + "list_item_id": "vhECeh", + }, + ] + ) + questionnaire_store.data_stores.list_store = ListStore( + [{"items": ["tUJzGV", "vhECeh"], "name": "supermarkets"}] + ) + questionnaire_store.set_metadata({"response_expires_at": get_response_expires_at()}) + schema = load_schema_from_name("test_dynamic_answers_list_source") + + mocker.patch( + "app.views.handlers.question.Question.is_location_valid", + return_value=True, + ) + question = Question( + current_location=Location(section_id="section", block_id="dynamic-answer"), + form_data=None, + language=language, + questionnaire_store=questionnaire_store, + request_args=mocker.MagicMock(), + schema=schema, + ) + + form = question.form + question.handle_post() + + assert form.question["answers"] == [ + { + "decimal_places": 0, + "id": "percentage-of-shopping-tUJzGV", + "label": "Percentage of shopping at Tesco", + "list_item_id": "tUJzGV", + "mandatory": False, + "maximum": {"value": 100}, + "original_answer_id": "percentage-of-shopping", + "type": "Percentage", + }, + { + "decimal_places": 0, + "id": "percentage-of-shopping-vhECeh", + "label": "Percentage of shopping at Aldi", + "list_item_id": "vhECeh", + "mandatory": False, + "maximum": {"value": 100}, + "original_answer_id": "percentage-of-shopping", + "type": "Percentage", + }, + { + "decimal_places": 0, + "id": "days-a-week-tUJzGV", + "label": "How many days a week you shop at Tesco", + "list_item_id": "tUJzGV", + "mandatory": False, + "maximum": {"value": 7}, + "minimum": {"value": 1}, + "original_answer_id": "days-a-week", + "type": "Number", + }, + { + "decimal_places": 0, + "id": "days-a-week-vhECeh", + "label": "How many days a week you shop at Aldi", + "list_item_id": "vhECeh", + "mandatory": False, + "maximum": {"value": 7}, + "minimum": {"value": 1}, + "original_answer_id": "days-a-week", + "type": "Number", + }, + { + "id": "based-checkbox-answer", + "instruction": "Select any answers that apply", + "label": "Are supermarkets UK or non UK based?", + "mandatory": False, + "options": [ + {"label": "UK based supermarkets", "value": "UK based supermarkets"}, + { + "label": "Non UK based supermarkets", + "value": "Non UK based supermarkets", + }, + ], + "type": "Checkbox", + }, + ] diff --git a/tests/app/views/handlers/test_submission_handler.py b/tests/app/views/handlers/test_submission_handler.py index 6e93553f33..d5aa871247 100644 --- a/tests/app/views/handlers/test_submission_handler.py +++ b/tests/app/views/handlers/test_submission_handler.py @@ -3,8 +3,10 @@ import pytest from freezegun import freeze_time +from app.authentication.auth_payload_versions import AuthPayloadVersion from app.data_models.session_store import SessionStore from app.questionnaire.questionnaire_schema import QuestionnaireSchema +from app.utilities.schema import load_schema_from_name from app.views.handlers.submission import SubmissionHandler @@ -17,7 +19,7 @@ def test_submission_language_code_uses_session_data_language_if_present( return_value=submission_payload_session_store, ) mocker.patch( - "app.views.handlers.submission.convert_answers", mocker.Mock(return_value={}) + "app.views.handlers.submission.convert_answers_v2", mocker.Mock(return_value={}) ) submission_handler = SubmissionHandler( QuestionnaireSchema({}), mock_questionnaire_store, {} @@ -33,7 +35,7 @@ def test_submission_language_code_uses_default_language_if_session_data_language mocker, ): mocker.patch( - "app.views.handlers.submission.convert_answers", mocker.Mock(return_value={}) + "app.views.handlers.submission.convert_answers_v2", mocker.Mock(return_value={}) ) submission_payload_session_data.language_code = None submission_payload_session_data.launch_language_code = None @@ -80,3 +82,57 @@ def test_submit_view_submitted_response_true_submitted_at_set( assert mock_questionnaire_store.submitted_at == datetime.now(timezone.utc) assert mock_questionnaire_store.save.called assert not mock_questionnaire_store.delete.called + + +@freeze_time(datetime.now(timezone.utc).replace(second=0, microsecond=0)) +@pytest.mark.usefixtures("app") +def test_submission_payload_structure_v2( + app, submission_payload_session_store, mock_questionnaire_store_v2, mocker +): + expected_payload = { + "case_id": "case_id", + "tx_id": "tx_id", + "type": "uk.gov.ons.edc.eq:surveyresponse", + "version": AuthPayloadVersion.V2.value, + "data_version": "0.0.3", + "origin": "uk.gov.ons.edc.eq", + "collection_exercise_sid": "ce_sid", + "schema_name": "1_0000", + "flushed": False, + "submitted_at": datetime.now(timezone.utc).isoformat(), + "launch_language_code": "en", + "channel": "H", + "region_code": "GB_WLS", + "survey_metadata": { + "period_id": "2016-02-01", + "period_str": "2016-01-01", + "ref_p_start_date": "2016-02-02", + "ref_p_end_date": "2016-03-03", + "ru_ref": "12345678901A", + "ru_name": "ru_name", + "case_type": "I", + "form_type": "I", + "case_ref": "1000000000000001", + "display_address": "68 Abingdon Road, Goathill", + "user_id": "789473423", + "survey_id": "0", + }, + "submission_language_code": "cy", + "data": {"answers": [], "lists": []}, + } + + with app.test_request_context(): + mocker.patch( + "app.views.handlers.submission.get_session_store", + return_value=submission_payload_session_store, + ) + schema = load_schema_from_name("test_checkbox") + + submission_handler = SubmissionHandler( + schema, + mock_questionnaire_store_v2, + full_routing_path=[], + ) + payload = submission_handler.get_payload() + + assert expected_payload == payload diff --git a/tests/app/views/handlers/test_view_preview_questions.py b/tests/app/views/handlers/test_view_preview_questions.py new file mode 100644 index 0000000000..db87dbba1f --- /dev/null +++ b/tests/app/views/handlers/test_view_preview_questions.py @@ -0,0 +1,43 @@ +from datetime import datetime, timezone + +import pytest +from freezegun import freeze_time + +from app.data_models import QuestionnaireStore +from app.questionnaire import QuestionnaireSchema +from app.views.contexts.preview_context import PreviewNotEnabledException +from app.views.handlers.preview_questions_pdf import PreviewQuestionsPDF +from app.views.handlers.view_preview_questions import ViewPreviewQuestions +from tests.app.views.handlers.conftest import set_storage_data + + +@pytest.mark.usefixtures("app") +@freeze_time("2022-06-01T15:34:54+00:00") +def test_view_preview_questions_context(storage, language): + submitted_at = datetime.now(timezone.utc) + set_storage_data(storage, submitted_at=submitted_at) + + questionnaire_store = QuestionnaireStore(storage) + schema = QuestionnaireSchema({"preview_questions": True}) + preview = ViewPreviewQuestions(schema, questionnaire_store, language) + + assert preview.get_context() == { + "hide_sign_out_button": True, + "pdf_url": "/questionnaire/preview/download-pdf", + "preview": {"sections": []}, + } + + +def test_not_enabled(storage, language): + submitted_at = datetime.now(timezone.utc) + set_storage_data(storage, submitted_at=submitted_at) + + questionnaire_store = QuestionnaireStore(storage) + + with pytest.raises(PreviewNotEnabledException): + preview_questions_pdf = PreviewQuestionsPDF( + QuestionnaireSchema({"preview_questions": False}), + questionnaire_store, + language, + ) + preview_questions_pdf.get_context() diff --git a/tests/app/views/handlers/test_view_submitted_response_pdf.py b/tests/app/views/handlers/test_view_submitted_response_pdf.py index 5b2f9a93ac..f046491d9a 100644 --- a/tests/app/views/handlers/test_view_submitted_response_pdf.py +++ b/tests/app/views/handlers/test_view_submitted_response_pdf.py @@ -1,12 +1,12 @@ from datetime import datetime, timedelta, timezone import pytest +from freezegun import freeze_time from app.data_models import QuestionnaireStore from app.views.handlers.view_submitted_response import ViewSubmittedResponseExpired from app.views.handlers.view_submitted_response_pdf import ViewSubmittedResponsePDF - -from .conftest import set_storage_data +from tests.app.views.handlers.conftest import set_storage_data @pytest.mark.usefixtures("app") @@ -25,6 +25,7 @@ def test_pdf_not_downloadable(storage, schema, language): @pytest.mark.usefixtures("app") +@freeze_time("2022-06-01T15:34:54+00:00") def test_filename_uses_schema_name(storage, schema, language): submitted_at = datetime.now(timezone.utc) set_storage_data(storage, submitted_at=submitted_at) @@ -33,4 +34,4 @@ def test_filename_uses_schema_name(storage, schema, language): questionnaire_store.set_metadata({"schema_name": "test_view_submitted_response"}) pdf = ViewSubmittedResponsePDF(schema, questionnaire_store, language) - assert pdf.filename == "test_view_submitted_response.pdf" + assert pdf.filename == "test-schema-view-submitted-response-2022-06-01.pdf" diff --git a/tests/functional/base_pages/base.page.js b/tests/functional/base_pages/base.page.js index 15f9e3925c..89b1bfb23e 100644 --- a/tests/functional/base_pages/base.page.js +++ b/tests/functional/base_pages/base.page.js @@ -20,7 +20,7 @@ export default class BasePage { } acceptCookies() { - return ".ons-js-accept-cookies"; + return '[data-button="accept"]'; } submit() { diff --git a/tests/functional/base_pages/calculated-summary.page.js b/tests/functional/base_pages/calculated-summary.page.js index e147911349..1ac4b20ff7 100644 --- a/tests/functional/base_pages/calculated-summary.page.js +++ b/tests/functional/base_pages/calculated-summary.page.js @@ -1,6 +1,6 @@ import BasePage from "./base.page"; -class CalculatedSummaryPage extends BasePage { +class CalculatedSummaryBasePage extends BasePage { calculatedSummaryTitle() { return '[data-qa="calculated-summary-title"]'; } @@ -12,6 +12,10 @@ class CalculatedSummaryPage extends BasePage { calculatedSummaryAnswer() { return "[data-qa=calculated-summary-answer]"; } + + summaryItems() { + return "dl.ons-summary__items"; + } } -export default CalculatedSummaryPage; +export default CalculatedSummaryBasePage; diff --git a/tests/functional/base_pages/census-thank-you.page.js b/tests/functional/base_pages/census-thank-you.page.js deleted file mode 100644 index 07e2b81eda..0000000000 --- a/tests/functional/base_pages/census-thank-you.page.js +++ /dev/null @@ -1,21 +0,0 @@ -import BasePage from "./base.page"; - -class CensusThankYouPage extends BasePage { - title() { - return '[data-qa="title"]'; - } - - exit() { - return '[data-qa="btn-exit"]'; - } - - feedback() { - return ".ons-feedback"; - } - - feedbackLink() { - return ".ons-feedback__link"; - } -} - -export default new CensusThankYouPage("thank-you"); diff --git a/tests/functional/base_pages/confirm-email.page.js b/tests/functional/base_pages/confirm-email.page.js index 8238b8c9c1..db533e8b0c 100644 --- a/tests/functional/base_pages/confirm-email.page.js +++ b/tests/functional/base_pages/confirm-email.page.js @@ -1,6 +1,6 @@ import BasePage from "./base.page"; -class ConfirmEmailPage extends BasePage { +class ConfirmEmailBasePage extends BasePage { questionTitle() { return '[data-qa="confirm-email-title"]'; } @@ -14,7 +14,7 @@ class ConfirmEmailPage extends BasePage { } errorPanel() { - return `[data-qa=error-body] div.ons-panel__body > ol`; + return `[data-qa=error-body] div.ons-panel__body > [data-qa=error-list]`; } } -export default new ConfirmEmailPage("confirm-email"); +export default new ConfirmEmailBasePage("confirm-email"); diff --git a/tests/functional/base_pages/confirmation-email-sent.page.js b/tests/functional/base_pages/confirmation-email-sent.page.js index 38501c2dc5..ea8e34bd89 100644 --- a/tests/functional/base_pages/confirmation-email-sent.page.js +++ b/tests/functional/base_pages/confirmation-email-sent.page.js @@ -1,6 +1,6 @@ import BasePage from "./base.page"; -class ConfirmationEmailSentPage extends BasePage { +class ConfirmationEmailSentBasePage extends BasePage { confirmationText() { return '[data-qa="confirmation-text"]'; } @@ -17,4 +17,4 @@ class ConfirmationEmailSentPage extends BasePage { return ".ons-feedback__link"; } } -export default new ConfirmationEmailSentPage("email-confirmation"); +export default new ConfirmationEmailSentBasePage("email-confirmation"); diff --git a/tests/functional/base_pages/confirmation-email.page.js b/tests/functional/base_pages/confirmation-email.page.js index 46d09945c8..1c3d44b4fd 100644 --- a/tests/functional/base_pages/confirmation-email.page.js +++ b/tests/functional/base_pages/confirmation-email.page.js @@ -1,6 +1,6 @@ import BasePage from "./base.page"; -class ConfirmationEmailSentPage extends BasePage { +class ConfirmationEmailBasePage extends BasePage { title() { return '[data-qa="title"]'; } @@ -10,11 +10,11 @@ class ConfirmationEmailSentPage extends BasePage { } errorPanel() { - return `[data-qa=error-body] div.ons-panel__body > ol`; + return `[data-qa=error-body] div.ons-panel__body > [data-qa=error-list]`; } feedback() { return ".ons-feedback"; } } -export default new ConfirmationEmailSentPage("email-confirmation"); +export default new ConfirmationEmailBasePage("email-confirmation"); diff --git a/tests/functional/base_pages/feedback-sent.page.js b/tests/functional/base_pages/feedback-sent.page.js index 7c49ca4286..69cb868e85 100644 --- a/tests/functional/base_pages/feedback-sent.page.js +++ b/tests/functional/base_pages/feedback-sent.page.js @@ -1,6 +1,6 @@ import FeedbackBasePage from "./feedback-base.page.js"; -class FeedbackSentPage extends FeedbackBasePage { +class FeedbackSentBasePage extends FeedbackBasePage { feedbackThankYouText() { return '[data-qa="feedback-thank-you-text"]'; } @@ -9,4 +9,4 @@ class FeedbackSentPage extends FeedbackBasePage { return '[data-qa="btn-done"]'; } } -export default new FeedbackSentPage("sent"); +export default new FeedbackSentBasePage("sent"); diff --git a/tests/functional/base_pages/feedback.page.js b/tests/functional/base_pages/feedback.page.js index 901b37fd0a..b561de7283 100644 --- a/tests/functional/base_pages/feedback.page.js +++ b/tests/functional/base_pages/feedback.page.js @@ -9,10 +9,6 @@ class FeedbackPage extends FeedbackBasePage { return "#feedback-type"; } - feedbackTypeCensusQuestions() { - return "#feedback-type-0"; - } - feedbackTypePageDesignAndStructure() { return "#feedback-type-1"; } diff --git a/tests/functional/base_pages/grand-calculated-summary.page.js b/tests/functional/base_pages/grand-calculated-summary.page.js new file mode 100644 index 0000000000..89f69bbfe5 --- /dev/null +++ b/tests/functional/base_pages/grand-calculated-summary.page.js @@ -0,0 +1,17 @@ +import BasePage from "./base.page"; + +class GrandCalculatedSummaryBasePage extends BasePage { + grandCalculatedSummaryTitle() { + return '[data-qa="grand-calculated-summary-title"]'; + } + + grandCalculatedSummaryQuestion() { + return "[data-qa=grand-calculated-summary-question]"; + } + + grandCalculatedSummaryAnswer() { + return "[data-qa=grand-calculated-summary-answer]"; + } +} + +export default GrandCalculatedSummaryBasePage; diff --git a/tests/functional/base_pages/hub.page.js b/tests/functional/base_pages/hub.page.js index 921619db39..6755160e98 100644 --- a/tests/functional/base_pages/hub.page.js +++ b/tests/functional/base_pages/hub.page.js @@ -1,12 +1,12 @@ import BasePage from "./base.page"; -class HubPage extends BasePage { +class HubBasePage extends BasePage { url() { return `/${this.pageName}/`; } summaryItems() { - return "table.ons-summary__items"; + return "dl.ons-summary__items"; } summaryRowState(sectionId) { @@ -22,4 +22,4 @@ class HubPage extends BasePage { } } -export default new HubPage("questionnaire"); +export default new HubBasePage("questionnaire"); diff --git a/tests/functional/base_pages/introduction.page.js b/tests/functional/base_pages/introduction.page.js index 7de98a03dd..bde3065313 100644 --- a/tests/functional/base_pages/introduction.page.js +++ b/tests/functional/base_pages/introduction.page.js @@ -32,6 +32,14 @@ class IntroductionBasePage extends BasePage { introDescription() { return "#use-of-information p"; } + + previewQuestions() { + return 'a[href="/questionnaire/preview"]'; + } + + introQuestion(number = 1) { + return `#intro-questions-${number}`; + } } export default IntroductionBasePage; diff --git a/tests/functional/base_pages/question-preview.page.js b/tests/functional/base_pages/question-preview.page.js new file mode 100644 index 0000000000..f9a6437d29 --- /dev/null +++ b/tests/functional/base_pages/question-preview.page.js @@ -0,0 +1,13 @@ +import BasePage from "./base.page"; + +class QuestionPreviewBasePage extends BasePage { + url() { + return `/submitted/feedback/${this.pageName}`; + } + + showButton() { + return '[data-ga-category="Preview Survey"]'; + } +} + +export default QuestionPreviewBasePage; diff --git a/tests/functional/base_pages/question.page.js b/tests/functional/base_pages/question.page.js index 8d96ed0ea0..fcd926369f 100644 --- a/tests/functional/base_pages/question.page.js +++ b/tests/functional/base_pages/question.page.js @@ -1,6 +1,6 @@ import BasePage from "./base.page"; -class QuestionPage extends BasePage { +class QuestionBasePage extends BasePage { constructor(pageName) { super(pageName); this.questions = []; @@ -51,4 +51,4 @@ class QuestionPage extends BasePage { } } -export default QuestionPage; +export default QuestionBasePage; diff --git a/tests/functional/base_pages/submit.page.js b/tests/functional/base_pages/submit.page.js index c07e9f6b92..d9d1e4ec81 100644 --- a/tests/functional/base_pages/submit.page.js +++ b/tests/functional/base_pages/submit.page.js @@ -14,7 +14,7 @@ class SubmitBasePage extends BasePage { } summaryShowAllButton() { - return ".ons-js-collapsible-all"; + return ".ons-accordion__toggle-all"; } } diff --git a/tests/functional/base_pages/thank-you.page.js b/tests/functional/base_pages/thank-you.page.js index 5921ec34ae..4ccef94430 100644 --- a/tests/functional/base_pages/thank-you.page.js +++ b/tests/functional/base_pages/thank-you.page.js @@ -1,6 +1,6 @@ import BasePage from "./base.page"; -class ThankYouPage extends BasePage { +class ThankYouBasePage extends BasePage { url() { return `/submitted/${this.pageName}`; } @@ -30,7 +30,7 @@ class ThankYouPage extends BasePage { } metadata() { - return ".ons-metadata"; + return ".ons-description-list"; } exitButton() { @@ -57,4 +57,4 @@ class ThankYouPage extends BasePage { return ".ons-feedback__link"; } } -export default new ThankYouPage("thank-you"); +export default new ThankYouBasePage("thank-you"); diff --git a/tests/functional/base_pages/view_submitted_response_base.page.js b/tests/functional/base_pages/view_submitted_response_base.page.js index 735ef3006c..785fc4589c 100644 --- a/tests/functional/base_pages/view_submitted_response_base.page.js +++ b/tests/functional/base_pages/view_submitted_response_base.page.js @@ -2,15 +2,15 @@ import BasePage from "./base.page"; class ViewSubmittedResponseBasePage extends BasePage { metadata() { - return ".ons-metadata"; + return ".ons-description-list"; } metadataTerm(number = 1) { - return `.ons-metadata > dt:nth-of-type(${number})`; + return `.ons-description-list > dt:nth-of-type(${number})`; } metadataValue(number = 1) { - return `.ons-metadata > dd:nth-of-type(${number})`; + return `.ons-description-list > dd:nth-of-type(${number})`; } informationPanel() { diff --git a/tests/functional/generate_pages.py b/tests/functional/generate_pages.py index 073bff6d0e..156b678297 100755 --- a/tests/functional/generate_pages.py +++ b/tests/functional/generate_pages.py @@ -6,6 +6,7 @@ import os import re from string import Template +from typing import Mapping, Sequence from app.utilities.json import json_loads @@ -84,25 +85,25 @@ """ DEFINITION_TITLE_GETTER = Template( - r""" definitionTitle(definitionIndex) { return `[data-qa='${definitionId}-${definitionIndex}-title']`; } + r""" definitionTitle() { return `[data-qa='${definitionId}-title']`; } """ ) DEFINITION_CONTENT_GETTER = Template( - r""" definitionContent(definitionIndex) { return `[data-qa='${definitionId}-${definitionIndex}-content']`; } + r""" definitionContent() { return `[data-qa='${definitionId}-content']`; } """ ) -DEFINITION_BUTTON_GETTER = Template( - r""" definitionButton(definitionIndex) { return `[data-qa='${definitionId}-${definitionIndex}-button']`; } +GUIDANCE_PANEL_GETTER = Template( + r""" guidancePanel(guidanceIndex) { return `[data-qa='${guidanceId}-${guidanceIndex}']`; } """ ) -GUIDANCE_PANEL_GETTER = Template( - r""" guidancePanel(guidanceIndex) { return `[data-qa='${guidanceId}-${guidanceIndex}']`; } +CONTENT_ITEM_GETTER = Template( + r""" ${contentName}Content() { return `#${contentId}`; } """ ) @@ -153,6 +154,14 @@ """ ) +ANSWER_NUMBERED_ERROR_LIST_GETTER = r""" errorList() { return `ol[data-qa="error-list"]`; } + +""" + +ANSWER_SINGLE_ERROR_LINK_GETTER = r""" singleErrorLink() { return `p[data-qa="error-list"]`; } + +""" + ANSWER_LABEL_DESCRIPTION_GETTER = Template( r""" ${answerName}LabelDescription() { return `#${answerId}-label-description-hint`; @@ -169,6 +178,22 @@ """ ) +ANSWER_SUFFIX_GETTER = Template( + r""" ${answerName}Suffix() { + return `#${answerId} + span`; + } + +""" +) + +QUESTION_LABELS_GETTER = r""" labels() { return `.ons-label`; } + +""" + +QUESTION_INPUTS_GETTER = r""" inputs() { return `[data-qa="input-text"]`; } + +""" + DYNAMIC_ANSWER_GETTER = Template( r""" answerByIndex(answerIndex) { return `#${answerId}-${answerIndex}`; @@ -211,6 +236,12 @@ """ ) +SUMMARY_GROUP_GETTER = Template( + r""" ${group_id_camel}Content(groupNumber) { return `#${group_id_without_number}-` + groupNumber; } + +""" +) + SUMMARY_QUESTION_GETTER = Template( r""" ${questionName}() { return `[data-qa=${questionId}]`; } @@ -244,7 +275,7 @@ """ LIST_SECTION_SUMMARY_LABEL_GETTER = Template( - r""" ${list_name}ListLabel(listItemInstance) { return `div[data-qa="${list_name}-list-summary"] td[data-qa="list-item-` + listItemInstance + `-label"]`; } + r""" ${list_name}ListLabel(listItemInstance) { return `div[data-qa="${list_name}-list-summary"] dt[data-qa="list-item-` + listItemInstance + `-label"]`; } """ ) @@ -268,6 +299,29 @@ """ ) +NON_ITEM_ANSWERS_LIST_SECTION_SUMMARY_LABEL_GETTER = Template( + r""" ${list_name}ListLabel(listItemInstance) { return `dt[data-qa="list-item-` + listItemInstance + `-label"]`; } + +""" +) + +NON_ITEM_ANSWERS_LIST_SECTION_SUMMARY_ADD_LINK_GETTER = Template( + r""" ${list_name}ListAddLink() { return `a[data-qa="add-item-link"]`; } + +""" +) + +NON_ITEM_ANSWERS_LIST_SECTION_SUMMARY_EDIT_LINK_GETTER = Template( + r""" ${list_name}ListEditLink(listItemInstance) { return `a[data-qa="list-item-change-` + listItemInstance + `-link"]`; } + +""" +) +NON_ITEM_ANSWERS_LIST_SECTION_SUMMARY_REMOVE_LINK_GETTER = Template( + r""" ${list_name}ListRemoveLink(listItemInstance) { return `a[data-qa="list-item-remove-` + listItemInstance + `-link"]`; } + +""" +) + RELATIONSHIP_PLAYBACK_GETTER = r""" playback() { return `[class*="relationships__playback"]`; } """ @@ -401,6 +455,7 @@ def process_answer(answer, page_spec, long_names, page_name): elif answer["type"] in "Duration": page_spec.write(_write_duration_answer(answer["id"], answer["units"], prefix)) + page_spec.write(_write_duration_suffix(answer["id"], answer["units"], prefix)) elif answer["type"] == "Address": page_spec.write(_write_address_answer(answer["id"], prefix)) elif answer["type"] in { @@ -427,18 +482,22 @@ def process_answer(answer, page_spec, long_names, page_name): def process_question(question, page_spec, num_questions, page_name): long_names = long_names_required(question, num_questions) - if "definitions" in question: + if ("definition" in question) or ("definitions" in question): context = {"definitionId": "question-definition"} process_definition(context, page_spec) for answer in question.get("answers", []): process_answer(answer, page_spec, long_names, page_name) - question_or_answer_id = ( - question["id"] - if question["type"] in ["DateRange", "MutuallyExclusive"] - else question["answers"][0]["id"] - ) + try: + question_or_answer_id = ( + question["id"] + if question["type"] in ["DateRange", "MutuallyExclusive"] + else question.get("answers", [])[0].get("id", None) + ) + except IndexError: + question_or_answer_id = None + question_name = generate_pascal_case_from_id(question["id"]).replace(page_name, "") question_context = { "questionName": camel_case(question_name), @@ -447,6 +506,10 @@ def process_question(question, page_spec, num_questions, page_name): } page_spec.write(QUESTION_ERROR_PANEL.substitute(question_context)) page_spec.write(QUESTION_TITLE.substitute(question_context)) + page_spec.write(ANSWER_NUMBERED_ERROR_LIST_GETTER) + page_spec.write(ANSWER_SINGLE_ERROR_LINK_GETTER) + page_spec.write(QUESTION_LABELS_GETTER) + page_spec.write(QUESTION_INPUTS_GETTER) def process_calculated_summary(answers, page_spec): @@ -525,32 +588,71 @@ def process_view_submitted_response(schema_data, require_path, dir_out, spec_fil def process_definition(context, page_spec): page_spec.write(DEFINITION_TITLE_GETTER.safe_substitute(context)) page_spec.write(DEFINITION_CONTENT_GETTER.safe_substitute(context)) - page_spec.write(DEFINITION_BUTTON_GETTER.safe_substitute(context)) def process_guidance(context, page_spec): page_spec.write(GUIDANCE_PANEL_GETTER.safe_substitute(context)) -def write_summary_spec(page_spec, section, collapsible, answers_are_editable=False): +def process_content(context, page_spec): + page_spec.write(CONTENT_ITEM_GETTER.safe_substitute(context)) + + +# pylint: disable=too-many-locals +def write_summary_spec( + page_spec, + section, + collapsible, + answers_are_editable=False, + show_non_item_answers=False, +): list_summaries = [ summary_element for summary_element in section.get("summary", {}).get("items", []) if summary_element["type"] == "List" ] for list_block in list_summaries: - list_context = {"list_name": list_block["for_list"]} - if answers_are_editable: - page_spec.write( - LIST_SECTION_SUMMARY_ADD_LINK_GETTER.substitute(list_context) - ) - page_spec.write( - LIST_SECTION_SUMMARY_EDIT_LINK_GETTER.substitute(list_context) + list_context = { + "list_name": camel_case( + generate_pascal_case_from_id(list_block["for_list"]) ) + } + if answers_are_editable: + if show_non_item_answers: + page_spec.write( + NON_ITEM_ANSWERS_LIST_SECTION_SUMMARY_ADD_LINK_GETTER.substitute( + list_context + ) + ) + page_spec.write( + NON_ITEM_ANSWERS_LIST_SECTION_SUMMARY_EDIT_LINK_GETTER.substitute( + list_context + ) + ) + page_spec.write( + NON_ITEM_ANSWERS_LIST_SECTION_SUMMARY_REMOVE_LINK_GETTER.substitute( + list_context + ) + ) + + else: + page_spec.write( + LIST_SECTION_SUMMARY_ADD_LINK_GETTER.substitute(list_context) + ) + page_spec.write( + LIST_SECTION_SUMMARY_EDIT_LINK_GETTER.substitute(list_context) + ) + page_spec.write( + LIST_SECTION_SUMMARY_REMOVE_LINK_GETTER.substitute(list_context) + ) + if show_non_item_answers: page_spec.write( - LIST_SECTION_SUMMARY_REMOVE_LINK_GETTER.substitute(list_context) + NON_ITEM_ANSWERS_LIST_SECTION_SUMMARY_LABEL_GETTER.substitute( + list_context + ) ) - page_spec.write(LIST_SECTION_SUMMARY_LABEL_GETTER.substitute(list_context)) + else: + page_spec.write(LIST_SECTION_SUMMARY_LABEL_GETTER.substitute(list_context)) for group in section["groups"]: for block in group["blocks"]: @@ -581,9 +683,11 @@ def write_summary_spec(page_spec, section, collapsible, answers_are_editable=Fal if not collapsible: group_context = { "group_id_camel": camel_case(generate_pascal_case_from_id(group["id"])), - "group_id": group["id"], + "group_id": f'{group["id"]}-0', + "group_id_without_number": f'{group["id"]}', } page_spec.write(SUMMARY_TITLE_GETTER.substitute(group_context)) + page_spec.write(SUMMARY_GROUP_GETTER.substitute(group_context)) def long_names_required(question, num_questions): @@ -643,8 +747,23 @@ def _write_duration_answer(answer_id, units, prefix): resp.append( ANSWER_GETTER.substitute( { - "answerName": prefix + unit.title(), - "answerId": answer_id + "-" + unit, + "answerName": f"{prefix}{unit.title()}", + "answerId": f"{answer_id}-{unit}", + } + ) + ) + + return "".join(resp) + + +def _write_duration_suffix(answer_id, units, prefix): + resp = [] + for unit in units: + resp.append( + ANSWER_SUFFIX_GETTER.substitute( + { + "answerName": f"{prefix}{unit.title()}", + "answerId": f"{answer_id}-{unit}", } ) ) @@ -712,6 +831,17 @@ def process_block( if not page_filename: page_filename = block["id"] + ".page.js" + if block["type"] in {"ListCollector", "ListCollectorContent"}: + for repeating_block in block.get("repeating_blocks", []): + process_block( + repeating_block, + dir_out, + schema_data, + spec_file, + relative_require, + page_filename=f'{repeating_block["id"]}-repeating-block.page.js', + ) + if block["type"] == "ListCollector": list_operations = ["add", "edit", "remove"] for list_operation in list_operations: @@ -751,13 +881,17 @@ def process_block( with open(page_path, "w", encoding="utf-8") as page_spec: page_name = generate_pascal_case_from_id(block["id"]) - base_page = "QuestionPage" + base_page = "QuestionBasePage" base_page_file = "question.page" if block["type"] == "CalculatedSummary": - base_page = "CalculatedSummaryPage" + base_page = "CalculatedSummaryBasePage" base_page_file = "calculated-summary.page" + if block["type"] == "GrandCalculatedSummary": + base_page = "GrandCalculatedSummaryBasePage" + base_page_file = "grand-calculated-summary.page" + if block["type"] == "Introduction": base_page = "IntroductionPageBase" base_page_file = "introduction.page" @@ -777,6 +911,13 @@ def process_block( has_guidance = False for content in block.get("primary_content", []): contents_block = content.get("contents") + content_context = { + "contentId": content["id"], + "contentName": camel_case( + generate_pascal_case_from_id(content["id"]) + ), + } + process_content(content_context, page_spec) if contents_block and _has_guidance_in_primary_contents(contents_block): has_guidance = True @@ -786,9 +927,38 @@ def process_block( process_guidance(context, page_spec) elif block["type"] == "CalculatedSummary": - process_calculated_summary( - block["calculation"]["answers_to_calculate"], page_spec + if block["calculation"].get("answers_to_calculate"): + process_calculated_summary( + block["calculation"]["answers_to_calculate"], page_spec + ) + else: + values = _get_dictionaries_with_key( + "source", block["calculation"]["operation"] + ) + + calculated_summary_answer_ids = [ + value["identifier"] + for value in values + if value["source"] == "answers" + ] + + process_calculated_summary(calculated_summary_answer_ids, page_spec) + + elif block["type"] == "GrandCalculatedSummary": + values = _get_dictionaries_with_key( + "source", block["calculation"]["operation"] ) + + calculated_summary_ids = [ + value["identifier"] + for value in values + if value["source"] == "calculated_summary" + ] + + # each calculated summary in a grand calculated summary is constructed such that it will have a single "answer" linking back to it + # so the processing for calculated summaries can be directly reused. + process_calculated_summary(calculated_summary_ids, page_spec) + elif block["type"] == "Interstitial": has_definition = False if "content_variants" in block: @@ -816,9 +986,11 @@ def process_block( process_question(question, page_spec, num_questions, page_name) if block["type"] == "ListCollector": - page_spec.write(LIST_SUMMARY_LABEL_GETTER) page_spec.write(LIST_SUMMARY_EDIT_LINK_GETTER) page_spec.write(LIST_SUMMARY_REMOVE_LINK_GETTER) + + if block["type"] in {"ListCollector", "ListCollectorContent"}: + page_spec.write(LIST_SUMMARY_LABEL_GETTER) page_spec.write(LIST_SUMMARY_LIST_GETTER) if block["type"] == "UnrelatedQuestion": @@ -838,6 +1010,20 @@ def _has_guidance_in_primary_contents(block_contents): return any("guidance" in element for element in block_contents) +def _get_dictionaries_with_key( + key, + dictionary, +): + if key in dictionary: + yield dictionary + + for value in dictionary.values(): + if isinstance(value, Sequence): + for element in value: + if isinstance(element, Mapping): + yield from _get_dictionaries_with_key(key, element) + + def process_schema(in_schema, out_dir, spec_file, require_path=".."): try: with open(in_schema, encoding="utf-8") as schema: @@ -897,7 +1083,6 @@ def process_section_summary( logger.info("creating %s...", page_path) with open(page_path, "w", encoding="utf-8") as page_spec: - section_context = { "pageName": generate_pascal_case_from_id(section_id), "basePage": "SubmitBasePage", @@ -913,11 +1098,18 @@ def process_section_summary( page_spec.write(CLASS_NAME.substitute(section_context)) page_spec.write(CONSTRUCTOR.substitute(section_context)) page_spec.write(SECTION_SUMMARY_PAGE_URL) + + show_non_item_answers = False + if summary := section.get("summary"): + if summary.get("show_non_item_answers"): + show_non_item_answers = True + write_summary_spec( page_spec, section, collapsible=False, answers_are_editable=True, + show_non_item_answers=show_non_item_answers, ) page_spec.write(FOOTER.substitute(section_context)) diff --git a/tests/functional/helpers.js b/tests/functional/helpers.js index 84148d5406..2bd7be5287 100644 --- a/tests/functional/helpers.js +++ b/tests/functional/helpers.js @@ -1,9 +1,57 @@ -const checkPeopleInList = (peopleExpected, listLabel) => { - $(listLabel(1)).waitForDisplayed(); +export const checkItemsInList = async (itemsExpected, listLabel) => { + await $(listLabel(1)).waitForDisplayed(); - for (let i = 1; i <= peopleExpected.length; i++) { - expect($(listLabel(i)).getText()).to.equal(peopleExpected[i - 1]); + for (let i = 1; i <= itemsExpected.length; i++) { + await expect(await $(listLabel(i)).getText()).toContain(itemsExpected[i - 1]); } }; -export default checkPeopleInList; +export const summaryItemComplete = async (summaryItemLabel, status) => { + await expect(await $(summaryItemLabel).$(`.ons-summary__item-title-icon.ons-summary__item-title-icon--check`).isExisting()).toBe(status); +}; + +export const listItemComplete = async (listItemLabel, status) => { + await expect(await $(listItemLabel).$(`.ons-list__prefix.ons-list__prefix--icon-check`).isExisting()).toBe(status); +}; + +const assertSummaryFunction = (selector) => { + return async (entities) => { + // check each summary value/item/title is present and that the number of them matches what is on the page + await entities.map(async (entity, index) => { + await expect(await $$(selector)[index].getText()).toEqual(entity); + }); + await expect(await $$(selector).length).toEqual(entities.length); + }; +}; + +export const assertSummaryValues = assertSummaryFunction(".ons-summary__values"); +export const assertSummaryTitles = assertSummaryFunction(".ons-summary__title"); +export const assertSummaryItems = assertSummaryFunction(".ons-summary__item--text"); + +export const repeatingAnswerChangeLink = (answerIndex) => { + return $$('dd[class="ons-summary__actions"]')[answerIndex].$("a"); +}; + +export const listItemIds = () => { + return $$("[data-list-item-id]").map((listItem) => listItem.getAttribute("data-list-item-id")); +}; + +export const click = async (selector) => { + // This was introduced due to a css property on ons-btn: + // .ons-btn:active { + // top: 0.1666666667em + // } + // When the button is right on the very edge of the screen, webdriverio sees that the button is accessible, so does not scroll + // but clicks down on the very top of the button which moves down and just below the mouse. When the mouse click is released + // it's no longer over the button and the click silently fails. This means that when the test comes to do assertions on the following page + // they fail, as we never navigated to that page. + await $(selector).scrollIntoView({ block: "center", inline: "center" }); + await $(selector).click(); + + // Allow time in case the click loads a new page. + await browser.pause(100); +}; + +export const verifyUrlContains = async (expectedUrlString) => { + await expect(browser).toHaveUrl(expect.stringContaining(expectedUrlString)); +}; diff --git a/tests/functional/jwt_helper.js b/tests/functional/jwt_helper.js index 7c01784f1d..ba8d84c65d 100644 --- a/tests/functional/jwt_helper.js +++ b/tests/functional/jwt_helper.js @@ -61,24 +61,31 @@ export function getRandomString(length) { export function generateToken( schema, { + launchVersion, + theme, userId, collectionId, responseId, + surveyId = "123", periodId = "201605", periodStr = "May 2016", + ruRef = "12345678901A", + sdsDatasetId = null, regionCode = "GB-ENG", languageCode = "en", - sexualIdentity = false, includeLogoutUrl = true, - country = "", - locality = "", - townName = "", - postcode = "", displayAddress = "", - } + cirInstrumentId = null, + booleanFlag = false, + }, ) { - const schemaParts = schemaRegEx.exec(schema); - + let schemaParams = {}; + if (schema) { + const schemaParts = schemaRegEx.exec(schema); + schemaParams = { schema_name: `${schemaParts[1]}_${schemaParts[2]}` }; + } else if (cirInstrumentId) { + schemaParams = { cir_instrument_id: cirInstrumentId }; + } // Header const oHeader = { alg: "RS256", @@ -87,34 +94,29 @@ export function generateToken( }; // Payload + const txId = uuidv4(); + const jti = uuidv4(); + const iat = KJUR.jws.IntDate.get("now"); + const exp = KJUR.jws.IntDate.get("now") + 1800; + const caseId = uuidv4(); + const currentDate = new Date(); + currentDate.setUTCDate(currentDate.getUTCDate() + 1); + const isoDate = currentDate.toISOString(); const oPayload = { - tx_id: uuidv4(), - jti: uuidv4(), - iat: KJUR.jws.IntDate.get("now"), - exp: KJUR.jws.IntDate.get("now") + 1800, - user_id: userId, - case_id: uuidv4(), - ru_ref: "12346789012A", + tx_id: txId, + jti, + iat, + exp, + case_id: caseId, response_id: responseId, - ru_name: "Apple", - trad_as: "Apple", - schema_name: `${schemaParts[1]}_${schemaParts[2]}`, + ...schemaParams, collection_exercise_sid: collectionId, - period_id: periodId, - period_str: periodStr, - ref_p_start_date: "2017-01-01", - ref_p_end_date: "2017-02-01", - employment_date: "2016-06-10", - return_by: "2017-03-01", - country, - locality, - town_name: townName, - postcode, - display_address: displayAddress, region_code: regionCode, language_code: languageCode, - sexual_identity: sexualIdentity, account_service_url: "http://localhost:8000", + survey_metadata: getSurveyMetadata(theme, userId, displayAddress, surveyId, periodId, periodStr, ruRef, sdsDatasetId, booleanFlag), + version: launchVersion, + response_expires_at: isoDate, }; if (includeLogoutUrl) { @@ -156,3 +158,38 @@ export function generateToken( return token; }); } + +function getSurveyMetadata(theme, userId, displayAddress, surveyId, periodId, periodStr, ruRef, sdsDatasetId, booleanFlag) { + let surveyMetadata = {}; + + if (theme === "social") { + surveyMetadata = { + data: { + case_ref: "1000000000000001", + qid: "1000000000000001", + }, + receipting_keys: ["qid"], + }; + } else { + surveyMetadata = { + data: { + user_id: userId, + display_address: displayAddress, + ru_ref: ruRef, + survey_id: surveyId, + period_id: periodId, + period_str: periodStr, + sds_dataset_id: sdsDatasetId, + ref_p_start_date: "2017-01-01", + ref_p_end_date: "2017-02-01", + employment_date: "2016-06-10", + return_by: "2017-03-01", + ru_name: "Apple", + trad_as: "Apple", + boolean_flag: booleanFlag, + }, + }; + } + + return surveyMetadata; +} diff --git a/tests/functional/spec/answer_action_redirect_to_list_add_block_checkbox.spec.js b/tests/functional/spec/answer_action_redirect_to_list_add_block_checkbox.spec.js deleted file mode 100644 index d8efda5043..0000000000 --- a/tests/functional/spec/answer_action_redirect_to_list_add_block_checkbox.spec.js +++ /dev/null @@ -1,71 +0,0 @@ -import checkPeopleInList from "../helpers"; -import AnyoneLiveAtListCollector from "../generated_pages/answer_action_redirect_to_list_add_block_checkbox/anyone-else-live-at.page"; -import AnyoneLiveAtListCollectorAddPage from "../generated_pages/answer_action_redirect_to_list_add_block_checkbox/anyone-else-live-at-add.page"; -import AnyoneLiveAtListCollectorRemovePage from "../generated_pages/answer_action_redirect_to_list_add_block_checkbox/anyone-else-live-at-remove.page"; -import AnyoneUsuallyLiveAt from "../generated_pages/answer_action_redirect_to_list_add_block_checkbox/anyone-usually-live-at.page"; - -describe("Answer Action: Redirect To List Add Question (Checkbox)", () => { - describe('Given the user is on a question with a "RedirectToListAddBlock" action enabled', () => { - before("Launch survey", () => { - browser.openQuestionnaire("test_answer_action_redirect_to_list_add_block_checkbox.json"); - }); - - it('When the user selects "No", Then, they should be taken to the list collector.', () => { - $(AnyoneUsuallyLiveAt.no()).click(); - $(AnyoneUsuallyLiveAt.submit()).click(); - expect(browser.getUrl()).to.contain(AnyoneLiveAtListCollector.pageName); - }); - - it('When the user selects "Yes" then they should be taken to the list collector add question.', () => { - browser.url(AnyoneUsuallyLiveAt.url()); - $(AnyoneUsuallyLiveAt.iThinkSo()).click(); - $(AnyoneUsuallyLiveAt.submit()).click(); - expect(browser.getUrl()).to.contain(AnyoneLiveAtListCollectorAddPage.pageName); - expect(browser.getUrl()).to.contain("?previous=anyone-usually-live-at"); - }); - - it('When the user clicks the "Previous" link from the add question then they should be taken to the block they came from, not the list collector', () => { - $(AnyoneLiveAtListCollectorAddPage.previous()).click(); - expect(browser.getUrl()).to.contain(AnyoneUsuallyLiveAt.pageName); - }); - - it("When the user adds a household member, Then, they are taken to the list collector and the household members are displayed", () => { - $(AnyoneUsuallyLiveAt.submit()).click(); - $(AnyoneLiveAtListCollectorAddPage.firstName()).setValue("Marcus"); - $(AnyoneLiveAtListCollectorAddPage.lastName()).setValue("Twin"); - $(AnyoneLiveAtListCollectorAddPage.submit()).click(); - expect(browser.getUrl()).to.contain(AnyoneLiveAtListCollector.pageName); - - const peopleExpected = ["Marcus Twin"]; - checkPeopleInList(peopleExpected, AnyoneLiveAtListCollector.listLabel); - }); - - it('When the user click the "Previous" link from the list collector, Then, they are taken to the last complete block', () => { - $(AnyoneLiveAtListCollector.previous()).click(); - expect(browser.getUrl()).to.contain(AnyoneUsuallyLiveAt.pageName); - }); - - it("When the user resubmits the first block and then list is not empty, Then they are taken to the list collector", () => { - $(AnyoneUsuallyLiveAt.submit()).click(); - expect(browser.getUrl()).to.contain(AnyoneLiveAtListCollector.pageName); - }); - - it("When the users removes the only person (Marcus Twain), Then, they are shown an empty list collector", () => { - $(AnyoneLiveAtListCollector.listRemoveLink(1)).click(); - $(AnyoneLiveAtListCollectorRemovePage.yes()).click(); - $(AnyoneLiveAtListCollectorRemovePage.submit()).click(); - expect(browser.getUrl()).to.contain(AnyoneLiveAtListCollector.pageName); - expect($(AnyoneLiveAtListCollector.listLabel(1)).isExisting()).to.be.false; - }); - - it("When the user resubmits the first block and then list is empty, Then they are taken to the add question", () => { - expect(browser.getUrl()).to.contain(AnyoneLiveAtListCollector.pageName); - - $(AnyoneLiveAtListCollector.previous()).click(); - expect(browser.getUrl()).to.contain(AnyoneUsuallyLiveAt.pageName); - - $(AnyoneUsuallyLiveAt.submit()).click(); - expect(browser.getUrl()).to.contain(AnyoneLiveAtListCollectorAddPage.pageName); - }); - }); -}); diff --git a/tests/functional/spec/answer_action_redirect_to_list_add_block_radio.spec.js b/tests/functional/spec/answer_action_redirect_to_list_add_block_radio.spec.js deleted file mode 100644 index 9a4c6a942a..0000000000 --- a/tests/functional/spec/answer_action_redirect_to_list_add_block_radio.spec.js +++ /dev/null @@ -1,71 +0,0 @@ -import checkPeopleInList from "../helpers"; -import AnyoneLiveAtListCollector from "../generated_pages/answer_action_redirect_to_list_add_block_radio/anyone-else-live-at.page"; -import AnyoneLiveAtListCollectorAddPage from "../generated_pages/answer_action_redirect_to_list_add_block_radio/anyone-else-live-at-add.page"; -import AnyoneLiveAtListCollectorRemovePage from "../generated_pages/answer_action_redirect_to_list_add_block_radio/anyone-else-live-at-remove.page"; -import AnyoneUsuallyLiveAt from "../generated_pages/answer_action_redirect_to_list_add_block_radio/anyone-usually-live-at.page"; - -describe("Answer Action: Redirect To List Add Question (Radio)", () => { - describe('Given the user is on a question with a "RedirectToListAddBlock" action enabled', () => { - before("Launch survey", () => { - browser.openQuestionnaire("test_answer_action_redirect_to_list_add_block_radio.json"); - }); - - it('When the user answers "No", Then, they should be taken to straight the list collector.', () => { - $(AnyoneUsuallyLiveAt.no()).click(); - $(AnyoneUsuallyLiveAt.submit()).click(); - expect(browser.getUrl()).to.contain(AnyoneLiveAtListCollector.pageName); - }); - - it('When the user answers "Yes" then they should be taken to the list collector add question.', () => { - browser.url(AnyoneUsuallyLiveAt.url()); - $(AnyoneUsuallyLiveAt.yes()).click(); - $(AnyoneUsuallyLiveAt.submit()).click(); - expect(browser.getUrl()).to.contain(AnyoneLiveAtListCollectorAddPage.pageName); - expect(browser.getUrl()).to.contain("?previous=anyone-usually-live-at"); - }); - - it('When the user clicks the "Previous" link from the add question then they should be taken to the block they came from, not the list collector', () => { - $(AnyoneLiveAtListCollectorAddPage.previous()).click(); - expect(browser.getUrl()).to.contain(AnyoneUsuallyLiveAt.pageName); - }); - - it("When the user adds a household member, Then, they are taken to the list collector and the household members are displayed", () => { - $(AnyoneUsuallyLiveAt.submit()).click(); - $(AnyoneLiveAtListCollectorAddPage.firstName()).setValue("Marcus"); - $(AnyoneLiveAtListCollectorAddPage.lastName()).setValue("Twin"); - $(AnyoneLiveAtListCollectorAddPage.submit()).click(); - expect(browser.getUrl()).to.contain(AnyoneLiveAtListCollector.pageName); - - const peopleExpected = ["Marcus Twin"]; - checkPeopleInList(peopleExpected, AnyoneLiveAtListCollector.listLabel); - }); - - it('When the user click the "Previous" link from the list collector, Then, they are taken to the last complete block', () => { - $(AnyoneLiveAtListCollector.previous()).click(); - expect(browser.getUrl()).to.contain(AnyoneUsuallyLiveAt.pageName); - }); - - it("When the user resubmits the first block and then list is not empty, Then they are taken to the list collector", () => { - $(AnyoneUsuallyLiveAt.submit()).click(); - expect(browser.getUrl()).to.contain(AnyoneLiveAtListCollector.pageName); - }); - - it("When the users removes the only person (Marcus Twain), Then, they are shown an empty list collector", () => { - $(AnyoneLiveAtListCollector.listRemoveLink(1)).click(); - $(AnyoneLiveAtListCollectorRemovePage.yes()).click(); - $(AnyoneLiveAtListCollectorRemovePage.submit()).click(); - expect(browser.getUrl()).to.contain(AnyoneLiveAtListCollector.pageName); - expect($(AnyoneLiveAtListCollector.listLabel(1)).isExisting()).to.be.false; - }); - - it("When the user resubmits the first block and then list is empty, Then they are taken to the add question", () => { - expect(browser.getUrl()).to.contain(AnyoneLiveAtListCollector.pageName); - - $(AnyoneLiveAtListCollector.previous()).click(); - expect(browser.getUrl()).to.contain(AnyoneUsuallyLiveAt.pageName); - - $(AnyoneUsuallyLiveAt.submit()).click(); - expect(browser.getUrl()).to.contain(AnyoneLiveAtListCollectorAddPage.pageName); - }); - }); -}); diff --git a/tests/functional/spec/census_thank_you.spec.js b/tests/functional/spec/census_thank_you.spec.js deleted file mode 100644 index 62d643d5c9..0000000000 --- a/tests/functional/spec/census_thank_you.spec.js +++ /dev/null @@ -1,19 +0,0 @@ -import { SubmitPage } from "../base_pages/submit.page.js"; -import HubPage from "../base_pages/hub.page"; - -import ThankYouPage from "../base_pages/thank-you.page"; - -describe("Thank You Census Household", () => { - describe("Given I launch a census schema without feedback enabled", () => { - beforeEach(() => { - browser.openQuestionnaire("test_thank_you_census_household.json"); - }); - - it("When I navigate to the thank you page, Then I should not see the feedback call to action", () => { - $(SubmitPage.submit()).click(); - $(HubPage.submit()).click(); - expect(browser.getUrl()).to.contain(ThankYouPage.pageName); - expect($(ThankYouPage.feedback()).isExisting()).to.equal(false); - }); - }); -}); diff --git a/tests/functional/spec/checkbox.spec.js b/tests/functional/spec/checkbox.spec.js index a5ea63434e..141ba83afd 100644 --- a/tests/functional/spec/checkbox.spec.js +++ b/tests/functional/spec/checkbox.spec.js @@ -2,150 +2,151 @@ import MandatoryCheckboxPage from "../generated_pages/checkbox/mandatory-checkbo import NonMandatoryCheckboxPage from "../generated_pages/checkbox/non-mandatory-checkbox.page"; import singleCheckboxPage from "../generated_pages/checkbox/single-checkbox.page"; import SubmitPage from "../generated_pages/checkbox/submit.page"; +import { click, verifyUrlContains } from "../helpers"; describe('Checkbox with "other" option', () => { - beforeEach("Load the survey", () => { - browser.openQuestionnaire("test_checkbox.json"); + beforeEach("Load the survey", async () => { + await browser.openQuestionnaire("test_checkbox.json"); }); - it("Given a label has not been provided in the schema for a checkbox answer, When the checkbox answer is displayed, Then the default label should be visible", () => { - expect($("body").getText()).to.have.string("Select all that apply"); + it("Given a label has not been provided in the schema for a checkbox answer, When the checkbox answer is displayed, Then the default label should be visible", async () => { + await expect(await $("body").getText()).toContain("Select all that apply"); }); - it("Given a label has been set in the schema for a checkbox answer, When the checkbox answer is displayed, Then the label should be visible", () => { - $(MandatoryCheckboxPage.none()).click(); - $(MandatoryCheckboxPage.submit()).click(); - expect($("body").getText()).to.have.string("Select any answers that apply"); + it("Given a label has been set in the schema for a checkbox answer, When the checkbox answer is displayed, Then the label should be visible", async () => { + await $(MandatoryCheckboxPage.none()).click(); + await click(MandatoryCheckboxPage.submit()); + await expect(await $("body").getText()).toContain("Select any answers that apply"); }); - it("Given that there is only one checkbox, When the checkbox answer is displayed, Then no label should be present", () => { - $(MandatoryCheckboxPage.none()).click(); - $(MandatoryCheckboxPage.submit()).click(); - $(NonMandatoryCheckboxPage.submit()).click(); - expect($("body").getText()).to.not.have.string("Select all that apply"); + it("Given that there is only one checkbox, When the checkbox answer is displayed, Then no label should be present", async () => { + await $(MandatoryCheckboxPage.none()).click(); + await click(MandatoryCheckboxPage.submit()); + await click(NonMandatoryCheckboxPage.submit()); + await expect(await $("body").getText()).not.toBe("Select all that apply"); }); - it('Given an "other" option is available, when the user clicks the "other" option the other input should be visible.', () => { - expect($(MandatoryCheckboxPage.otherLabelDescription()).getText()).to.have.string("Choose any other topping"); - $(MandatoryCheckboxPage.other()).click(); - expect($(MandatoryCheckboxPage.otherDetail()).isDisplayed()).to.be.true; + it('Given an "other" option is available, when the user clicks the "other" option the other input should be visible.', async () => { + await expect(await $(MandatoryCheckboxPage.otherLabelDescription()).getText()).toBe("Choose any other topping"); + await $(MandatoryCheckboxPage.other()).click(); + await expect(await $(MandatoryCheckboxPage.otherDetail()).isDisplayed()).toBe(true); }); - it("Given a mandatory checkbox answer, When I select the other option, leave the input field empty and submit, Then an error should be displayed.", () => { + it("Given a mandatory checkbox answer, When I select the other option, leave the input field empty and submit, Then an error should be displayed.", async () => { // When - $(MandatoryCheckboxPage.other()).click(); - $(MandatoryCheckboxPage.submit()).click(); + await $(MandatoryCheckboxPage.other()).click(); + await click(MandatoryCheckboxPage.submit()); // Then - expect($(MandatoryCheckboxPage.error()).isDisplayed()).to.be.true; + await expect(await $(MandatoryCheckboxPage.error()).isDisplayed()).toBe(true); }); - it("Given a mandatory checkbox answer, When I leave the input field empty and submit, Then the question text should be hidden in the error message using a span element.", () => { + it("Given a mandatory checkbox answer, When I leave the input field empty and submit, Then the question text should be hidden in the error message using a span element.", async () => { // When - $(MandatoryCheckboxPage.submit()).click(); + await click(MandatoryCheckboxPage.submit()); // Then - expect($(MandatoryCheckboxPage.error()).getHTML()).to.contain( - 'Select at least one answer to ‘Which pizza toppings would you like?’' + await expect(await $(MandatoryCheckboxPage.error()).getHTML()).toContain( + 'Select at least one answer to ‘Which pizza toppings would you like?’', ); }); - it("Given a mandatory checkbox answer, when there is an error on the page for other field and I enter valid value and submit page, then the error is cleared and I navigate to next page.s", () => { - $(MandatoryCheckboxPage.other()).click(); - $(MandatoryCheckboxPage.submit()).click(); - expect($(MandatoryCheckboxPage.error()).isDisplayed()).to.be.true; + it("Given a mandatory checkbox answer, when there is an error on the page for other field and I enter valid value and submit page, then the error is cleared and I navigate to next page.", async () => { + await $(MandatoryCheckboxPage.other()).click(); + await click(MandatoryCheckboxPage.submit()); + await expect(await $(MandatoryCheckboxPage.error()).isDisplayed()).toBe(true); // When - $(MandatoryCheckboxPage.otherDetail()).setValue("Other Text"); - $(MandatoryCheckboxPage.submit()).click(); - expect(browser.getUrl()).to.contain(NonMandatoryCheckboxPage.pageName); + await $(MandatoryCheckboxPage.otherDetail()).setValue("Other Text"); + await click(MandatoryCheckboxPage.submit()); + await verifyUrlContains(NonMandatoryCheckboxPage.pageName); }); - it('Given a non-mandatory checkbox answer, when the user does not select an option, then "No answer provided" should be displayed on the summary screen', () => { + it('Given a non-mandatory checkbox answer, when the user does not select an option, then "No answer provided" should be displayed on the summary screen', async () => { // When - $(MandatoryCheckboxPage.other()).click(); - $(MandatoryCheckboxPage.otherDetail()).setValue("Other value"); - $(MandatoryCheckboxPage.submit()).click(); - $(NonMandatoryCheckboxPage.submit()).click(); - $(singleCheckboxPage.submit()).click(); + await $(MandatoryCheckboxPage.other()).click(); + await $(MandatoryCheckboxPage.otherDetail()).setValue("Other value"); + await click(MandatoryCheckboxPage.submit()); + await click(NonMandatoryCheckboxPage.submit()); + await click(singleCheckboxPage.submit()); // Then - expect($(SubmitPage.nonMandatoryCheckboxAnswer()).getText()).to.contain("No answer provided"); + await expect(await $(SubmitPage.nonMandatoryCheckboxAnswer()).getText()).toBe("No answer provided"); }); - it('Given a non-mandatory checkbox answer, when the user selects Other but does not supply a value, then "Other" should be displayed on the summary screen', () => { + it('Given a non-mandatory checkbox answer, when the user selects Other but does not supply a value, then "Other" should be displayed on the summary screen', async () => { // When - $(MandatoryCheckboxPage.other()).click(); - $(MandatoryCheckboxPage.otherDetail()).setValue("Other value"); - $(MandatoryCheckboxPage.submit()).click(); - $(NonMandatoryCheckboxPage.other()).click(); - $(NonMandatoryCheckboxPage.submit()).click(); - $(singleCheckboxPage.submit()).click(); + await $(MandatoryCheckboxPage.other()).click(); + await $(MandatoryCheckboxPage.otherDetail()).setValue("Other value"); + await click(MandatoryCheckboxPage.submit()); + await $(NonMandatoryCheckboxPage.other()).click(); + await click(NonMandatoryCheckboxPage.submit()); + await click(singleCheckboxPage.submit()); // Then - expect($(SubmitPage.nonMandatoryCheckboxAnswer()).getText()).to.contain("Other"); + await expect(await $(SubmitPage.nonMandatoryCheckboxAnswer()).getText()).toBe("Other"); }); - it("Given a non-mandatory checkbox answer, when the user selects Other and supplies a value, then the supplied value should be displayed on the summary screen", () => { + it("Given a non-mandatory checkbox answer, when the user selects Other and supplies a value, then the supplied value should be displayed on the summary screen", async () => { // When - $(MandatoryCheckboxPage.other()).click(); - $(MandatoryCheckboxPage.otherDetail()).setValue("Other value"); - $(MandatoryCheckboxPage.submit()).click(); - $(NonMandatoryCheckboxPage.other()).click(); - $(NonMandatoryCheckboxPage.otherDetail()).setValue("The other value"); - $(NonMandatoryCheckboxPage.submit()).click(); - $(singleCheckboxPage.submit()).click(); + await $(MandatoryCheckboxPage.other()).click(); + await $(MandatoryCheckboxPage.otherDetail()).setValue("Other value"); + await click(MandatoryCheckboxPage.submit()); + await $(NonMandatoryCheckboxPage.other()).click(); + await $(NonMandatoryCheckboxPage.otherDetail()).setValue("The other value"); + await click(NonMandatoryCheckboxPage.submit()); + await click(singleCheckboxPage.submit()); // Then - expect($(SubmitPage.nonMandatoryCheckboxAnswer()).getText()).to.contain("The other value"); + await expect(await $(SubmitPage.nonMandatoryCheckboxAnswer()).getText()).toContain("The other value"); }); - it("Given that there is an escaped character in an answer label, when the user selects the answer, then the label should be displayed on the summary screen", () => { + it("Given that there is an escaped character in an answer label, when the user selects the answer, then the label should be displayed on the summary screen", async () => { // When - $(MandatoryCheckboxPage.hamCheese()).click(); - $(MandatoryCheckboxPage.submit()).click(); - $(NonMandatoryCheckboxPage.other()).click(); - $(NonMandatoryCheckboxPage.otherDetail()).setValue("The other value"); - $(NonMandatoryCheckboxPage.submit()).click(); - $(singleCheckboxPage.submit()).click(); + await $(MandatoryCheckboxPage.hamCheese()).click(); + await click(MandatoryCheckboxPage.submit()); + await $(NonMandatoryCheckboxPage.other()).click(); + await $(NonMandatoryCheckboxPage.otherDetail()).setValue("The other value"); + await click(NonMandatoryCheckboxPage.submit()); + await click(singleCheckboxPage.submit()); // Then - expect($(SubmitPage.mandatoryCheckboxAnswer()).getText()).to.contain("Ham & Cheese"); + await expect(await $(SubmitPage.mandatoryCheckboxAnswer()).getText()).toBe("Ham & Cheese"); }); - it("Given I have previously added text in other textfield and saved, when I uncheck other options and select a different checkbox as answer, then the text entered in other field must be wiped.", () => { + it("Given I have previously added text in other textfield and saved, when I uncheck other options and select a different checkbox as answer, then the text entered in other field must be wiped.", async () => { // When - $(MandatoryCheckboxPage.other()).click(); - $(MandatoryCheckboxPage.otherDetail()).setValue("Other value"); - $(MandatoryCheckboxPage.submit()).click(); - $(NonMandatoryCheckboxPage.previous()).click(); - $(MandatoryCheckboxPage.other()).click(); - $(MandatoryCheckboxPage.hamCheese()).click(); - $(MandatoryCheckboxPage.submit()).click(); - $(NonMandatoryCheckboxPage.previous()).click(); + await $(MandatoryCheckboxPage.other()).click(); + await $(MandatoryCheckboxPage.otherDetail()).setValue("Other value"); + await click(MandatoryCheckboxPage.submit()); + await $(NonMandatoryCheckboxPage.previous()).click(); + await $(MandatoryCheckboxPage.other()).click(); + await $(MandatoryCheckboxPage.hamCheese()).click(); + await click(MandatoryCheckboxPage.submit()); + await $(NonMandatoryCheckboxPage.previous()).click(); // Then - $(MandatoryCheckboxPage.other()).click(); - expect($(MandatoryCheckboxPage.otherDetail()).getValue()).to.equal(""); + await $(MandatoryCheckboxPage.other()).click(); + await expect(await $(MandatoryCheckboxPage.otherDetail()).getValue()).toBe(""); }); - it("Given a mandatory checkbox answer, when the user selects only one option, then the answer should not be displayed as a list on the summary screen", () => { + it("Given a mandatory checkbox answer, when the user selects only one option, then the answer should not be displayed as a list on the summary screen", async () => { // When - $(MandatoryCheckboxPage.ham()).click(); - $(MandatoryCheckboxPage.submit()).click(); - $(NonMandatoryCheckboxPage.submit()).click(); + await $(MandatoryCheckboxPage.ham()).click(); + await click(MandatoryCheckboxPage.submit()); + await click(NonMandatoryCheckboxPage.submit()); // Then - const listLength = $$(`${SubmitPage.mandatoryCheckboxAnswer()} li`).length; + const listLength = await $$(`${SubmitPage.mandatoryCheckboxAnswer()} li`).length; // Then - expect(listLength).to.equal(0); + await expect(listLength).toBe(0); }); - it("Given a mandatory checkbox answer, when the user selects more than one option, then the answer should be displayed as a list on the summary screen", () => { + it("Given a mandatory checkbox answer, when the user selects more than one option, then the answer should be displayed as a list on the summary screen", async () => { // When - $(MandatoryCheckboxPage.ham()).click(); - $(MandatoryCheckboxPage.hamCheese()).click(); - $(MandatoryCheckboxPage.submit()).click(); - $(NonMandatoryCheckboxPage.submit()).click(); - $(singleCheckboxPage.submit()).click(); + await $(MandatoryCheckboxPage.ham()).click(); + await $(MandatoryCheckboxPage.hamCheese()).click(); + await click(MandatoryCheckboxPage.submit()); + await click(NonMandatoryCheckboxPage.submit()); + await click(singleCheckboxPage.submit()); - const listLength = $$(`${SubmitPage.mandatoryCheckboxAnswer()} li`).length; + const listLength = await $$(`${SubmitPage.mandatoryCheckboxAnswer()} li`).length; // Then - expect(listLength).to.equal(2); + await expect(listLength).toBe(2); }); }); diff --git a/tests/functional/spec/components/address/address.spec.js b/tests/functional/spec/components/address/address.spec.js index aeeda4e142..db27e65147 100644 --- a/tests/functional/spec/components/address/address.spec.js +++ b/tests/functional/spec/components/address/address.spec.js @@ -2,86 +2,87 @@ import AddressConfirmation from "../../../generated_pages/address/address-confir import AddressMandatory from "../../../generated_pages/address/address-block-mandatory.page"; import AddressOptional from "../../../generated_pages/address/address-block-optional.page"; import SubmitPage from "../../../generated_pages/address/submit.page"; +import { click, verifyUrlContains } from "../../../helpers"; describe("Address Answer Type", () => { - beforeEach("Launch survey", () => { - browser.openQuestionnaire("test_address.json"); + beforeEach("Launch survey", async () => { + await browser.openQuestionnaire("test_address.json"); }); describe("Given the user is on an address input question", () => { - it("When the user enters all address fields, Then the summary displays the address fields", () => { - $(AddressMandatory.Line1()).setValue("Evelyn Street"); - $(AddressMandatory.Line2()).setValue("Apt 7"); - $(AddressMandatory.Town()).setValue("Barry"); - $(AddressMandatory.Postcode()).setValue("CF63 4JG"); + it("When the user enters all address fields, Then the summary displays the address fields", async () => { + await $(AddressMandatory.Line1()).setValue("Evelyn Street"); + await $(AddressMandatory.Line2()).setValue("Apt 7"); + await $(AddressMandatory.Town()).setValue("Barry"); + await $(AddressMandatory.Postcode()).setValue("CF63 4JG"); - $(AddressMandatory.submit()).click(); - $(AddressOptional.submit()).click(); - $(AddressConfirmation.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); - expect($(SubmitPage.addressMandatory()).getText()).to.equal("Evelyn Street\nApt 7\nBarry\nCF63 4JG"); - expect($(SubmitPage.addressMandatory()).getHTML()).to.contain("Evelyn Street
Apt 7
Barry
CF63 4JG"); + await click(AddressMandatory.submit()); + await click(AddressOptional.submit()); + await click(AddressConfirmation.submit()); + await verifyUrlContains(SubmitPage.pageName); + await expect(await $(SubmitPage.addressMandatory()).getText()).toBe("Evelyn Street\nApt 7\nBarry\nCF63 4JG"); + await expect(await $(SubmitPage.addressMandatory()).getHTML()).toContain("Evelyn Street
Apt 7
Barry
CF63 4JG"); }); }); describe("Given the user is on an address input question", () => { - it("When the user enters only address line 1, Then the summary only displays address line 1", () => { - $(AddressMandatory.Line1()).setValue("Evelyn Street"); + it("When the user enters only address line 1, Then the summary only displays address line 1", async () => { + await $(AddressMandatory.Line1()).setValue("Evelyn Street"); - $(AddressMandatory.submit()).click(); - $(AddressOptional.submit()).click(); - $(AddressConfirmation.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); - expect($(SubmitPage.addressMandatory()).getText()).to.equal("Evelyn Street"); + await click(AddressMandatory.submit()); + await click(AddressOptional.submit()); + await click(AddressConfirmation.submit()); + await verifyUrlContains(SubmitPage.pageName); + await expect(await $(SubmitPage.addressMandatory()).getText()).toBe("Evelyn Street"); }); }); describe("Given the user is on an mandatory address input question", () => { - it("When the user submits the page without entering address line 1, Then an error is displayed", () => { - $(AddressMandatory.submit()).click(); - expect($(AddressMandatory.error()).getText()).to.equal("Enter an address"); + it("When the user submits the page without entering address line 1, Then an error is displayed", async () => { + await click(AddressMandatory.submit()); + await expect(await $(AddressMandatory.error()).getText()).toBe("Enter an address"); }); }); describe("Given the user is on an optional address input question", () => { - it("When the user submits the page without entering any fields, Then the summary should display `No answer provided`.", () => { + it("When the user submits the page without entering any fields, Then the summary should display `No answer provided`.", async () => { // Get to optional address question - $(AddressMandatory.Line1()).setValue("Evelyn Street"); - $(AddressMandatory.submit()).click(); + await $(AddressMandatory.Line1()).setValue("Evelyn Street"); + await click(AddressMandatory.submit()); - $(AddressOptional.submit()).click(); - $(AddressConfirmation.submit()).click(); - expect($(SubmitPage.addressOptional()).getText()).to.equal("No answer provided"); + await click(AddressOptional.submit()); + await click(AddressConfirmation.submit()); + await expect(await $(SubmitPage.addressOptional()).getText()).toBe("No answer provided"); }); }); describe("Given the user has submitted an address answer type question", () => { - it("When the user revisits the address question page, Then all entered fields are filled in", () => { - $(AddressMandatory.Line1()).setValue("Evelyn Street"); - $(AddressMandatory.Line2()).setValue("Apt 7"); - $(AddressMandatory.Town()).setValue("Barry"); - $(AddressMandatory.Postcode()).setValue("CF63 4JG"); + it("When the user revisits the address question page, Then all entered fields are filled in", async () => { + await $(AddressMandatory.Line1()).setValue("Evelyn Street"); + await $(AddressMandatory.Line2()).setValue("Apt 7"); + await $(AddressMandatory.Town()).setValue("Barry"); + await $(AddressMandatory.Postcode()).setValue("CF63 4JG"); - $(AddressMandatory.submit()).click(); - expect(browser.getUrl()).to.contain(AddressOptional.pageName); + await click(AddressMandatory.submit()); + await verifyUrlContains(AddressOptional.pageName); - browser.url(AddressMandatory.url()); + await browser.url(AddressMandatory.url()); - expect($(AddressMandatory.Line1()).getValue()).to.contain("Evelyn Street"); - expect($(AddressMandatory.Line2()).getValue()).to.contain("Apt 7"); - expect($(AddressMandatory.Town()).getValue()).to.contain("Barry"); - expect($(AddressMandatory.Postcode()).getValue()).to.contain("CF63 4JG"); + await expect(await $(AddressMandatory.Line1()).getValue()).toBe("Evelyn Street"); + await expect(await $(AddressMandatory.Line2()).getValue()).toBe("Apt 7"); + await expect(await $(AddressMandatory.Town()).getValue()).toBe("Barry"); + await expect(await $(AddressMandatory.Postcode()).getValue()).toBe("CF63 4JG"); }); }); describe("Given the user has submitted an address answer type question", () => { - it("When the user visits the address confirmation question page, Then the first line of the address is displayed", () => { - $(AddressMandatory.Line1()).setValue("Evelyn Street"); - $(AddressMandatory.Line2()).setValue("Apt 7"); - $(AddressMandatory.Town()).setValue("Barry"); - $(AddressMandatory.Postcode()).setValue("CF63 4JG"); - $(AddressMandatory.submit()).click(); - $(AddressOptional.submit()).click(); - expect($(AddressConfirmation.questionText()).getText()).to.equal("Please confirm the first line of your address is Evelyn Street"); + it("When the user visits the address confirmation question page, Then the first line of the address is displayed", async () => { + await $(AddressMandatory.Line1()).setValue("Evelyn Street"); + await $(AddressMandatory.Line2()).setValue("Apt 7"); + await $(AddressMandatory.Town()).setValue("Barry"); + await $(AddressMandatory.Postcode()).setValue("CF63 4JG"); + await click(AddressMandatory.submit()); + await click(AddressOptional.submit()); + await expect(await $(AddressConfirmation.questionText()).getText()).toBe("Please confirm the first line of your address is Evelyn Street"); }); }); }); diff --git a/tests/functional/spec/components/checkbox/checkbox_detail_answer.spec.js b/tests/functional/spec/components/checkbox/checkbox_detail_answer.spec.js index 37b5031380..4ebb80789d 100644 --- a/tests/functional/spec/components/checkbox/checkbox_detail_answer.spec.js +++ b/tests/functional/spec/components/checkbox/checkbox_detail_answer.spec.js @@ -2,37 +2,38 @@ import CheckboxVisibleTruePage from "../../../generated_pages/checkbox_detail_an import CheckboxVisibleFalsePage from "../../../generated_pages/checkbox_detail_answer_textfield/checkbox-visible-false.page.js"; import CheckboxVisibleNonePage from "../../../generated_pages/checkbox_detail_answer_textfield/checkbox-visible-none.page.js"; import MutuallyExclusivePage from "../../../generated_pages/checkbox_detail_answer_textfield/mutually-exclusive.page.js"; +import { click } from "../../../helpers"; describe("Given the checkbox detail_answer questionnaire,", () => { - beforeEach(() => { - browser.openQuestionnaire("test_checkbox_detail_answer_textfield.json"); + beforeEach(async () => { + await browser.openQuestionnaire("test_checkbox_detail_answer_textfield.json"); }); - it("When a checkbox has a detail_answer with visible set to true, Then the detail answer write-in field should be shown", () => { - expect($(CheckboxVisibleTruePage.otherDetail()).isDisplayed()).to.be.true; + it("When a checkbox has a detail_answer with visible set to true, Then the detail answer write-in field should be shown", async () => { + await expect(await $(CheckboxVisibleTruePage.otherDetail()).isDisplayed()).toBe(true); }); - it("When a checkbox has a detail_answer with visible set to true and another answer is checked, then the detail answer write-in field should still be shown", () => { - $(CheckboxVisibleTruePage.coffee()).click(); - expect($(CheckboxVisibleTruePage.otherDetail()).isDisplayed()).to.be.true; + it("When a checkbox has a detail_answer with visible set to true and another answer is checked, then the detail answer write-in field should still be shown", async () => { + await $(CheckboxVisibleTruePage.coffee()).click(); + await expect(await $(CheckboxVisibleTruePage.otherDetail()).isDisplayed()).toBe(true); }); - it("When a checkbox has a detail_answer with visible set to false, Then the detail answer write-in field should not be shown", () => { - $(CheckboxVisibleTruePage.coffee()).click(); - $(CheckboxVisibleTruePage.submit()).click(); - expect($(CheckboxVisibleFalsePage.otherDetail()).isDisplayed()).to.be.false; + it("When a checkbox has a detail_answer with visible set to false, Then the detail answer write-in field should not be shown", async () => { + await $(CheckboxVisibleTruePage.coffee()).click(); + await click(CheckboxVisibleTruePage.submit()); + await expect(await $(CheckboxVisibleFalsePage.otherDetail()).isDisplayed()).toBe(false); }); - it("When a checkbox has a detail_answer with visible not set, Then the detail answer write-in field should not be shown", () => { - $(CheckboxVisibleTruePage.coffee()).click(); - $(CheckboxVisibleTruePage.submit()).click(); - $(CheckboxVisibleFalsePage.iceCream()).click(); - $(CheckboxVisibleFalsePage.submit()).click(); - expect($(CheckboxVisibleNonePage.otherDetail()).isDisplayed()).to.be.false; + it("When a checkbox has a detail_answer with visible not set, Then the detail answer write-in field should not be shown", async () => { + await $(CheckboxVisibleTruePage.coffee()).click(); + await click(CheckboxVisibleTruePage.submit()); + await $(CheckboxVisibleFalsePage.iceCream()).click(); + await click(CheckboxVisibleFalsePage.submit()); + await expect(await $(CheckboxVisibleNonePage.otherDetail()).isDisplayed()).toBe(false); }); - it("When a mutually exclusive checkbox has a detail_answer with visible set to true, Then the detail answer write-in field should be shown", () => { - $(CheckboxVisibleTruePage.coffee()).click(); - $(CheckboxVisibleTruePage.submit()).click(); - $(CheckboxVisibleFalsePage.iceCream()).click(); - $(CheckboxVisibleFalsePage.submit()).click(); - $(CheckboxVisibleNonePage.blue()).click(); - $(CheckboxVisibleNonePage.submit()).click(); - expect($(MutuallyExclusivePage.otherDetail()).isDisplayed()).to.be.true; + it("When a mutually exclusive checkbox has a detail_answer with visible set to true, Then the detail answer write-in field should be shown", async () => { + await $(CheckboxVisibleTruePage.coffee()).click(); + await click(CheckboxVisibleTruePage.submit()); + await $(CheckboxVisibleFalsePage.iceCream()).click(); + await click(CheckboxVisibleFalsePage.submit()); + await $(CheckboxVisibleNonePage.blue()).click(); + await click(CheckboxVisibleNonePage.submit()); + await expect(await $(MutuallyExclusivePage.otherDetail()).isDisplayed()).toBe(true); }); }); diff --git a/tests/functional/spec/components/checkbox/checkbox_detail_answer_dropdown.spec.js b/tests/functional/spec/components/checkbox/checkbox_detail_answer_dropdown.spec.js index e554d1b733..4b8b8dc462 100644 --- a/tests/functional/spec/components/checkbox/checkbox_detail_answer_dropdown.spec.js +++ b/tests/functional/spec/components/checkbox/checkbox_detail_answer_dropdown.spec.js @@ -1,91 +1,91 @@ import CheckboxDropdownPage from "../../../generated_pages/checkbox_detail_answer_dropdown/optional-checkbox-with-dropdown-detail-answer-block.page"; import SubmitPage from "../../../generated_pages/checkbox_detail_answer_dropdown/submit.page"; import DropdownMandatoryPage from "../../../generated_pages/dropdown_mandatory/dropdown-mandatory.page"; - +import { click } from "../../../helpers"; describe("Optional Checkbox with a Dropdown detail answer", () => { - beforeEach(() => { - browser.openQuestionnaire("test_checkbox_detail_answer_dropdown.json"); + beforeEach(async () => { + await browser.openQuestionnaire("test_checkbox_detail_answer_dropdown.json"); }); describe("Given an optional checkbox with a dropdown detail answer", () => { - it("When a placeholder is set for the detail answer, Then that value should be displayed as the first option", () => { - $(CheckboxDropdownPage.fruit()).click(); + it("When a placeholder is set for the detail answer, Then that value should be displayed as the first option", async () => { + await $(CheckboxDropdownPage.fruit()).click(); - expect($(CheckboxDropdownPage.fruitDetail()).getText()).to.contain("Select fruit"); + await expect(await $(CheckboxDropdownPage.fruitDetail()).getText()).toContain("Select fruit"); }); - it("When a placeholder is not set for the detail answer, Then the default placeholder should be displayed as the first option", () => { - $(CheckboxDropdownPage.jam()).click(); + it("When a placeholder is not set for the detail answer, Then the default placeholder should be displayed as the first option", async () => { + await $(CheckboxDropdownPage.jam()).click(); - expect($(CheckboxDropdownPage.jamDetail()).getText()).to.contain("Select an answer"); + await expect(await $(CheckboxDropdownPage.jamDetail()).getText()).toContain("Select an answer"); }); - it("When the user does not provide an answer and submits, Then the summary should display 'No answer provided'", () => { - $(CheckboxDropdownPage.submit()).click(); + it("When the user does not provide an answer and submits, Then the summary should display 'No answer provided'", async () => { + await click(CheckboxDropdownPage.submit()); - expect($(SubmitPage.optionalCheckboxWithDropdownDetailAnswer()).getText()).to.equal("No answer provided"); + await expect(await $(SubmitPage.optionalCheckboxWithDropdownDetailAnswer()).getText()).toBe("No answer provided"); }); - it("When the user selects an option with an optional detail answer but does not provide a detail answer, Then the summary should display the chosen option without the detail answer", () => { - $(CheckboxDropdownPage.fruit()).click(); - $(CheckboxDropdownPage.submit()).click(); + it("When the user selects an option with an optional detail answer but does not provide a detail answer, Then the summary should display the chosen option without the detail answer", async () => { + await $(CheckboxDropdownPage.fruit()).click(); + await click(CheckboxDropdownPage.submit()); - expect($(SubmitPage.optionalCheckboxWithDropdownDetailAnswer()).getText()).to.equal("Fruit"); + await expect(await $(SubmitPage.optionalCheckboxWithDropdownDetailAnswer()).getText()).toBe("Fruit"); }); - it("When the user selects an option with an optional detail answer and provides a detail answer, Then the summary should display the chosen option and the detail answer", () => { - $(CheckboxDropdownPage.fruit()).click(); - $(CheckboxDropdownPage.fruitDetail()).selectByAttribute("value", "Mango"); - $(CheckboxDropdownPage.submit()).click(); + it("When the user selects an option with an optional detail answer and provides a detail answer, Then the summary should display the chosen option and the detail answer", async () => { + await $(CheckboxDropdownPage.fruit()).click(); + await $(CheckboxDropdownPage.fruitDetail()).selectByAttribute("value", "Mango"); + await click(CheckboxDropdownPage.submit()); - expect($(SubmitPage.optionalCheckboxWithDropdownDetailAnswer()).getText()).to.equal("Fruit\nMango"); + await expect(await $(SubmitPage.optionalCheckboxWithDropdownDetailAnswer()).getText()).toBe("Fruit\nMango"); }); - it("When the user selects the default dropdown option after submitting a detail answer, Then the summary should not display the detail answer", () => { - $(CheckboxDropdownPage.fruit()).click(); - $(CheckboxDropdownPage.fruitDetail()).selectByAttribute("value", "Mango"); - $(CheckboxDropdownPage.submit()).click(); - $(SubmitPage.previous()).click(); - $(CheckboxDropdownPage.fruitDetail()).selectByVisibleText("Select fruit"); - $(CheckboxDropdownPage.submit()).click(); + it("When the user selects the default dropdown option after submitting a detail answer, Then the summary should not display the detail answer", async () => { + await $(CheckboxDropdownPage.fruit()).click(); + await $(CheckboxDropdownPage.fruitDetail()).selectByAttribute("value", "Mango"); + await click(CheckboxDropdownPage.submit()); + await $(SubmitPage.previous()).click(); + await $(CheckboxDropdownPage.fruitDetail()).selectByVisibleText("Select fruit"); + await click(CheckboxDropdownPage.submit()); - expect($(SubmitPage.optionalCheckboxWithDropdownDetailAnswer()).getText()).to.equal("Fruit"); + await expect(await $(SubmitPage.optionalCheckboxWithDropdownDetailAnswer()).getText()).toBe("Fruit"); }); - it("When the user selects an option with an mandatory detail answer but does not provide a detail answer, Then an error should be displayed when the user submits", () => { - $(CheckboxDropdownPage.jam()).click(); - $(CheckboxDropdownPage.submit()).click(); + it("When the user selects an option with an mandatory detail answer but does not provide a detail answer, Then an error should be displayed when the user submits", async () => { + await $(CheckboxDropdownPage.jam()).click(); + await click(CheckboxDropdownPage.submit()); - expect($(DropdownMandatoryPage.errorNumber(1)).getText()).to.equal("Please select the type of Jam"); + await expect(await $(DropdownMandatoryPage.errorNumber(1)).getText()).toBe("Please select the type of Jam"); }); - it("When the user selects an option with an mandatory detail answer and provides a detail answer, Then the summary should display the chosen option and its details", () => { - $(CheckboxDropdownPage.jam()).click(); - $(CheckboxDropdownPage.jamDetail()).selectByAttribute("value", "Strawberry"); - $(CheckboxDropdownPage.submit()).click(); + it("When the user selects an option with an mandatory detail answer and provides a detail answer, Then the summary should display the chosen option and its details", async () => { + await $(CheckboxDropdownPage.jam()).click(); + await $(CheckboxDropdownPage.jamDetail()).selectByAttribute("value", "Strawberry"); + await click(CheckboxDropdownPage.submit()); - expect($(SubmitPage.optionalCheckboxWithDropdownDetailAnswer()).getText()).to.equal("Jam\nStrawberry"); + await expect(await $(SubmitPage.optionalCheckboxWithDropdownDetailAnswer()).getText()).toBe("Jam\nStrawberry"); }); - it("When the user removes a previously submitted detail answer, Then the summary should not display the removed detail answer", () => { - $(CheckboxDropdownPage.fruit()).click(); - $(CheckboxDropdownPage.fruitDetail()).selectByAttribute("value", "Mango"); - $(CheckboxDropdownPage.submit()).click(); - $(SubmitPage.previous()).click(); - $(CheckboxDropdownPage.fruit()).click(); - $(CheckboxDropdownPage.submit()).click(); + it("When the user removes a previously submitted detail answer, Then the summary should not display the removed detail answer", async () => { + await $(CheckboxDropdownPage.fruit()).click(); + await $(CheckboxDropdownPage.fruitDetail()).selectByAttribute("value", "Mango"); + await click(CheckboxDropdownPage.submit()); + await $(SubmitPage.previous()).click(); + await $(CheckboxDropdownPage.fruit()).click(); + await click(CheckboxDropdownPage.submit()); - expect($(SubmitPage.optionalCheckboxWithDropdownDetailAnswer()).getText()).to.equal("No answer provided"); + await expect(await $(SubmitPage.optionalCheckboxWithDropdownDetailAnswer()).getText()).toBe("No answer provided"); }); - it("When the user selects multiple options with detail answers and submits, Then the summary should display all the chosen options and their detail answer", () => { - $(CheckboxDropdownPage.fruit()).click(); - $(CheckboxDropdownPage.fruitDetail()).selectByAttribute("value", "Mango"); - $(CheckboxDropdownPage.jam()).click(); - $(CheckboxDropdownPage.jamDetail()).selectByAttribute("value", "Strawberry"); - $(CheckboxDropdownPage.submit()).click(); + it("When the user selects multiple options with detail answers and submits, Then the summary should display all the chosen options and their detail answer", async () => { + await $(CheckboxDropdownPage.fruit()).click(); + await $(CheckboxDropdownPage.fruitDetail()).selectByAttribute("value", "Mango"); + await $(CheckboxDropdownPage.jam()).click(); + await $(CheckboxDropdownPage.jamDetail()).selectByAttribute("value", "Strawberry"); + await click(CheckboxDropdownPage.submit()); - expect($(SubmitPage.optionalCheckboxWithDropdownDetailAnswer()).getText()).to.equal("Fruit\nMango\nJam\nStrawberry"); + await expect(await $(SubmitPage.optionalCheckboxWithDropdownDetailAnswer()).getText()).toBe("Fruit\nMango\nJam\nStrawberry"); }); }); }); diff --git a/tests/functional/spec/components/checkbox/checkbox_detail_answer_multiple.spec.js b/tests/functional/spec/components/checkbox/checkbox_detail_answer_multiple.spec.js index aece2d7822..ac8cf20f34 100644 --- a/tests/functional/spec/components/checkbox/checkbox_detail_answer_multiple.spec.js +++ b/tests/functional/spec/components/checkbox/checkbox_detail_answer_multiple.spec.js @@ -1,94 +1,94 @@ import MandatoryCheckboxPage from "../../../generated_pages/checkbox_detail_answer_multiple/mandatory-checkbox.page"; import SubmitPage from "../../../generated_pages/checkbox_detail_answer_multiple/submit.page"; - +import { click, verifyUrlContains } from "../../../helpers"; describe('Checkbox with multiple "detail_answer" options', () => { const checkboxSchema = "test_checkbox_detail_answer_multiple.json"; - it("Given detail answer options are available, When the user clicks an option, Then the detail answer input should be visible.", () => { - browser.openQuestionnaire(checkboxSchema); - $(MandatoryCheckboxPage.yourChoice()).click(); - expect($(MandatoryCheckboxPage.yourChoiceDetail()).isDisplayed()).to.be.true; - $(MandatoryCheckboxPage.cheese()).click(); - expect($(MandatoryCheckboxPage.cheeseDetail()).isDisplayed()).to.be.true; + it("Given detail answer options are available, When the user clicks an option, Then the detail answer input should be visible.", async () => { + await browser.openQuestionnaire(checkboxSchema); + await $(MandatoryCheckboxPage.yourChoice()).click(); + await expect(await $(MandatoryCheckboxPage.yourChoiceDetail()).isDisplayed()).toBe(true); + await $(MandatoryCheckboxPage.cheese()).click(); + await expect(await $(MandatoryCheckboxPage.cheeseDetail()).isDisplayed()).toBe(true); }); - it("Given a mandatory detail answer, When I select the option but leave the input field empty and submit, Then an error should be displayed.", () => { + it("Given a mandatory detail answer, When I select the option but leave the input field empty and submit, Then an error should be displayed.", async () => { // Given - browser.openQuestionnaire(checkboxSchema); + await browser.openQuestionnaire(checkboxSchema); // When // Non-Mandatory detail answer given - $(MandatoryCheckboxPage.cheese()).click(); - $(MandatoryCheckboxPage.cheeseDetail()).setValue("Mozzarella"); + await $(MandatoryCheckboxPage.cheese()).click(); + await $(MandatoryCheckboxPage.cheeseDetail()).setValue("Mozzarella"); // Mandatory detail answer left blank - $(MandatoryCheckboxPage.yourChoice()).click(); - $(MandatoryCheckboxPage.submit()).click(); + await $(MandatoryCheckboxPage.yourChoice()).click(); + await click(MandatoryCheckboxPage.submit()); // Then - expect($(MandatoryCheckboxPage.error()).isDisplayed()).to.be.true; - expect($(MandatoryCheckboxPage.errorNumber(1)).getText()).to.contain("Enter your topping choice to continue"); + await expect(await $(MandatoryCheckboxPage.error()).isDisplayed()).toBe(true); + await expect(await $(MandatoryCheckboxPage.errorNumber(1)).getText()).toBe("Enter your topping choice to continue"); }); - it("Given a selected checkbox answer with an error for a mandatory detail answer, When I enter valid value and submit the page, Then the error is cleared and I navigate to next page.", () => { + it("Given a selected checkbox answer with an error for a mandatory detail answer, When I enter valid value and submit the page, Then the error is cleared and I navigate to next page.", async () => { // Given - browser.openQuestionnaire(checkboxSchema); - $(MandatoryCheckboxPage.yourChoice()).click(); - $(MandatoryCheckboxPage.submit()).click(); - expect($(MandatoryCheckboxPage.error()).isDisplayed()).to.be.true; + await browser.openQuestionnaire(checkboxSchema); + await $(MandatoryCheckboxPage.yourChoice()).click(); + await click(MandatoryCheckboxPage.submit()); + await expect(await $(MandatoryCheckboxPage.error()).isDisplayed()).toBe(true); // When - $(MandatoryCheckboxPage.yourChoiceDetail()).setValue("Bacon"); - $(MandatoryCheckboxPage.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); + await $(MandatoryCheckboxPage.yourChoiceDetail()).setValue("Bacon"); + await click(MandatoryCheckboxPage.submit()); + await verifyUrlContains(SubmitPage.pageName); }); - it("Given a non-mandatory detail answer, When the user does not provide any text, Then just the option value should be displayed on the summary screen", () => { + it("Given a non-mandatory detail answer, When the user does not provide any text, Then just the option value should be displayed on the summary screen", async () => { // Given - browser.openQuestionnaire(checkboxSchema); + await browser.openQuestionnaire(checkboxSchema); // When - $(MandatoryCheckboxPage.cheese()).click(); - expect($(MandatoryCheckboxPage.cheeseDetail()).isDisplayed()).to.be.true; - $(MandatoryCheckboxPage.submit()).click(); + await $(MandatoryCheckboxPage.cheese()).click(); + await expect(await $(MandatoryCheckboxPage.cheeseDetail()).isDisplayed()).toBe(true); + await click(MandatoryCheckboxPage.submit()); // Then - expect($(SubmitPage.mandatoryCheckboxAnswer()).getText()).to.equal("Cheese"); + await expect(await $(SubmitPage.mandatoryCheckboxAnswer()).getText()).toBe("Cheese"); }); - it("Given multiple detail answers, When the user provides text for all, Then that text should be displayed on the summary screen", () => { + it("Given multiple detail answers, When the user provides text for all, Then that text should be displayed on the summary screen", async () => { // Given - browser.openQuestionnaire(checkboxSchema); + await browser.openQuestionnaire(checkboxSchema); // When - $(MandatoryCheckboxPage.cheese()).click(); - $(MandatoryCheckboxPage.cheeseDetail()).setValue("Mozzarella"); - $(MandatoryCheckboxPage.yourChoice()).click(); - $(MandatoryCheckboxPage.yourChoiceDetail()).setValue("Bacon"); - $(MandatoryCheckboxPage.submit()).click(); + await $(MandatoryCheckboxPage.cheese()).click(); + await $(MandatoryCheckboxPage.cheeseDetail()).setValue("Mozzarella"); + await $(MandatoryCheckboxPage.yourChoice()).click(); + await $(MandatoryCheckboxPage.yourChoiceDetail()).setValue("Bacon"); + await click(MandatoryCheckboxPage.submit()); // Then - expect($(SubmitPage.mandatoryCheckboxAnswer()).getText()).to.equal("Cheese\nMozzarella\nYour choice\nBacon"); + await expect(await $(SubmitPage.mandatoryCheckboxAnswer()).getText()).toBe("Cheese\nMozzarella\nYour choice\nBacon"); }); - it("Given multiple detail answers, When the user provides text for just one, Then that text should be displayed on the summary screen", () => { + it("Given multiple detail answers, When the user provides text for just one, Then that text should be displayed on the summary screen", async () => { // Given - browser.openQuestionnaire(checkboxSchema); + await browser.openQuestionnaire(checkboxSchema); // When - $(MandatoryCheckboxPage.yourChoice()).click(); - $(MandatoryCheckboxPage.yourChoiceDetail()).setValue("Bacon"); - $(MandatoryCheckboxPage.submit()).click(); + await $(MandatoryCheckboxPage.yourChoice()).click(); + await $(MandatoryCheckboxPage.yourChoiceDetail()).setValue("Bacon"); + await click(MandatoryCheckboxPage.submit()); // Then - expect($(SubmitPage.mandatoryCheckboxAnswer()).getText()).to.equal("Your choice\nBacon"); + await expect(await $(SubmitPage.mandatoryCheckboxAnswer()).getText()).toBe("Your choice\nBacon"); }); - it("Given I have previously added text in a detail answer and saved, When I uncheck the detail answer option and select a different checkbox, Then the text entered in the detail answer field should be empty.", () => { + it("Given I have previously added text in a detail answer and saved, When I uncheck the detail answer option and select a different checkbox, Then the text entered in the detail answer field should be empty.", async () => { // Given - browser.openQuestionnaire(checkboxSchema); + await browser.openQuestionnaire(checkboxSchema); // When - $(MandatoryCheckboxPage.cheese()).click(); - $(MandatoryCheckboxPage.cheeseDetail()).setValue("Mozzarella"); - $(MandatoryCheckboxPage.submit()).click(); - $(SubmitPage.previous()).click(); - $(MandatoryCheckboxPage.cheese()).click(); - $(MandatoryCheckboxPage.ham()).click(); - $(MandatoryCheckboxPage.submit()).click(); - $(SubmitPage.previous()).click(); + await $(MandatoryCheckboxPage.cheese()).click(); + await $(MandatoryCheckboxPage.cheeseDetail()).setValue("Mozzarella"); + await click(MandatoryCheckboxPage.submit()); + await $(SubmitPage.previous()).click(); + await $(MandatoryCheckboxPage.cheese()).click(); + await $(MandatoryCheckboxPage.ham()).click(); + await click(MandatoryCheckboxPage.submit()); + await $(SubmitPage.previous()).click(); // Then - $(MandatoryCheckboxPage.cheese()).click(); - expect($(MandatoryCheckboxPage.cheeseDetail()).getValue()).to.equal(""); + await $(MandatoryCheckboxPage.cheese()).click(); + await expect(await $(MandatoryCheckboxPage.cheeseDetail()).getValue()).toBe(""); }); }); diff --git a/tests/functional/spec/components/checkbox/checkbox_detail_answer_numeric.spec.js b/tests/functional/spec/components/checkbox/checkbox_detail_answer_numeric.spec.js index be92f9a3a5..b1dd29fb35 100644 --- a/tests/functional/spec/components/checkbox/checkbox_detail_answer_numeric.spec.js +++ b/tests/functional/spec/components/checkbox/checkbox_detail_answer_numeric.spec.js @@ -1,75 +1,75 @@ import CheckboxNumericDetailPage from "../../../generated_pages/checkbox_detail_answer_numeric/checkbox-numeric-detail.page"; import SubmitPage from "../../../generated_pages/checkbox_detail_answer_numeric/submit.page"; - +import { click } from "../../../helpers"; describe('Checkbox with a numeric "detail_answer" option', () => { - beforeEach(() => { - browser.openQuestionnaire("test_checkbox_detail_answer_numeric.json"); - $(CheckboxNumericDetailPage.other()).click(); + beforeEach(async () => { + await browser.openQuestionnaire("test_checkbox_detail_answer_numeric.json"); + await $(CheckboxNumericDetailPage.other()).click(); }); - it("Given a numeric detail answer options are available, When the user clicks an option, Then the detail answer input should be visible.", () => { - expect($(CheckboxNumericDetailPage.otherDetail()).isDisplayed()).to.be.true; + it("Given a numeric detail answer options are available, When the user clicks an option, Then the detail answer input should be visible.", async () => { + await expect(await $(CheckboxNumericDetailPage.otherDetail()).isDisplayed()).toBe(true); }); - it("Given a numeric detail answer, When the user does not provide any text, Then just the option value should be displayed on the summary screen", () => { + it("Given a numeric detail answer, When the user does not provide any text, Then just the option value should be displayed on the summary screen", async () => { // When - expect($(CheckboxNumericDetailPage.otherDetail()).isDisplayed()).to.be.true; - $(CheckboxNumericDetailPage.submit()).click(); + await expect(await $(CheckboxNumericDetailPage.otherDetail()).isDisplayed()).toBe(true); + await click(CheckboxNumericDetailPage.submit()); // Then - expect($(SubmitPage.checkboxNumericDetailAnswer()).getText()).to.contain("Other"); + await expect(await $(SubmitPage.checkboxNumericDetailAnswer()).getText()).toBe("Other"); }); - it("Given a numeric detail answer, When the user provides text, Then that text should be displayed on the summary screen", () => { + it("Given a numeric detail answer, When the user provides text, Then that text should be displayed on the summary screen", async () => { // When - $(CheckboxNumericDetailPage.otherDetail()).setValue("15"); - $(CheckboxNumericDetailPage.submit()).click(); + await $(CheckboxNumericDetailPage.otherDetail()).setValue("15"); + await click(CheckboxNumericDetailPage.submit()); // Then - expect($(SubmitPage.checkboxNumericDetailAnswer()).getText()).to.contain("15"); + await expect(await $(SubmitPage.checkboxNumericDetailAnswer()).getText()).toContain("15"); }); - it("Given a numeric detail answer, When the user provides text, An error should be displayed", () => { + it("Given a numeric detail answer, When the user provides text, An error should be displayed", async () => { // When - $(CheckboxNumericDetailPage.otherDetail()).setValue("fhdjkshfjkds"); - $(CheckboxNumericDetailPage.submit()).click(); + await $(CheckboxNumericDetailPage.otherDetail()).setValue("fhdjkshfjkds"); + await click(CheckboxNumericDetailPage.submit()); // Then - expect($(CheckboxNumericDetailPage.error()).isDisplayed()).to.be.true; - expect($(CheckboxNumericDetailPage.errorNumber(1)).getText()).to.contain("Please enter an integer"); + await expect(await $(CheckboxNumericDetailPage.error()).isDisplayed()).toBe(true); + await expect(await $(CheckboxNumericDetailPage.errorNumber(1)).getText()).toBe("Please enter an integer"); }); - it("Given a numeric detail answer, When the user provides a number larger than 20, An error should be displayed", () => { + it("Given a numeric detail answer, When the user provides a number larger than 20, An error should be displayed", async () => { // When - $(CheckboxNumericDetailPage.otherDetail()).setValue("250"); - $(CheckboxNumericDetailPage.submit()).click(); + await $(CheckboxNumericDetailPage.otherDetail()).setValue("250"); + await click(CheckboxNumericDetailPage.submit()); // Then - expect($(CheckboxNumericDetailPage.error()).isDisplayed()).to.be.true; - expect($(CheckboxNumericDetailPage.errorNumber(1)).getText()).to.contain("Number is too large"); + await expect(await $(CheckboxNumericDetailPage.error()).isDisplayed()).toBe(true); + await expect(await $(CheckboxNumericDetailPage.errorNumber(1)).getText()).toBe("Number is too large"); }); - it("Given a numeric detail answer, When the user provides a number less than 0, An error should be displayed", () => { + it("Given a numeric detail answer, When the user provides a number less than 0, An error should be displayed", async () => { // When - $(CheckboxNumericDetailPage.otherDetail()).setValue("-1"); - $(CheckboxNumericDetailPage.submit()).click(); + await $(CheckboxNumericDetailPage.otherDetail()).setValue("-1"); + await click(CheckboxNumericDetailPage.submit()); // Then - expect($(CheckboxNumericDetailPage.error()).isDisplayed()).to.be.true; - expect($(CheckboxNumericDetailPage.errorNumber(1)).getText()).to.contain("Number cannot be less than zero"); + await expect(await $(CheckboxNumericDetailPage.error()).isDisplayed()).toBe(true); + await expect(await $(CheckboxNumericDetailPage.errorNumber(1)).getText()).toBe("Number cannot be less than zero"); }); - it("Given a numeric detail answer, When the user provides text, An error should be displayed and the text in the textbox should be kept", () => { + it("Given a numeric detail answer, When the user provides text, An error should be displayed and the text in the textbox should be kept", async () => { // When - $(CheckboxNumericDetailPage.otherDetail()).setValue("biscuits"); - $(CheckboxNumericDetailPage.submit()).click(); + await $(CheckboxNumericDetailPage.otherDetail()).setValue("biscuits"); + await click(CheckboxNumericDetailPage.submit()); // Then - expect($(CheckboxNumericDetailPage.error()).isDisplayed()).to.be.true; - expect($(CheckboxNumericDetailPage.errorNumber(1)).getText()).to.contain("Please enter an integer"); - browser.pause(1000); - expect($(CheckboxNumericDetailPage.otherDetail()).getValue()).to.equal("biscuits"); + await expect(await $(CheckboxNumericDetailPage.error()).isDisplayed()).toBe(true); + await expect(await $(CheckboxNumericDetailPage.errorNumber(1)).getText()).toBe("Please enter an integer"); + await browser.pause(1000); + await expect(await $(CheckboxNumericDetailPage.otherDetail()).getValue()).toBe("biscuits"); }); - it('Given a numeric detail answer, When the user enters "0" and submits, Then "0" should be displayed on the summary screen', () => { + it('Given a numeric detail answer, When the user enters "0" and submits, Then "0" should be displayed on the summary screen', async () => { // When - $(CheckboxNumericDetailPage.otherDetail()).setValue("0"); - $(CheckboxNumericDetailPage.submit()).click(); + await $(CheckboxNumericDetailPage.otherDetail()).setValue("0"); + await click(CheckboxNumericDetailPage.submit()); // Then - expect($(SubmitPage.checkboxNumericDetailAnswer()).getText()).to.contain("0"); + await expect(await $(SubmitPage.checkboxNumericDetailAnswer()).getText()).toContain("0"); }); }); diff --git a/tests/functional/spec/components/checkbox/checkbox_label.spec.js b/tests/functional/spec/components/checkbox/checkbox_label.spec.js index fb61b22323..71e74bf249 100644 --- a/tests/functional/spec/components/checkbox/checkbox_label.spec.js +++ b/tests/functional/spec/components/checkbox/checkbox_label.spec.js @@ -1,34 +1,34 @@ import DefaultInstructionPage from "../../../generated_pages/checkbox_instruction/default-instruction-checkbox.page"; import NoInstructionPage from "../../../generated_pages/checkbox_instruction/no-instruction-checkbox.page"; import CustomInstructionPage from "../../../generated_pages/checkbox_instruction/custom-instruction-checkbox.page"; - +import { click } from "../../../helpers"; describe("Given the checkbox label variants questionnaire,", () => { - beforeEach(() => { - browser.openQuestionnaire("test_checkbox_instruction.json"); + beforeEach(async () => { + await browser.openQuestionnaire("test_checkbox_instruction.json"); }); - it("Given an instruction has not been set in the schema for a checkbox answer, When the checkbox answer is displayed, Then the default instruction should be visible", () => { - expect($("body").getText()).to.have.string("Select all that apply"); + it("Given an instruction has not been set in the schema for a checkbox answer, When the checkbox answer is displayed, Then the default instruction should be visible", async () => { + await expect(await $("body").getText()).toContain("Select all that apply"); }); - it("Given an instruction has been set to null in the schema for a checkbox answer, When the checkbox answer is displayed, Then the instruction should not be visible", () => { - $(DefaultInstructionPage.red()).click(); - $(DefaultInstructionPage.submit()).click(); - expect($("body").getText()).to.not.have.string("Select all that apply"); + it("Given an instruction has been set to null in the schema for a checkbox answer, When the checkbox answer is displayed, Then the instruction should not be visible", async () => { + await $(DefaultInstructionPage.red()).click(); + await click(DefaultInstructionPage.submit()); + await expect(await $("body").getText()).not.toBe("Select all that apply"); }); - it("Given a custom instruction has been set in the schema for a checkbox answer, When the checkbox answer is displayed, Then the custom instruction should be visible", () => { - $(DefaultInstructionPage.red()).click(); - $(DefaultInstructionPage.submit()).click(); - $(NoInstructionPage.rugby()).click(); - $(NoInstructionPage.submit()).click(); - expect($("body").getText()).to.have.string("Select your answer"); + it("Given a custom instruction has been set in the schema for a checkbox answer, When the checkbox answer is displayed, Then the custom instruction should be visible", async () => { + await $(DefaultInstructionPage.red()).click(); + await click(DefaultInstructionPage.submit()); + await $(NoInstructionPage.rugby()).click(); + await click(NoInstructionPage.submit()); + await expect(await $("body").getText()).toContain("Select your answer"); }); - it("Given a label and custom instruction have been set in the schema for a checkbox answer, When the checkbox answer is displayed, Then both the custom instruction and label should be visible", () => { - $(DefaultInstructionPage.red()).click(); - $(DefaultInstructionPage.submit()).click(); - $(NoInstructionPage.rugby()).click(); - $(NoInstructionPage.submit()).click(); - $(CustomInstructionPage.monday()).click(); - $(CustomInstructionPage.submit()).click(); - expect($("body").getText()).to.have.string("Days of the Week"); - expect($("body").getText()).to.have.string("Select your answer"); + it("Given a label and custom instruction have been set in the schema for a checkbox answer, When the checkbox answer is displayed, Then both the custom instruction and label should be visible", async () => { + await $(DefaultInstructionPage.red()).click(); + await click(DefaultInstructionPage.submit()); + await $(NoInstructionPage.rugby()).click(); + await click(NoInstructionPage.submit()); + await $(CustomInstructionPage.monday()).click(); + await click(CustomInstructionPage.submit()); + await expect(await $("body").getText()).toContain("Days of the Week"); + await expect(await $("body").getText()).toContain("Select your answer"); }); }); diff --git a/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_checkbox.spec.js b/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_checkbox.spec.js index c9933448ff..c9644aecf4 100644 --- a/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_checkbox.spec.js +++ b/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_checkbox.spec.js @@ -1,130 +1,131 @@ import MandatoryCheckboxPage from "../../../../generated_pages/mutually_exclusive/mutually-exclusive-checkbox.page"; import SummaryPage from "../../../../generated_pages/mutually_exclusive/mutually-exclusive-checkbox-section-summary.page"; +import { click } from "../../../../helpers"; describe("Component: Mutually Exclusive Checkbox With Single Checkbox Override", () => { - beforeEach(() => { - browser.openQuestionnaire("test_mutually_exclusive.json"); + beforeEach(async () => { + await browser.openQuestionnaire("test_mutually_exclusive.json"); }); describe("Given the user has clicked multiple non-exclusive options", () => { - it("When then user clicks the mutually exclusive option, Then only the mutually exclusive option should be checked.", () => { + it("When then user clicks the mutually exclusive option, Then only the mutually exclusive option should be checked.", async () => { // Given - $(MandatoryCheckboxPage.checkboxBritish()).click(); - $(MandatoryCheckboxPage.checkboxIrish()).click(); - $(MandatoryCheckboxPage.checkboxOther()).click(); - $(MandatoryCheckboxPage.checkboxOtherDetail()).setValue("The other option"); + await $(MandatoryCheckboxPage.checkboxBritish()).click(); + await $(MandatoryCheckboxPage.checkboxIrish()).click(); + await $(MandatoryCheckboxPage.checkboxOther()).click(); + await $(MandatoryCheckboxPage.checkboxOtherDetail()).setValue("The other option"); - expect($(MandatoryCheckboxPage.checkboxBritish()).isSelected()).to.be.true; - expect($(MandatoryCheckboxPage.checkboxIrish()).isSelected()).to.be.true; - expect($(MandatoryCheckboxPage.checkboxOther()).isSelected()).to.be.true; - expect($(MandatoryCheckboxPage.checkboxOtherDetail()).getValue()).to.contain("The other option"); + await expect(await $(MandatoryCheckboxPage.checkboxBritish()).isSelected()).toBe(true); + await expect(await $(MandatoryCheckboxPage.checkboxIrish()).isSelected()).toBe(true); + await expect(await $(MandatoryCheckboxPage.checkboxOther()).isSelected()).toBe(true); + await expect(await $(MandatoryCheckboxPage.checkboxOtherDetail()).getValue()).toBe("The other option"); // When - $(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).click(); - expect($(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).isSelected()).to.be.true; + await $(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).click(); + await expect(await $(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).isSelected()).toBe(true); // Then - expect($(MandatoryCheckboxPage.checkboxBritish()).isSelected()).to.be.false; - expect($(MandatoryCheckboxPage.checkboxIrish()).isSelected()).to.be.false; - expect($(MandatoryCheckboxPage.checkboxOther()).isSelected()).to.be.false; - expect($(MandatoryCheckboxPage.checkboxOtherDetail()).getValue()).to.contain(""); + await expect(await $(MandatoryCheckboxPage.checkboxBritish()).isSelected()).toBe(false); + await expect(await $(MandatoryCheckboxPage.checkboxIrish()).isSelected()).toBe(false); + await expect(await $(MandatoryCheckboxPage.checkboxOther()).isSelected()).toBe(false); + await expect(await $(MandatoryCheckboxPage.checkboxOtherDetail()).getValue()).toBe(""); - $(MandatoryCheckboxPage.submit()).click(); + await click(MandatoryCheckboxPage.submit()); - expect($(SummaryPage.checkboxExclusiveAnswer()).getText()).to.have.string("I prefer not to say"); - expect($(SummaryPage.checkboxExclusiveAnswer()).getText()).to.not.have.string("British\nIrish"); + await expect(await $(SummaryPage.checkboxExclusiveAnswer()).getText()).toBe("I prefer not to say"); + await expect(await $(SummaryPage.checkboxExclusiveAnswer()).getText()).not.toBe("British\nIrish"); }); }); describe('Given the user has clicked the mutually exclusive "other" option', () => { - it("When the user returns to the question, Then the mutually exclusive other option should remain checked.", () => { + it("When the user returns to the question, Then the mutually exclusive other option should remain checked.", async () => { // Given - $(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).click(); - $(MandatoryCheckboxPage.submit()).click(); + await $(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).click(); + await click(MandatoryCheckboxPage.submit()); // When - $(SummaryPage.previous()).click(); + await $(SummaryPage.previous()).click(); // Then - expect($(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).isSelected()).to.be.true; + await expect(await $(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).isSelected()).toBe(true); }); }); describe("Given the user has clicked the mutually exclusive option", () => { - it("When the user clicks the non-exclusive options, Then only the non-exclusive options should be checked.", () => { + it("When the user clicks the non-exclusive options, Then only the non-exclusive options should be checked.", async () => { // Given - $(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).click(); - expect($(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).isSelected()).to.be.true; + await $(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).click(); + await expect(await $(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).isSelected()).toBe(true); // When - $(MandatoryCheckboxPage.checkboxBritish()).click(); - $(MandatoryCheckboxPage.checkboxIrish()).click(); + await $(MandatoryCheckboxPage.checkboxBritish()).click(); + await $(MandatoryCheckboxPage.checkboxIrish()).click(); // Then - expect($(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).isSelected()).to.be.false; - expect($(MandatoryCheckboxPage.checkboxBritish()).isSelected()).to.be.true; - expect($(MandatoryCheckboxPage.checkboxIrish()).isSelected()).to.be.true; + await expect(await $(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).isSelected()).toBe(false); + await expect(await $(MandatoryCheckboxPage.checkboxBritish()).isSelected()).toBe(true); + await expect(await $(MandatoryCheckboxPage.checkboxIrish()).isSelected()).toBe(true); - $(MandatoryCheckboxPage.submit()).click(); + await click(MandatoryCheckboxPage.submit()); - expect($(SummaryPage.checkboxAnswer()).getText()).to.have.string("British\nIrish"); - expect($(SummaryPage.checkboxAnswer()).getText()).to.not.have.string("I prefer not to say"); + await expect(await $(SummaryPage.checkboxAnswer()).getText()).toBe("British\nIrish"); + await expect(await $(SummaryPage.checkboxAnswer()).getText()).not.toBe("I prefer not to say"); }); }); describe("Given the user has not clicked the mutually exclusive option", () => { - it("When the user clicks multiple non-exclusive options, Then only the non-exclusive options should be checked.", () => { + it("When the user clicks multiple non-exclusive options, Then only the non-exclusive options should be checked.", async () => { // Given - expect($(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).isSelected()).toBe(false); // When - $(MandatoryCheckboxPage.checkboxBritish()).click(); - $(MandatoryCheckboxPage.checkboxIrish()).click(); + await $(MandatoryCheckboxPage.checkboxBritish()).click(); + await $(MandatoryCheckboxPage.checkboxIrish()).click(); // Then - expect($(MandatoryCheckboxPage.checkboxBritish()).isSelected()).to.be.true; - expect($(MandatoryCheckboxPage.checkboxIrish()).isSelected()).to.be.true; + await expect(await $(MandatoryCheckboxPage.checkboxBritish()).isSelected()).toBe(true); + await expect(await $(MandatoryCheckboxPage.checkboxIrish()).isSelected()).toBe(true); - $(MandatoryCheckboxPage.submit()).click(); + await click(MandatoryCheckboxPage.submit()); - expect($(SummaryPage.checkboxAnswer()).getText()).to.have.string("British\nIrish"); - expect($(SummaryPage.checkboxAnswer()).getText()).to.not.have.string("I prefer not to say"); + await expect(await $(SummaryPage.checkboxAnswer()).getText()).toBe("British\nIrish"); + await expect(await $(SummaryPage.checkboxAnswer()).getText()).not.toBe("I prefer not to say"); }); }); describe("Given the user has not clicked any of the non-exclusive options", () => { - it("When the user clicks the mutually exclusive option, Then only the exclusive option should be checked.", () => { + it("When the user clicks the mutually exclusive option, Then only the exclusive option should be checked.", async () => { // Given - expect($(MandatoryCheckboxPage.checkboxBritish()).isSelected()).to.be.false; - expect($(MandatoryCheckboxPage.checkboxIrish()).isSelected()).to.be.false; - expect($(MandatoryCheckboxPage.checkboxOther()).isSelected()).to.be.false; + await expect(await $(MandatoryCheckboxPage.checkboxBritish()).isSelected()).toBe(false); + await expect(await $(MandatoryCheckboxPage.checkboxIrish()).isSelected()).toBe(false); + await expect(await $(MandatoryCheckboxPage.checkboxOther()).isSelected()).toBe(false); // When - $(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).click(); - expect($(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).isSelected()).to.be.true; - $(MandatoryCheckboxPage.submit()).click(); + await $(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).click(); + await expect(await $(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).isSelected()).toBe(true); + await click(MandatoryCheckboxPage.submit()); // Then - expect($(SummaryPage.checkboxExclusiveAnswer()).getText()).to.have.string("I prefer not to say"); - expect($(SummaryPage.checkboxExclusiveAnswer()).getText()).to.not.have.string("British\nIrish"); + await expect(await $(SummaryPage.checkboxExclusiveAnswer()).getText()).toBe("I prefer not to say"); + await expect(await $(SummaryPage.checkboxExclusiveAnswer()).getText()).not.toBe("British\nIrish"); }); }); describe("Given the user has not clicked any options and the question is mandatory", () => { - it("When the user clicks the Continue button, Then a validation error message should be displayed.", () => { + it("When the user clicks the Continue button, Then a validation error message should be displayed.", async () => { // Given - expect($(MandatoryCheckboxPage.checkboxBritish()).isSelected()).to.be.false; - expect($(MandatoryCheckboxPage.checkboxIrish()).isSelected()).to.be.false; - expect($(MandatoryCheckboxPage.checkboxOther()).isSelected()).to.be.false; - expect($(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(MandatoryCheckboxPage.checkboxBritish()).isSelected()).toBe(false); + await expect(await $(MandatoryCheckboxPage.checkboxIrish()).isSelected()).toBe(false); + await expect(await $(MandatoryCheckboxPage.checkboxOther()).isSelected()).toBe(false); + await expect(await $(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).isSelected()).toBe(false); // When - $(MandatoryCheckboxPage.submit()).click(); + await click(MandatoryCheckboxPage.submit()); // Then - expect($(MandatoryCheckboxPage.errorHeader()).getText()).to.contain("There is a problem with your answer"); - expect($(MandatoryCheckboxPage.errorNumber(1)).getText()).to.contain("Select at least one answer"); - expect($(MandatoryCheckboxPage.questionErrorPanel()).isExisting()).to.be.true; + await expect(await $(MandatoryCheckboxPage.errorHeader()).getText()).toBe("There is a problem with your answer"); + await expect(await $(MandatoryCheckboxPage.errorNumber(1)).getText()).toContain("Select at least one answer"); + await expect(await $(MandatoryCheckboxPage.questionErrorPanel()).isExisting()).toBe(true); }); }); }); diff --git a/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_currency.spec.js b/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_currency.spec.js index 4004ce36fe..f570d6c5f3 100644 --- a/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_currency.spec.js +++ b/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_currency.spec.js @@ -1,99 +1,101 @@ import CurrencyPage from "../../../../generated_pages/mutually_exclusive/mutually-exclusive-currency.page"; import SummaryPage from "../../../../generated_pages/mutually_exclusive/mutually-exclusive-currency-section-summary.page"; +import { click } from "../../../../helpers"; describe("Component: Mutually Exclusive Currency With Single Checkbox Override", () => { - beforeEach(() => { - browser.openQuestionnaire("test_mutually_exclusive.json"); - browser.url("/questionnaire/mutually-exclusive-currency"); + beforeEach(async () => { + await browser.openQuestionnaire("test_mutually_exclusive.json"); + await browser.pause(100); + await browser.url("/questionnaire/mutually-exclusive-currency"); }); describe("Given the user has entered a value for the non-exclusive currency answer", () => { - it("When then user clicks the mutually exclusive checkbox answer, Then only the mutually exclusive checkbox should be answered.", () => { + it("When then user clicks the mutually exclusive checkbox answer, Then only the mutually exclusive checkbox should be answered.", async () => { // Given - $(CurrencyPage.currency()).setValue("123"); - expect($(CurrencyPage.currency()).getValue()).to.contain("123"); + await $(CurrencyPage.currency()).setValue("123"); + await expect(await $(CurrencyPage.currency()).getValue()).toBe("123"); // When - $(CurrencyPage.currencyExclusiveIPreferNotToSay()).click(); + await $(CurrencyPage.currencyExclusiveIPreferNotToSay()).click(); // Then - expect($(CurrencyPage.currencyExclusiveIPreferNotToSay()).isSelected()).to.be.true; - expect($(CurrencyPage.currency()).getValue()).to.contain(""); + await expect(await $(CurrencyPage.currencyExclusiveIPreferNotToSay()).isSelected()).toBe(true); + await expect(await $(CurrencyPage.currency()).getValue()).toBe(""); - $(CurrencyPage.submit()).click(); + await click(CurrencyPage.submit()); - expect($(SummaryPage.currencyExclusiveAnswer()).getText()).to.have.string("I prefer not to say"); - expect($(SummaryPage.currencyExclusiveAnswer()).getText()).to.not.have.string("123"); + await expect(await $(SummaryPage.currencyExclusiveAnswer()).getText()).toBe("I prefer not to say"); + await expect(await $(SummaryPage.currencyExclusiveAnswer()).getText()).not.toBe("123"); }); }); describe("Given the user has clicked the mutually exclusive checkbox answer", () => { - it("When the user enters a value for the non-exclusive currency answer and removes focus, Then only the non-exclusive currency answer should be answered.", () => { + it("When the user enters a value for the non-exclusive currency answer and removes focus, Then only the non-exclusive currency answer should be answered.", async () => { // Given - $(CurrencyPage.currencyExclusiveIPreferNotToSay()).click(); - expect($(CurrencyPage.currencyExclusiveIPreferNotToSay()).isSelected()).to.be.true; + await $(CurrencyPage.currencyExclusiveIPreferNotToSay()).click(); + await expect(await $(CurrencyPage.currencyExclusiveIPreferNotToSay()).isSelected()).toBe(true); // When - $(CurrencyPage.currency()).setValue("123"); + await $(CurrencyPage.currency()).setValue("123"); // Then - $(CurrencyPage.currency()).getValue(); - expect($(CurrencyPage.currencyExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await $(CurrencyPage.currency()).getValue(); + await expect(await $(CurrencyPage.currencyExclusiveIPreferNotToSay()).isSelected()).toBe(false); - $(CurrencyPage.submit()).click(); + await click(CurrencyPage.submit()); - expect($(SummaryPage.currencyAnswer()).getText()).to.have.string("123"); - expect($(SummaryPage.currencyAnswer()).getText()).to.not.have.string("I prefer not to say"); + await expect(await $(SummaryPage.currencyAnswer()).getText()).toBe("ÂŖ123"); + await expect(await $(SummaryPage.currencyAnswer()).getText()).not.toBe("I prefer not to say"); }); }); describe("Given the user has not clicked the mutually exclusive checkbox answer", () => { - it("When the user enters a value for the non-exclusive currency answer, Then only the non-exclusive currency answer should be answered.", () => { + it("When the user enters a value for the non-exclusive currency answer, Then only the non-exclusive currency answer should be answered.", async () => { // Given - expect($(CurrencyPage.currencyExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(CurrencyPage.currencyExclusiveIPreferNotToSay()).isSelected()).toBe(false); // When - $(CurrencyPage.currency()).setValue("123"); + await $(CurrencyPage.currency()).setValue("123"); // Then - expect($(CurrencyPage.currency()).getValue()).to.contain("123"); - expect($(CurrencyPage.currencyExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(CurrencyPage.currency()).getValue()).toBe("123"); + await expect(await $(CurrencyPage.currencyExclusiveIPreferNotToSay()).isSelected()).toBe(false); - $(CurrencyPage.submit()).click(); + await click(CurrencyPage.submit()); - expect($(SummaryPage.currencyAnswer()).getText()).to.have.string("123"); - expect($(SummaryPage.currencyAnswer()).getText()).to.not.have.string("I prefer not to say"); + await expect(await $(SummaryPage.currencyAnswer()).getText()).toBe("ÂŖ123"); + await expect(await $(SummaryPage.currencyAnswer()).getText()).not.toBe("I prefer not to say"); }); }); describe("Given the user has not answered the non-exclusive currency answer", () => { - it("When the user clicks the mutually exclusive checkbox answer, Then only the exclusive checkbox should be answered.", () => { + it("When the user clicks the mutually exclusive checkbox answer, Then only the exclusive checkbox should be answered.", async () => { // Given - expect($(CurrencyPage.currency()).getValue()).to.contain(""); + await expect(await $(CurrencyPage.currency()).getValue()).toBe(""); // When - $(CurrencyPage.currencyExclusiveIPreferNotToSay()).click(); - expect($(CurrencyPage.currencyExclusiveIPreferNotToSay()).isSelected()).to.be.true; + await $(CurrencyPage.currencyExclusiveIPreferNotToSay()).click(); + await expect(await $(CurrencyPage.currencyExclusiveIPreferNotToSay()).isSelected()).toBe(true); // Then - $(CurrencyPage.submit()).click(); + await click(CurrencyPage.submit()); - expect($(SummaryPage.currencyExclusiveAnswer()).getText()).to.have.string("I prefer not to say"); - expect($(SummaryPage.currencyExclusiveAnswer()).getText()).to.not.have.string("123"); + await expect(await $(SummaryPage.currencyExclusiveAnswer()).getText()).toBe("I prefer not to say"); + await expect(await $(SummaryPage.currencyExclusiveAnswer()).getText()).not.toBe("123"); }); }); describe("Given the user has not answered the question and the question is optional", () => { - it("When the user clicks the Continue button, Then it should display `No answer provided`", () => { + it("When the user clicks the Continue button, Then it should display `No answer provided`", async () => { // Given - expect($(CurrencyPage.currency()).getValue()).to.contain(""); - expect($(CurrencyPage.currencyExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(CurrencyPage.currency()).getValue()).toBe(""); + await expect(await $(CurrencyPage.currencyExclusiveIPreferNotToSay()).isSelected()).toBe(false); // When - $(CurrencyPage.submit()).click(); + await click(CurrencyPage.submit()); // Then - expect($(SummaryPage.currencyAnswer()).getText()).to.contain("No answer provided"); + await expect(await $(SummaryPage.currencyAnswer()).getText()).toBe("No answer provided"); }); }); }); diff --git a/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_day_month_year_date.spec.js b/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_day_month_year_date.spec.js index 2866bab6bf..5a8869797f 100644 --- a/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_day_month_year_date.spec.js +++ b/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_day_month_year_date.spec.js @@ -1,117 +1,119 @@ import DatePage from "../../../../generated_pages/mutually_exclusive/mutually-exclusive-date.page"; import SummaryPage from "../../../../generated_pages/mutually_exclusive/mutually-exclusive-date-section-summary.page"; +import { click } from "../../../../helpers"; describe("Component: Mutually Exclusive Day Month Year Date With Single Checkbox Override", () => { - beforeEach(() => { - browser.openQuestionnaire("test_mutually_exclusive.json"); - browser.url("/questionnaire/mutually-exclusive-date"); + beforeEach(async () => { + await browser.openQuestionnaire("test_mutually_exclusive.json"); + await browser.pause(100); + await browser.url("/questionnaire/mutually-exclusive-date"); }); describe("Given the user has entered a value for the non-exclusive month year date answer", () => { - it("When then user clicks the mutually exclusive checkbox answer, Then only the mutually exclusive checkbox should be answered.", () => { + it("When then user clicks the mutually exclusive checkbox answer, Then only the mutually exclusive checkbox should be answered.", async () => { // Given - $(DatePage.dateday()).setValue("17"); - $(DatePage.datemonth()).setValue("3"); - $(DatePage.dateyear()).setValue("2018"); - expect($(DatePage.dateday()).getValue()).to.contain("17"); - expect($(DatePage.datemonth()).getValue()).to.contain("3"); - expect($(DatePage.dateyear()).getValue()).to.contain("2018"); + await $(DatePage.dateday()).setValue("17"); + await $(DatePage.datemonth()).setValue("3"); + await $(DatePage.dateyear()).setValue("2018"); + await expect(await $(DatePage.dateday()).getValue()).toBe("17"); + await expect(await $(DatePage.datemonth()).getValue()).toBe("3"); + await expect(await $(DatePage.dateyear()).getValue()).toBe("2018"); // When - $(DatePage.dateExclusiveIPreferNotToSay()).click(); + await $(DatePage.dateExclusiveIPreferNotToSay()).click(); // Then - expect($(DatePage.dateExclusiveIPreferNotToSay()).isSelected()).to.be.true; - expect($(DatePage.dateday()).getValue()).to.contain(""); - expect($(DatePage.datemonth()).getValue()).to.contain(""); - expect($(DatePage.dateyear()).getValue()).to.contain(""); + await expect(await $(DatePage.dateExclusiveIPreferNotToSay()).isSelected()).toBe(true); + await expect(await $(DatePage.dateday()).getValue()).toBe(""); + await expect(await $(DatePage.datemonth()).getValue()).toBe(""); + await expect(await $(DatePage.dateyear()).getValue()).toBe(""); - $(DatePage.submit()).click(); + await click(DatePage.submit()); - expect($(SummaryPage.dateExclusiveAnswer()).getText()).to.have.string("I prefer not to say"); - expect($(SummaryPage.dateExclusiveAnswer()).getText()).to.not.have.string("17 March 2018"); + await expect(await $(SummaryPage.dateExclusiveAnswer()).getText()).toBe("I prefer not to say"); + await expect(await $(SummaryPage.dateExclusiveAnswer()).getText()).not.toBe("17 March 2018"); }); }); describe("Given the user has clicked the mutually exclusive checkbox answer", () => { - it("When the user enters a value for the non-exclusive month year date answer and removes focus, Then only the non-exclusive month year date answer should be answered.", () => { + it("When the user enters a value for the non-exclusive month year date answer and removes focus, Then only the non-exclusive month year date answer should be answered.", async () => { // Given - $(DatePage.dateExclusiveIPreferNotToSay()).click(); - expect($(DatePage.dateExclusiveIPreferNotToSay()).isSelected()).to.be.true; + await $(DatePage.dateExclusiveIPreferNotToSay()).click(); + await expect(await $(DatePage.dateExclusiveIPreferNotToSay()).isSelected()).toBe(true); // When - $(DatePage.dateday()).setValue("17"); - $(DatePage.datemonth()).setValue("3"); - $(DatePage.dateyear()).setValue("2018"); + await $(DatePage.dateday()).setValue("17"); + await $(DatePage.datemonth()).setValue("3"); + await $(DatePage.dateyear()).setValue("2018"); // Then - expect($(DatePage.dateday()).getValue()).to.contain("17"); - expect($(DatePage.datemonth()).getValue()).to.contain("3"); - expect($(DatePage.dateyear()).getValue()).to.contain("2018"); + await expect(await $(DatePage.dateday()).getValue()).toBe("17"); + await expect(await $(DatePage.datemonth()).getValue()).toBe("3"); + await expect(await $(DatePage.dateyear()).getValue()).toBe("2018"); - expect($(DatePage.dateExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(DatePage.dateExclusiveIPreferNotToSay()).isSelected()).toBe(false); - $(DatePage.submit()).click(); + await click(DatePage.submit()); - expect($(SummaryPage.dateAnswer()).getText()).to.have.string("17 March 2018"); - expect($(SummaryPage.dateAnswer()).getText()).to.not.have.string("I prefer not to say"); + await expect(await $(SummaryPage.dateAnswer()).getText()).toBe("17 March 2018"); + await expect(await $(SummaryPage.dateAnswer()).getText()).not.toBe("I prefer not to say"); }); }); describe("Given the user has not clicked the mutually exclusive checkbox answer", () => { - it("When the user enters a value for the non-exclusive month year date answer, Then only the non-exclusive month year date answer should be answered.", () => { + it("When the user enters a value for the non-exclusive month year date answer, Then only the non-exclusive month year date answer should be answered.", async () => { // Given - expect($(DatePage.dateExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(DatePage.dateExclusiveIPreferNotToSay()).isSelected()).toBe(false); // When - $(DatePage.dateday()).setValue("17"); - $(DatePage.datemonth()).setValue("3"); - $(DatePage.dateyear()).setValue("2018"); + await $(DatePage.dateday()).setValue("17"); + await $(DatePage.datemonth()).setValue("3"); + await $(DatePage.dateyear()).setValue("2018"); // Then - expect($(DatePage.dateday()).getValue()).to.contain("17"); - expect($(DatePage.datemonth()).getValue()).to.contain("3"); - expect($(DatePage.dateyear()).getValue()).to.contain("2018"); - expect($(DatePage.dateExclusiveIPreferNotToSay()).isSelected()).to.be.false; - - $(DatePage.submit()).click(); - expect($(SummaryPage.dateAnswer()).getText()).to.have.string("17 March 2018"); - expect($(SummaryPage.dateAnswer()).getText()).to.not.have.string("I prefer not to say"); + await expect(await $(DatePage.dateday()).getValue()).toBe("17"); + await expect(await $(DatePage.datemonth()).getValue()).toBe("3"); + await expect(await $(DatePage.dateyear()).getValue()).toBe("2018"); + await expect(await $(DatePage.dateExclusiveIPreferNotToSay()).isSelected()).toBe(false); + + await click(DatePage.submit()); + await expect(await $(SummaryPage.dateAnswer()).getText()).toBe("17 March 2018"); + await expect(await $(SummaryPage.dateAnswer()).getText()).not.toBe("I prefer not to say"); }); }); describe("Given the user has not answered the non-exclusive month year date answer", () => { - it("When the user clicks the mutually exclusive checkbox answer, Then only the exclusive checkbox should be answered.", () => { + it("When the user clicks the mutually exclusive checkbox answer, Then only the exclusive checkbox should be answered.", async () => { // Given - expect($(DatePage.dateday()).getValue()).to.contain(""); - expect($(DatePage.datemonth()).getValue()).to.contain(""); - expect($(DatePage.dateyear()).getValue()).to.contain(""); + await expect(await $(DatePage.dateday()).getValue()).toBe(""); + await expect(await $(DatePage.datemonth()).getValue()).toBe(""); + await expect(await $(DatePage.dateyear()).getValue()).toBe(""); // When - $(DatePage.dateExclusiveIPreferNotToSay()).click(); - expect($(DatePage.dateExclusiveIPreferNotToSay()).isSelected()).to.be.true; + await $(DatePage.dateExclusiveIPreferNotToSay()).click(); + await expect(await $(DatePage.dateExclusiveIPreferNotToSay()).isSelected()).toBe(true); // Then - $(DatePage.submit()).click(); + await click(DatePage.submit()); - expect($(SummaryPage.dateExclusiveAnswer()).getText()).to.have.string("I prefer not to say"); - expect($(SummaryPage.dateExclusiveAnswer()).getText()).to.not.have.string("17 March 2018"); + await expect(await $(SummaryPage.dateExclusiveAnswer()).getText()).toBe("I prefer not to say"); + await expect(await $(SummaryPage.dateExclusiveAnswer()).getText()).not.toBe("17 March 2018"); }); }); describe("Given the user has not answered the question and the question is optional", () => { - it("When the user clicks the Continue button, Then it should display `No answer provided`", () => { + it("When the user clicks the Continue button, Then it should display `No answer provided`", async () => { // Given - expect($(DatePage.dateday()).getValue()).to.contain(""); - expect($(DatePage.datemonth()).getValue()).to.contain(""); - expect($(DatePage.dateyear()).getValue()).to.contain(""); - expect($(DatePage.dateExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(DatePage.dateday()).getValue()).toBe(""); + await expect(await $(DatePage.datemonth()).getValue()).toBe(""); + await expect(await $(DatePage.dateyear()).getValue()).toBe(""); + await expect(await $(DatePage.dateExclusiveIPreferNotToSay()).isSelected()).toBe(false); // When - $(DatePage.submit()).click(); + await click(DatePage.submit()); // Then - expect($(SummaryPage.dateAnswer()).getText()).to.contain("No answer provided"); + await expect(await $(SummaryPage.dateAnswer()).getText()).toBe("No answer provided"); }); }); }); diff --git a/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_duration.spec.js b/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_duration.spec.js index b84727e006..acb887c4cc 100644 --- a/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_duration.spec.js +++ b/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_duration.spec.js @@ -1,109 +1,113 @@ import DurationPage from "../../../../generated_pages/mutually_exclusive/mutually-exclusive-duration.page"; import SummaryPage from "../../../../generated_pages/mutually_exclusive/mutually-exclusive-duration-section-summary.page"; +import { click } from "../../../../helpers"; describe("Component: Mutually Exclusive Duration With Single Checkbox Override", () => { - beforeEach(() => { - browser.openQuestionnaire("test_mutually_exclusive.json"); - browser.url("/questionnaire/mutually-exclusive-duration"); + beforeEach(async () => { + await browser.openQuestionnaire("test_mutually_exclusive.json"); + await browser.pause(100); + await browser.url("/questionnaire/mutually-exclusive-duration"); }); describe("Given the user has entered a value for the non-exclusive duration answer", () => { - it("When then user clicks the mutually exclusive checkbox answer, Then only the mutually exclusive checkbox should be answered.", () => { + it("When then user clicks the mutually exclusive checkbox answer, Then only the mutually exclusive checkbox should be answered.", async () => { // Given - $(DurationPage.durationYears()).setValue("1"); - $(DurationPage.durationMonths()).setValue("7"); + await $(DurationPage.durationYears()).setValue("1"); + await $(DurationPage.durationMonths()).setValue("7"); - expect($(DurationPage.durationYears()).getValue()).to.contain("1"); - expect($(DurationPage.durationMonths()).getValue()).to.contain("7"); + await expect(await $(DurationPage.durationYears()).getValue()).toBe("1"); + await expect(await $(DurationPage.durationMonths()).getValue()).toBe("7"); // When - $(DurationPage.durationExclusiveIPreferNotToSay()).click(); + await $(DurationPage.durationExclusiveIPreferNotToSay()).click(); // Then - expect($(DurationPage.durationExclusiveIPreferNotToSay()).isSelected()).to.be.true; - expect($(DurationPage.durationYears()).getValue()).to.contain(""); - expect($(DurationPage.durationMonths()).getValue()).to.contain(""); + await expect(await $(DurationPage.durationExclusiveIPreferNotToSay()).isSelected()).toBe(true); + await expect(await $(DurationPage.durationYears()).getValue()).toBe(""); + await expect(await $(DurationPage.durationMonths()).getValue()).toBe(""); - $(DurationPage.submit()).click(); + await click(DurationPage.submit()); - expect($(SummaryPage.durationExclusiveAnswer()).getText()).to.have.string("I prefer not to say"); - expect($(SummaryPage.durationExclusiveAnswer()).getText()).to.not.have.string("1 year 7 months"); + await expect(await $(SummaryPage.durationExclusiveAnswer()).getText()).toBe("I prefer not to say"); + await expect(await $(SummaryPage.durationExclusiveAnswer()).getText()).not.toBe("1 year 7 months"); }); }); describe("Given the user has clicked the mutually exclusive checkbox answer", () => { - it("When the user enters a value for the non-exclusive duration answer and removes focus, Then only the non-exclusive duration answer should be answered.", () => { + it("When the user enters a value for the non-exclusive duration answer and removes focus, Then only the non-exclusive duration answer should be answered.", async () => { // Given - $(DurationPage.durationExclusiveIPreferNotToSay()).click(); - expect($(DurationPage.durationExclusiveIPreferNotToSay()).isSelected()).to.be.true; + await $(DurationPage.durationExclusiveIPreferNotToSay()).click(); + await expect(await $(DurationPage.durationExclusiveIPreferNotToSay()).isSelected()).toBe(true); // When - $(DurationPage.durationYears()).setValue("1"); - $(DurationPage.durationMonths()).setValue("7"); + await $(DurationPage.durationYears()).setValue("1"); + await $(DurationPage.durationMonths()).setValue("7"); // Then - expect($(DurationPage.durationYears()).getValue()).to.contain("1"); - expect($(DurationPage.durationMonths()).getValue()).to.contain("7"); - expect($(DurationPage.durationExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(DurationPage.durationYears()).getValue()).toBe("1"); + await expect(await $(DurationPage.durationMonths()).getValue()).toBe("7"); + await expect(await $(DurationPage.durationExclusiveIPreferNotToSay()).isSelected()).toBe(false); - $(DurationPage.submit()).click(); + await click(DurationPage.submit()); - expect($(SummaryPage.durationAnswer()).getText()).to.have.string("1 year 7 months"); - expect($(SummaryPage.durationAnswer()).getText()).to.not.have.string("I prefer not to say"); + await expect(await $(SummaryPage.durationAnswer()).getText()).toBe("1 year 7 months"); + await expect(await $(SummaryPage.durationAnswer()).getText()).not.toBe("I prefer not to say"); }); }); describe("Given the user has not clicked the mutually exclusive checkbox answer", () => { - it("When the user enters a value for the non-exclusive duration answer, Then only the non-exclusive duration answer should be answered.", () => { + it("When the user enters a value for the non-exclusive duration answer, Then only the non-exclusive duration answer should be answered.", async () => { // Given - expect($(DurationPage.durationExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(DurationPage.durationExclusiveIPreferNotToSay()).isSelected()).toBe(false); // When - $(DurationPage.durationYears()).setValue("1"); - $(DurationPage.durationMonths()).setValue("7"); + await $(DurationPage.durationYears()).setValue("1"); + await $(DurationPage.durationMonths()).setValue("7"); // Then - expect($(DurationPage.durationYears()).getValue()).to.contain("1"); - expect($(DurationPage.durationMonths()).getValue()).to.contain("7"); - expect($(DurationPage.durationExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(DurationPage.durationYears()).getValue()).toBe("1"); + await expect(await $(DurationPage.durationMonths()).getValue()).toBe("7"); + await expect(await $(DurationPage.durationExclusiveIPreferNotToSay()).isSelected()).toBe(false); - $(DurationPage.submit()).click(); + await click(DurationPage.submit()); - expect($(SummaryPage.durationAnswer()).getText()).to.have.string("1 year 7 months"); - expect($(SummaryPage.durationAnswer()).getText()).to.not.have.string("I prefer not to say"); + await expect(await $(SummaryPage.durationAnswer()).getText()).toBe("1 year 7 months"); + await expect(await $(SummaryPage.durationAnswer()).getText()).not.toBe("I prefer not to say"); }); }); describe("Given the user has not answered the non-exclusive duration answer", () => { - it("When the user clicks the mutually exclusive checkbox answer, Then only the exclusive checkbox should be answered.", () => { + it("When the user clicks the mutually exclusive checkbox answer, Then only the exclusive checkbox should be answered.", async () => { // Given - expect($(DurationPage.durationYears()).getValue()).to.contain(""); - expect($(DurationPage.durationMonths()).getValue()).to.contain(""); + await browser.url("/questionnaire/mutually-exclusive-duration"); + await expect(await $(DurationPage.durationYears()).getValue()).toBe(""); + await expect(await $(DurationPage.durationMonths()).getValue()).toBe(""); // When - $(DurationPage.durationExclusiveIPreferNotToSay()).click(); - expect($(DurationPage.durationExclusiveIPreferNotToSay()).isSelected()).to.be.true; + await $(DurationPage.durationExclusiveIPreferNotToSay()).click(); + await expect(await $(DurationPage.durationExclusiveIPreferNotToSay()).isSelected()).toBe(true); // Then - $(DurationPage.submit()).click(); + await click(DurationPage.submit()); - expect($(SummaryPage.durationExclusiveAnswer()).getText()).to.have.string("I prefer not to say"); - expect($(SummaryPage.durationExclusiveAnswer()).getText()).to.not.have.string("1 year 7 months"); + await expect(await $(SummaryPage.durationExclusiveAnswer()).getText()).toBe("I prefer not to say"); + await expect(await $(SummaryPage.durationExclusiveAnswer()).getText()).not.toBe("1 year 7 months"); }); }); describe("Given the user has not answered the question and the question is optional", () => { - it("When the user clicks the Continue button, Then it should display `No answer provided`", () => { + it("When the user clicks the Continue button, Then it should display `No answer provided`", async () => { // Given - expect($(DurationPage.durationYears()).getValue()).to.contain(""); - expect($(DurationPage.durationMonths()).getValue()).to.contain(""); - expect($(DurationPage.durationExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await browser.url("/questionnaire/mutually-exclusive-duration"); + await expect(await $(DurationPage.durationYears()).getValue()).toBe(""); + await expect(await $(DurationPage.durationMonths()).getValue()).toBe(""); + await expect(await $(DurationPage.durationExclusiveIPreferNotToSay()).isSelected()).toBe(false); // When - $(DurationPage.submit()).click(); + await click(DurationPage.submit()); // Then - expect($(SummaryPage.durationAnswer()).getText()).to.contain("No answer provided"); + await expect(await $(SummaryPage.durationAnswer()).getText()).toBe("No answer provided"); }); }); }); diff --git a/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_month_year_date.spec.js b/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_month_year_date.spec.js index e2c583e4a2..b089da7571 100644 --- a/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_month_year_date.spec.js +++ b/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_month_year_date.spec.js @@ -1,109 +1,111 @@ import MonthYearDatePage from "../../../../generated_pages/mutually_exclusive/mutually-exclusive-month-year-date.page"; import SummaryPage from "../../../../generated_pages/mutually_exclusive/mutually-exclusive-month-year-date-section-summary.page"; +import { click } from "../../../../helpers"; describe("Component: Mutually Exclusive Month Year Date With Single Checkbox Override", () => { - beforeEach(() => { - browser.openQuestionnaire("test_mutually_exclusive.json"); - browser.url("/questionnaire/mutually-exclusive-month-year-date"); + beforeEach(async () => { + await browser.openQuestionnaire("test_mutually_exclusive.json"); + await browser.pause(100); + await browser.url("/questionnaire/mutually-exclusive-month-year-date"); }); describe("Given the user has entered a value for the non-exclusive month year date answer", () => { - it("When then user clicks the mutually exclusive checkbox answer, Then only the mutually exclusive checkbox should be answered.", () => { + it("When the user clicks the mutually exclusive checkbox answer, Then only the mutually exclusive checkbox should be answered.", async () => { // Given - $(MonthYearDatePage.monthYearDateMonth()).setValue("3"); - $(MonthYearDatePage.monthYearDateYear()).setValue("2018"); - expect($(MonthYearDatePage.monthYearDateMonth()).getValue()).to.contain("3"); - expect($(MonthYearDatePage.monthYearDateYear()).getValue()).to.contain("2018"); + await $(MonthYearDatePage.monthYearDateMonth()).setValue("3"); + await $(MonthYearDatePage.monthYearDateYear()).setValue("2018"); + await expect(await $(MonthYearDatePage.monthYearDateMonth()).getValue()).toBe("3"); + await expect(await $(MonthYearDatePage.monthYearDateYear()).getValue()).toBe("2018"); // When - $(MonthYearDatePage.monthYearDateExclusiveIPreferNotToSay()).click(); + await $(MonthYearDatePage.monthYearDateExclusiveIPreferNotToSay()).click(); // Then - expect($(MonthYearDatePage.monthYearDateExclusiveIPreferNotToSay()).isSelected()).to.be.true; - expect($(MonthYearDatePage.monthYearDateMonth()).getValue()).to.contain(""); - expect($(MonthYearDatePage.monthYearDateYear()).getValue()).to.contain(""); + await expect(await $(MonthYearDatePage.monthYearDateExclusiveIPreferNotToSay()).isSelected()).toBe(true); + await expect(await $(MonthYearDatePage.monthYearDateMonth()).getValue()).toBe(""); + await expect(await $(MonthYearDatePage.monthYearDateYear()).getValue()).toBe(""); - $(MonthYearDatePage.submit()).click(); + await click(MonthYearDatePage.submit()); - expect($(SummaryPage.monthYearDateExclusiveAnswer()).getText()).to.have.string("I prefer not to say"); - expect($(SummaryPage.monthYearDateExclusiveAnswer()).getText()).to.not.have.string("March 2018"); + await expect(await $(SummaryPage.monthYearDateExclusiveAnswer()).getText()).toBe("I prefer not to say"); + await expect(await $(SummaryPage.monthYearDateExclusiveAnswer()).getText()).not.toBe("March 2018"); }); }); describe("Given the user has clicked the mutually exclusive checkbox answer", () => { - it("When the user enters a value for the non-exclusive month year date answer and removes focus, Then only the non-exclusive month year date answer should be answered.", () => { + it("When the user enters a value for the non-exclusive month year date answer and removes focus, Then only the non-exclusive month year date answer should be answered.", async () => { // Given - $(MonthYearDatePage.monthYearDateExclusiveIPreferNotToSay()).click(); - expect($(MonthYearDatePage.monthYearDateExclusiveIPreferNotToSay()).isSelected()).to.be.true; + await $(MonthYearDatePage.monthYearDateExclusiveIPreferNotToSay()).click(); + await expect(await $(MonthYearDatePage.monthYearDateExclusiveIPreferNotToSay()).isSelected()).toBe(true); // When - $(MonthYearDatePage.monthYearDateMonth()).setValue("3"); - $(MonthYearDatePage.monthYearDateYear()).setValue("2018"); + await $(MonthYearDatePage.monthYearDateMonth()).setValue("3"); + await $(MonthYearDatePage.monthYearDateYear()).setValue("2018"); // Then - expect($(MonthYearDatePage.monthYearDateMonth()).getValue()).to.contain("3"); - expect($(MonthYearDatePage.monthYearDateYear()).getValue()).to.contain("2018"); + await expect(await $(MonthYearDatePage.monthYearDateMonth()).getValue()).toBe("3"); + await expect(await $(MonthYearDatePage.monthYearDateYear()).getValue()).toBe("2018"); - expect($(MonthYearDatePage.monthYearDateExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(MonthYearDatePage.monthYearDateExclusiveIPreferNotToSay()).isSelected()).toBe(false); - $(MonthYearDatePage.submit()).click(); + await click(MonthYearDatePage.submit()); - expect($(SummaryPage.monthYearDateAnswer()).getText()).to.have.string("March 2018"); - expect($(SummaryPage.monthYearDateAnswer()).getText()).to.not.have.string("I prefer not to say"); + await expect(await $(SummaryPage.monthYearDateAnswer()).getText()).toBe("March 2018"); + await expect(await $(SummaryPage.monthYearDateAnswer()).getText()).not.toBe("I prefer not to say"); }); }); describe("Given the user has not clicked the mutually exclusive checkbox answer", () => { - it("When the user enters a value for the non-exclusive month year date answer, Then only the non-exclusive month year date answer should be answered.", () => { + it("When the user enters a value for the non-exclusive month year date answer, Then only the non-exclusive month year date answer should be answered.", async () => { // Given - expect($(MonthYearDatePage.monthYearDateExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(MonthYearDatePage.monthYearDateExclusiveIPreferNotToSay()).isSelected()).toBe(false); // When - $(MonthYearDatePage.monthYearDateMonth()).setValue("3"); - $(MonthYearDatePage.monthYearDateYear()).setValue("2018"); + await $(MonthYearDatePage.monthYearDateMonth()).setValue("3"); + await $(MonthYearDatePage.monthYearDateYear()).setValue("2018"); // Then - expect($(MonthYearDatePage.monthYearDateMonth()).getValue()).to.contain("3"); - expect($(MonthYearDatePage.monthYearDateYear()).getValue()).to.contain("2018"); - expect($(MonthYearDatePage.monthYearDateExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(MonthYearDatePage.monthYearDateMonth()).getValue()).toBe("3"); + await expect(await $(MonthYearDatePage.monthYearDateYear()).getValue()).toBe("2018"); + await expect(await $(MonthYearDatePage.monthYearDateExclusiveIPreferNotToSay()).isSelected()).toBe(false); - $(MonthYearDatePage.submit()).click(); + await click(MonthYearDatePage.submit()); - expect($(SummaryPage.monthYearDateAnswer()).getText()).to.have.string("March 2018"); - expect($(SummaryPage.monthYearDateAnswer()).getText()).to.not.have.string("I prefer not to say"); + await expect(await $(SummaryPage.monthYearDateAnswer()).getText()).toBe("March 2018"); + await expect(await $(SummaryPage.monthYearDateAnswer()).getText()).not.toBe("I prefer not to say"); }); }); describe("Given the user has not answered the non-exclusive month year date answer", () => { - it("When the user clicks the mutually exclusive checkbox answer, Then only the exclusive checkbox should be answered.", () => { + it("When the user clicks the mutually exclusive checkbox answer, Then only the exclusive checkbox should be answered.", async () => { // Given - expect($(MonthYearDatePage.monthYearDateMonth()).getValue()).to.contain(""); - expect($(MonthYearDatePage.monthYearDateYear()).getValue()).to.contain(""); + await expect(await $(MonthYearDatePage.monthYearDateMonth()).getValue()).toBe(""); + await expect(await $(MonthYearDatePage.monthYearDateYear()).getValue()).toBe(""); // When - $(MonthYearDatePage.monthYearDateExclusiveIPreferNotToSay()).click(); - expect($(MonthYearDatePage.monthYearDateExclusiveIPreferNotToSay()).isSelected()).to.be.true; + await $(MonthYearDatePage.monthYearDateExclusiveIPreferNotToSay()).click(); + await expect(await $(MonthYearDatePage.monthYearDateExclusiveIPreferNotToSay()).isSelected()).toBe(true); // Then - $(MonthYearDatePage.submit()).click(); + await click(MonthYearDatePage.submit()); - expect($(SummaryPage.monthYearDateExclusiveAnswer()).getText()).to.have.string("I prefer not to say"); - expect($(SummaryPage.monthYearDateExclusiveAnswer()).getText()).to.not.have.string("March 2018"); + await expect(await $(SummaryPage.monthYearDateExclusiveAnswer()).getText()).toBe("I prefer not to say"); + await expect(await $(SummaryPage.monthYearDateExclusiveAnswer()).getText()).not.toBe("March 2018"); }); }); describe("Given the user has not answered the question and the question is optional", () => { - it("When the user clicks the Continue button, Then it should display `No answer provided`", () => { + it("When the user clicks the Continue button, Then it should display `No answer provided`", async () => { // Given - expect($(MonthYearDatePage.monthYearDateMonth()).getValue()).to.contain(""); - expect($(MonthYearDatePage.monthYearDateYear()).getValue()).to.contain(""); - expect($(MonthYearDatePage.monthYearDateExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(MonthYearDatePage.monthYearDateMonth()).getValue()).toBe(""); + await expect(await $(MonthYearDatePage.monthYearDateYear()).getValue()).toBe(""); + await expect(await $(MonthYearDatePage.monthYearDateExclusiveIPreferNotToSay()).isSelected()).toBe(false); // When - $(MonthYearDatePage.submit()).click(); + await click(MonthYearDatePage.submit()); // Then - expect($(SummaryPage.monthYearDateAnswer()).getText()).to.contain("No answer provided"); + await expect(await $(SummaryPage.monthYearDateAnswer()).getText()).toBe("No answer provided"); }); }); }); diff --git a/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_multiple_checkbox.spec.js b/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_multiple_checkbox.spec.js new file mode 100644 index 0000000000..fc505570af --- /dev/null +++ b/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_multiple_checkbox.spec.js @@ -0,0 +1,225 @@ +import MandatoryCheckboxPage from "../../../../generated_pages/mutually_exclusive_multiple/mutually-exclusive-checkbox.page"; +import SummaryPage from "../../../../generated_pages/mutually_exclusive_multiple/mutually-exclusive-checkbox-section-summary.page"; +import { click } from "../../../../helpers"; + +describe("Component: Mutually Exclusive Checkbox With Multiple Radio Override", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_mutually_exclusive_multiple.json"); + }); + + describe("Given the user has clicked multiple non-exclusive options", () => { + beforeEach(async () => { + // Given + await $(MandatoryCheckboxPage.checkboxBritish()).click(); + await $(MandatoryCheckboxPage.checkboxIrish()).click(); + await $(MandatoryCheckboxPage.checkboxOther()).click(); + await $(MandatoryCheckboxPage.checkboxOtherDetail()).setValue("The other option"); + + await expect(await $(MandatoryCheckboxPage.checkboxBritish()).isSelected()).toBe(true); + await expect(await $(MandatoryCheckboxPage.checkboxIrish()).isSelected()).toBe(true); + await expect(await $(MandatoryCheckboxPage.checkboxOther()).isSelected()).toBe(true); + await expect(await $(MandatoryCheckboxPage.checkboxOtherDetail()).getValue()).toBe("The other option"); + }); + + it("When then user clicks the first mutually exclusive option, Then only the first mutually exclusive option should be checked.", async () => { + // When + await $(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).click(); + await expect(await $(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).isSelected()).toBe(true); + await expect(await $(MandatoryCheckboxPage.checkboxExclusiveIAmAnAlien()).isSelected()).toBe(false); + + // Then + await expect(await $(MandatoryCheckboxPage.checkboxBritish()).isSelected()).toBe(false); + await expect(await $(MandatoryCheckboxPage.checkboxIrish()).isSelected()).toBe(false); + await expect(await $(MandatoryCheckboxPage.checkboxOther()).isSelected()).toBe(false); + await expect(await $(MandatoryCheckboxPage.checkboxOtherDetail()).getValue()).toBe(""); + + await click(MandatoryCheckboxPage.submit()); + + await expect(await $(SummaryPage.checkboxExclusiveAnswer()).getText()).toBe("I prefer not to say"); + await expect(await $(SummaryPage.checkboxExclusiveAnswer()).getText()).not.toBe("British\nIrish"); + }); + + it("When then user clicks the second mutually exclusive option, Then only the second mutually exclusive option should be checked.", async () => { + // When + await $(MandatoryCheckboxPage.checkboxExclusiveIAmAnAlien()).click(); + await expect(await $(MandatoryCheckboxPage.checkboxExclusiveIAmAnAlien()).isSelected()).toBe(true); + await expect(await $(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).isSelected()).toBe(false); + + // Then + await expect(await $(MandatoryCheckboxPage.checkboxBritish()).isSelected()).toBe(false); + await expect(await $(MandatoryCheckboxPage.checkboxIrish()).isSelected()).toBe(false); + await expect(await $(MandatoryCheckboxPage.checkboxOther()).isSelected()).toBe(false); + await expect(await $(MandatoryCheckboxPage.checkboxOtherDetail()).getValue()).toBe(""); + + await click(MandatoryCheckboxPage.submit()); + + await expect(await $(SummaryPage.checkboxExclusiveAnswer()).getText()).toBe("I am an alien"); + await expect(await $(SummaryPage.checkboxExclusiveAnswer()).getText()).not.toBe("British\nIrish"); + }); + }); + + describe("Given the user has clicked the first mutually exclusive option", () => { + it("When the user returns to the question, Then the mutually exclusive option should remain checked.", async () => { + // Given + await $(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).click(); + await click(MandatoryCheckboxPage.submit()); + + // When + await $(SummaryPage.previous()).click(); + + // Then + await expect(await $(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).isSelected()).toBe(true); + }); + }); + + describe("Given the user has clicked the second mutually exclusive option", () => { + it("When the user returns to the question, Then the mutually exclusive option should remain checked.", async () => { + // Given + await $(MandatoryCheckboxPage.checkboxExclusiveIAmAnAlien()).click(); + await click(MandatoryCheckboxPage.submit()); + + // When + await $(SummaryPage.previous()).click(); + + // Then + await expect(await $(MandatoryCheckboxPage.checkboxExclusiveIAmAnAlien()).isSelected()).toBe(true); + }); + }); + + describe("Given the user has clicked the first mutually exclusive option", () => { + it("When the user clicks the non-exclusive options, Then only the non-exclusive options should be checked.", async () => { + // Given + await $(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).click(); + await expect(await $(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).isSelected()).toBe(true); + + // When + await $(MandatoryCheckboxPage.checkboxBritish()).click(); + await $(MandatoryCheckboxPage.checkboxIrish()).click(); + + // Then + await expect(await $(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).isSelected()).toBe(false); + await expect(await $(MandatoryCheckboxPage.checkboxBritish()).isSelected()).toBe(true); + await expect(await $(MandatoryCheckboxPage.checkboxIrish()).isSelected()).toBe(true); + + await click(MandatoryCheckboxPage.submit()); + + await expect(await $(SummaryPage.checkboxAnswer()).getText()).toBe("British\nIrish"); + await expect(await $(SummaryPage.checkboxAnswer()).getText()).not.toBe("I prefer not to say"); + }); + }); + + describe("Given the user has clicked the second mutually exclusive option", () => { + it("When the user clicks the non-exclusive options, Then only the non-exclusive options should be checked.", async () => { + // Given + await $(MandatoryCheckboxPage.checkboxExclusiveIAmAnAlien()).click(); + await expect(await $(MandatoryCheckboxPage.checkboxExclusiveIAmAnAlien()).isSelected()).toBe(true); + + // When + await $(MandatoryCheckboxPage.checkboxBritish()).click(); + await $(MandatoryCheckboxPage.checkboxIrish()).click(); + + // Then + await expect(await $(MandatoryCheckboxPage.checkboxExclusiveIAmAnAlien()).isSelected()).toBe(false); + await expect(await $(MandatoryCheckboxPage.checkboxBritish()).isSelected()).toBe(true); + await expect(await $(MandatoryCheckboxPage.checkboxIrish()).isSelected()).toBe(true); + + await click(MandatoryCheckboxPage.submit()); + + await expect(await $(SummaryPage.checkboxAnswer()).getText()).toBe("British\nIrish"); + await expect(await $(SummaryPage.checkboxAnswer()).getText()).not.toBe("I am an alien"); + }); + }); + + describe("Given the user has not clicked a mutually exclusive option", () => { + it("When the user clicks multiple non-exclusive options, Then only the non-exclusive options should be checked.", async () => { + // Given + await expect(await $(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).isSelected()).toBe(false); + await expect(await $(MandatoryCheckboxPage.checkboxExclusiveIAmAnAlien()).isSelected()).toBe(false); + + // When + await $(MandatoryCheckboxPage.checkboxBritish()).click(); + await $(MandatoryCheckboxPage.checkboxIrish()).click(); + + // Then + await expect(await $(MandatoryCheckboxPage.checkboxBritish()).isSelected()).toBe(true); + await expect(await $(MandatoryCheckboxPage.checkboxIrish()).isSelected()).toBe(true); + + await click(MandatoryCheckboxPage.submit()); + + await expect(await $(SummaryPage.checkboxAnswer()).getText()).toBe("British\nIrish"); + await expect(await $(SummaryPage.checkboxAnswer()).getText()).not.toBe("I prefer not to say"); + await expect(await $(SummaryPage.checkboxAnswer()).getText()).not.toBe("I am an alien"); + }); + }); + + describe("Given the user has not clicked any of the non-exclusive options", () => { + beforeEach(async () => { + // Given + await expect(await $(MandatoryCheckboxPage.checkboxBritish()).isSelected()).toBe(false); + await expect(await $(MandatoryCheckboxPage.checkboxIrish()).isSelected()).toBe(false); + await expect(await $(MandatoryCheckboxPage.checkboxOther()).isSelected()).toBe(false); + }); + it("When the user clicks the first mutually exclusive option, Then only the first exclusive option should be checked.", async () => { + // When + await $(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).click(); + await expect(await $(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).isSelected()).toBe(true); + await expect(await $(MandatoryCheckboxPage.checkboxExclusiveIAmAnAlien()).isSelected()).toBe(false); + await click(MandatoryCheckboxPage.submit()); + + // Then + await expect(await $(SummaryPage.checkboxExclusiveAnswer()).getText()).toBe("I prefer not to say"); + await expect(await $(SummaryPage.checkboxExclusiveAnswer()).getText()).not.toBe("I am an alien"); + await expect(await $(SummaryPage.checkboxExclusiveAnswer()).getText()).not.toBe("British\nIrish"); + }); + it("When the user clicks the second mutually exclusive option, Then only the second exclusive option should be checked.", async () => { + // When + await $(MandatoryCheckboxPage.checkboxExclusiveIAmAnAlien()).click(); + await expect(await $(MandatoryCheckboxPage.checkboxExclusiveIAmAnAlien()).isSelected()).toBe(true); + await expect(await $(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).isSelected()).toBe(false); + await click(MandatoryCheckboxPage.submit()); + + // Then + await expect(await $(SummaryPage.checkboxExclusiveAnswer()).getText()).toBe("I am an alien"); + await expect(await $(SummaryPage.checkboxExclusiveAnswer()).getText()).not.toBe("I prefer not to say"); + await expect(await $(SummaryPage.checkboxExclusiveAnswer()).getText()).not.toBe("British\nIrish"); + }); + }); + + describe("Given the user has clicked a mutually exclusive option", () => { + it("When the user clicks another mutually exclusive option, Then only the most recently clicked mutually exclusive option should be checked.", async () => { + // Given + await $(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).click(); + await expect(await $(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).isSelected()).toBe(true); + await expect(await $(MandatoryCheckboxPage.checkboxExclusiveIAmAnAlien()).isSelected()).toBe(false); + + // When + await $(MandatoryCheckboxPage.checkboxExclusiveIAmAnAlien()).click(); + await expect(await $(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).isSelected()).toBe(false); + await expect(await $(MandatoryCheckboxPage.checkboxExclusiveIAmAnAlien()).isSelected()).toBe(true); + await click(MandatoryCheckboxPage.submit()); + + // Then + await expect(await $(SummaryPage.checkboxExclusiveAnswer()).getText()).toBe("I am an alien"); + await expect(await $(SummaryPage.checkboxExclusiveAnswer()).getText()).not.toBe("I prefer not to say"); + }); + }); + + describe("Given the user has not clicked any options and the question is mandatory", () => { + it("When the user clicks the Continue button, Then a validation error message should be displayed.", async () => { + // Given + await expect(await $(MandatoryCheckboxPage.checkboxBritish()).isSelected()).toBe(false); + await expect(await $(MandatoryCheckboxPage.checkboxIrish()).isSelected()).toBe(false); + await expect(await $(MandatoryCheckboxPage.checkboxOther()).isSelected()).toBe(false); + await expect(await $(MandatoryCheckboxPage.checkboxExclusiveIPreferNotToSay()).isSelected()).toBe(false); + await expect(await $(MandatoryCheckboxPage.checkboxExclusiveIAmAnAlien()).isSelected()).toBe(false); + + // When + await click(MandatoryCheckboxPage.submit()); + + // Then + await expect(await $(MandatoryCheckboxPage.errorHeader()).getText()).toBe("There is a problem with your answer"); + await expect(await $(MandatoryCheckboxPage.errorNumber(1)).getText()).toContain("Select at least one answer"); + await expect(await $(MandatoryCheckboxPage.questionErrorPanel()).isExisting()).toBe(true); + }); + }); +}); diff --git a/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_multiple_day_month_year_date.spec.js b/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_multiple_day_month_year_date.spec.js new file mode 100644 index 0000000000..036c53daa5 --- /dev/null +++ b/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_multiple_day_month_year_date.spec.js @@ -0,0 +1,208 @@ +import DatePage from "../../../../generated_pages/mutually_exclusive_multiple/mutually-exclusive-date.page"; +import SummaryPage from "../../../../generated_pages/mutually_exclusive_multiple/mutually-exclusive-date-section-summary.page"; +import { click } from "../../../../helpers"; + +describe("Component: Mutually Exclusive Day Month Year Date With Multiple Radio Override", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_mutually_exclusive_multiple.json"); + await browser.pause(100); + await browser.url("/questionnaire/mutually-exclusive-date"); + }); + describe("Given the user has entered a value for the non-exclusive month year date answer", () => { + beforeEach(async () => { + // Given + await $(DatePage.dateday()).setValue("17"); + await $(DatePage.datemonth()).setValue("3"); + await $(DatePage.dateyear()).setValue("2018"); + await expect(await $(DatePage.dateday()).getValue()).toBe("17"); + await expect(await $(DatePage.datemonth()).getValue()).toBe("3"); + await expect(await $(DatePage.dateyear()).getValue()).toBe("2018"); + }); + it("When then user clicks the first mutually exclusive radio answer, Then only the first mutually exclusive radio should be answered.", async () => { + // When + await $(DatePage.dateExclusiveIPreferNotToSay()).click(); + + // Then + await expect(await $(DatePage.dateExclusiveIPreferNotToSay()).isSelected()).toBe(true); + await expect(await $(DatePage.dateExclusiveIHaveNeverWorked()).isSelected()).toBe(false); + await expect(await $(DatePage.dateday()).getValue()).toBe(""); + await expect(await $(DatePage.datemonth()).getValue()).toBe(""); + await expect(await $(DatePage.dateyear()).getValue()).toBe(""); + + await click(DatePage.submit()); + + await expect(await $(SummaryPage.dateExclusiveAnswer()).getText()).toBe("I prefer not to say"); + await expect(await $(SummaryPage.dateExclusiveAnswer()).getText()).not.toBe("I have never worked"); + await expect(await $(SummaryPage.dateExclusiveAnswer()).getText()).not.toBe("17 March 2018"); + }); + + it("When then user clicks the second mutually exclusive radio answer, Then only the second mutually exclusive radio should be answered.", async () => { + // When + await $(DatePage.dateExclusiveIHaveNeverWorked()).click(); + + // Then + await expect(await $(DatePage.dateExclusiveIHaveNeverWorked()).isSelected()).toBe(true); + await expect(await $(DatePage.dateExclusiveIPreferNotToSay()).isSelected()).toBe(false); + await expect(await $(DatePage.dateday()).getValue()).toBe(""); + await expect(await $(DatePage.datemonth()).getValue()).toBe(""); + await expect(await $(DatePage.dateyear()).getValue()).toBe(""); + + await click(DatePage.submit()); + + await expect(await $(SummaryPage.dateExclusiveAnswer()).getText()).toBe("I have never worked"); + await expect(await $(SummaryPage.dateExclusiveAnswer()).getText()).not.toBe("I prefer not to say"); + await expect(await $(SummaryPage.dateExclusiveAnswer()).getText()).not.toBe("17 March 2018"); + }); + }); + + describe("Given the user has clicked the first mutually exclusive radio answer", () => { + it("When the user enters a value for the non-exclusive month year date answer and removes focus, Then only the non-exclusive month year date answer should be answered.", async () => { + // Given + await $(DatePage.dateExclusiveIPreferNotToSay()).click(); + await expect(await $(DatePage.dateExclusiveIPreferNotToSay()).isSelected()).toBe(true); + await expect(await $(DatePage.dateExclusiveIHaveNeverWorked()).isSelected()).toBe(false); + + // When + await $(DatePage.dateday()).setValue("17"); + await $(DatePage.datemonth()).setValue("3"); + await $(DatePage.dateyear()).setValue("2018"); + + // Then + await expect(await $(DatePage.dateday()).getValue()).toBe("17"); + await expect(await $(DatePage.datemonth()).getValue()).toBe("3"); + await expect(await $(DatePage.dateyear()).getValue()).toBe("2018"); + + await expect(await $(DatePage.dateExclusiveIPreferNotToSay()).isSelected()).toBe(false); + + await click(DatePage.submit()); + + await expect(await $(SummaryPage.dateAnswer()).getText()).toBe("17 March 2018"); + await expect(await $(SummaryPage.dateAnswer()).getText()).not.toBe("I prefer not to say"); + await expect(await $(SummaryPage.dateAnswer()).getText()).not.toBe("I have never worked"); + }); + }); + + describe("Given the user has clicked the second mutually exclusive radio answer", () => { + it("When the user enters a value for the non-exclusive month year date answer and removes focus, Then only the non-exclusive month year date answer should be answered.", async () => { + // Given + await $(DatePage.dateExclusiveIHaveNeverWorked()).click(); + await expect(await $(DatePage.dateExclusiveIHaveNeverWorked()).isSelected()).toBe(true); + await expect(await $(DatePage.dateExclusiveIPreferNotToSay()).isSelected()).toBe(false); + + // When + await $(DatePage.dateday()).setValue("17"); + await $(DatePage.datemonth()).setValue("3"); + await $(DatePage.dateyear()).setValue("2018"); + + // Then + await expect(await $(DatePage.dateday()).getValue()).toBe("17"); + await expect(await $(DatePage.datemonth()).getValue()).toBe("3"); + await expect(await $(DatePage.dateyear()).getValue()).toBe("2018"); + + await expect(await $(DatePage.dateExclusiveIHaveNeverWorked()).isSelected()).toBe(false); + + await click(DatePage.submit()); + + await expect(await $(SummaryPage.dateAnswer()).getText()).toBe("17 March 2018"); + await expect(await $(SummaryPage.dateAnswer()).getText()).not.toBe("I prefer not to say"); + await expect(await $(SummaryPage.dateAnswer()).getText()).not.toBe("I have never worked"); + }); + }); + + describe("Given the user has not clicked the mutually exclusive checkbox answer", () => { + it("When the user enters a value for the non-exclusive month year date answer, Then only the non-exclusive month year date answer should be answered.", async () => { + // Given + await expect(await $(DatePage.dateExclusiveIPreferNotToSay()).isSelected()).toBe(false); + await expect(await $(DatePage.dateExclusiveIHaveNeverWorked()).isSelected()).toBe(false); + + // When + await $(DatePage.dateday()).setValue("17"); + await $(DatePage.datemonth()).setValue("3"); + await $(DatePage.dateyear()).setValue("2018"); + + // Then + await expect(await $(DatePage.dateday()).getValue()).toBe("17"); + await expect(await $(DatePage.datemonth()).getValue()).toBe("3"); + await expect(await $(DatePage.dateyear()).getValue()).toBe("2018"); + await expect(await $(DatePage.dateExclusiveIPreferNotToSay()).isSelected()).toBe(false); + await expect(await $(DatePage.dateExclusiveIHaveNeverWorked()).isSelected()).toBe(false); + + await click(DatePage.submit()); + await expect(await $(SummaryPage.dateAnswer()).getText()).toBe("17 March 2018"); + await expect(await $(SummaryPage.dateAnswer()).getText()).not.toBe("I prefer not to say"); + await expect(await $(SummaryPage.dateAnswer()).getText()).not.toBe("I have never worked"); + }); + }); + + describe("Given the user has not answered the non-exclusive month year date answer", () => { + beforeEach(async () => { + // Given + await expect(await $(DatePage.dateday()).getValue()).toBe(""); + await expect(await $(DatePage.datemonth()).getValue()).toBe(""); + await expect(await $(DatePage.dateyear()).getValue()).toBe(""); + }); + it("When the user clicks the first mutually exclusive radio answer, Then only the first exclusive radio should be answered.", async () => { + // When + await $(DatePage.dateExclusiveIPreferNotToSay()).click(); + await expect(await $(DatePage.dateExclusiveIPreferNotToSay()).isSelected()).toBe(true); + await expect(await $(DatePage.dateExclusiveIHaveNeverWorked()).isSelected()).toBe(false); + + // Then + await click(DatePage.submit()); + + await expect(await $(SummaryPage.dateExclusiveAnswer()).getText()).toBe("I prefer not to say"); + await expect(await $(SummaryPage.dateExclusiveAnswer()).getText()).not.toBe("I have never worked"); + await expect(await $(SummaryPage.dateExclusiveAnswer()).getText()).not.toBe("17 March 2018"); + }); + + it("When the user clicks the second mutually exclusive radio answer, Then only the second exclusive radio should be answered.", async () => { + // When + await $(DatePage.dateExclusiveIHaveNeverWorked()).click(); + await expect(await $(DatePage.dateExclusiveIHaveNeverWorked()).isSelected()).toBe(true); + await expect(await $(DatePage.dateExclusiveIPreferNotToSay()).isSelected()).toBe(false); + + // Then + await click(DatePage.submit()); + + await expect(await $(SummaryPage.dateExclusiveAnswer()).getText()).toBe("I have never worked"); + await expect(await $(SummaryPage.dateExclusiveAnswer()).getText()).not.toBe("I prefer not to say"); + await expect(await $(SummaryPage.dateExclusiveAnswer()).getText()).not.toBe("17 March 2018"); + }); + }); + + describe("Given the user has not answered the question and the question is optional", () => { + it("When the user clicks the Continue button, Then it should display `No answer provided`", async () => { + // Given + await expect(await $(DatePage.dateday()).getValue()).toBe(""); + await expect(await $(DatePage.datemonth()).getValue()).toBe(""); + await expect(await $(DatePage.dateyear()).getValue()).toBe(""); + await expect(await $(DatePage.dateExclusiveIPreferNotToSay()).isSelected()).toBe(false); + await expect(await $(DatePage.dateExclusiveIHaveNeverWorked()).isSelected()).toBe(false); + + // When + await click(DatePage.submit()); + + // Then + await expect(await $(SummaryPage.dateAnswer()).getText()).toBe("No answer provided"); + }); + }); + + describe("Given the user has clicked a mutually exclusive option", () => { + it("When the user clicks another mutually exclusive option, Then only the most recently clicked mutually exclusive option should be checked.", async () => { + // Given + await $(DatePage.dateExclusiveIPreferNotToSay()).click(); + await expect(await $(DatePage.dateExclusiveIPreferNotToSay()).isSelected()).toBe(true); + await expect(await $(DatePage.dateExclusiveIHaveNeverWorked()).isSelected()).toBe(false); + + // When + await $(DatePage.dateExclusiveIHaveNeverWorked()).click(); + await expect(await $(DatePage.dateExclusiveIPreferNotToSay()).isSelected()).toBe(false); + await expect(await $(DatePage.dateExclusiveIHaveNeverWorked()).isSelected()).toBe(true); + await click(DatePage.submit()); + + // Then + await expect(await $(SummaryPage.dateExclusiveAnswer()).getText()).toBe("I have never worked"); + await expect(await $(SummaryPage.dateExclusiveAnswer()).getText()).not.toBe("I prefer not to say"); + }); + }); +}); diff --git a/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_multiple_textfield.spec.js b/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_multiple_textfield.spec.js new file mode 100644 index 0000000000..0ea9f981c9 --- /dev/null +++ b/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_multiple_textfield.spec.js @@ -0,0 +1,184 @@ +import TextFieldPage from "../../../../generated_pages/mutually_exclusive_multiple/mutually-exclusive-textfield.page"; +import SummaryPage from "../../../../generated_pages/mutually_exclusive_multiple/mutually-exclusive-textfield-section-summary.page"; +import { click } from "../../../../helpers"; + +describe("Component: Mutually Exclusive Textfield With Multiple Radio Override", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_mutually_exclusive_multiple.json"); + await browser.pause(100); + await browser.url("/questionnaire/mutually-exclusive-textfield"); + }); + + describe("Given the user has entered a value for the non-exclusive textfield answer", () => { + beforeEach(async () => { + // Given + await $(TextFieldPage.textfield()).setValue("Blue"); + await expect(await $(TextFieldPage.textfield()).getValue()).toBe("Blue"); + }); + it("When then user clicks the first mutually exclusive radio answer, Then only the first mutually exclusive radio should be answered.", async () => { + // When + await $(TextFieldPage.textfieldExclusiveIPreferNotToSay()).click(); + + // Then + await expect(await $(TextFieldPage.textfieldExclusiveIPreferNotToSay()).isSelected()).toBe(true); + await expect(await $(TextFieldPage.textfieldExclusiveIDontHaveAFavoriteColour()).isSelected()).toBe(false); + await expect(await $(TextFieldPage.textfield()).getValue()).toBe(""); + + await click(TextFieldPage.submit()); + + await expect(await $(SummaryPage.textfieldExclusiveAnswer()).getText()).toBe("I prefer not to say"); + await expect(await $(SummaryPage.textfieldExclusiveAnswer()).getText()).not.toBe("I dont have a favorite colour"); + await expect(await $(SummaryPage.textfieldExclusiveAnswer()).getText()).not.toBe("Blue"); + }); + it("When then user clicks the first mutually exclusive radio answer, Then only the first mutually exclusive radio should be answered.", async () => { + // When + await $(TextFieldPage.textfieldExclusiveIDontHaveAFavoriteColour()).click(); + + // Then + await expect(await $(TextFieldPage.textfieldExclusiveIDontHaveAFavoriteColour()).isSelected()).toBe(true); + await expect(await $(TextFieldPage.textfieldExclusiveIPreferNotToSay()).isSelected()).toBe(false); + await expect(await $(TextFieldPage.textfield()).getValue()).toBe(""); + + await click(TextFieldPage.submit()); + + await expect(await $(SummaryPage.textfieldExclusiveAnswer()).getText()).toBe("I dont have a favorite colour"); + await expect(await $(SummaryPage.textfieldExclusiveAnswer()).getText()).not.toBe("I prefer not to say"); + await expect(await $(SummaryPage.textfieldExclusiveAnswer()).getText()).not.toBe("Blue"); + }); + }); + + describe("Given the user has clicked the first mutually exclusive checkbox answer", () => { + it("When the user enters a value for the non-exclusive textfield answer and removes focus, Then only the non-exclusive textfield answer should be answered.", async () => { + // Given + await $(TextFieldPage.textfieldExclusiveIPreferNotToSay()).click(); + await expect(await $(TextFieldPage.textfieldExclusiveIPreferNotToSay()).isSelected()).toBe(true); + await expect(await $(TextFieldPage.textfieldExclusiveIDontHaveAFavoriteColour()).isSelected()).toBe(false); + + // When + await $(TextFieldPage.textfield()).setValue("Blue"); + + // Then + await expect(await $(TextFieldPage.textfield()).getValue()).toBe("Blue"); + await expect(await $(TextFieldPage.textfieldExclusiveIPreferNotToSay()).isSelected()).toBe(false); + await expect(await $(TextFieldPage.textfieldExclusiveIDontHaveAFavoriteColour()).isSelected()).toBe(false); + + await click(TextFieldPage.submit()); + + await expect(await $(SummaryPage.textfieldAnswer()).getText()).toBe("Blue"); + await expect(await $(SummaryPage.textfieldAnswer()).getText()).not.toBe("I prefer not to say"); + await expect(await $(SummaryPage.textfieldAnswer()).getText()).not.toBe("I dont have a favorite colour"); + }); + }); + + describe("Given the user has clicked the second mutually exclusive checkbox answer", () => { + it("When the user enters a value for the non-exclusive textfield answer and removes focus, Then only the non-exclusive textfield answer should be answered.", async () => { + // Given + await $(TextFieldPage.textfieldExclusiveIDontHaveAFavoriteColour()).click(); + await expect(await $(TextFieldPage.textfieldExclusiveIPreferNotToSay()).isSelected()).toBe(false); + await expect(await $(TextFieldPage.textfieldExclusiveIDontHaveAFavoriteColour()).isSelected()).toBe(true); + + // When + await $(TextFieldPage.textfield()).setValue("Blue"); + + // Then + await expect(await $(TextFieldPage.textfield()).getValue()).toBe("Blue"); + await expect(await $(TextFieldPage.textfieldExclusiveIPreferNotToSay()).isSelected()).toBe(false); + await expect(await $(TextFieldPage.textfieldExclusiveIDontHaveAFavoriteColour()).isSelected()).toBe(false); + + await click(TextFieldPage.submit()); + + await expect(await $(SummaryPage.textfieldAnswer()).getText()).toBe("Blue"); + await expect(await $(SummaryPage.textfieldAnswer()).getText()).not.toBe("I prefer not to say"); + await expect(await $(SummaryPage.textfieldAnswer()).getText()).not.toBe("I dont have a favorite colour"); + }); + }); + + describe("Given the user has not clicked the mutually exclusive checkbox answer", () => { + it("When the user enters a value for the non-exclusive textfield answer, Then only the non-exclusive textfield answer should be answered.", async () => { + // Given + await expect(await $(TextFieldPage.textfieldExclusiveIPreferNotToSay()).isSelected()).toBe(false); + await expect(await $(TextFieldPage.textfieldExclusiveIDontHaveAFavoriteColour()).isSelected()).toBe(false); + + // When + await $(TextFieldPage.textfield()).setValue("Blue"); + + // Then + await expect(await $(TextFieldPage.textfield()).getValue()).toBe("Blue"); + await expect(await $(TextFieldPage.textfieldExclusiveIPreferNotToSay()).isSelected()).toBe(false); + await expect(await $(TextFieldPage.textfieldExclusiveIDontHaveAFavoriteColour()).isSelected()).toBe(false); + + await click(TextFieldPage.submit()); + + await expect(await $(SummaryPage.textfieldAnswer()).getText()).toBe("Blue"); + await expect(await $(SummaryPage.textfieldAnswer()).getText()).not.toBe("I prefer not to say"); + await expect(await $(SummaryPage.textfieldAnswer()).getText()).not.toBe("I dont have a favorite colour"); + }); + }); + + describe("Given the user has not answered the non-exclusive textfield answer", () => { + beforeEach(async () => { + // Given + await expect(await $(TextFieldPage.textfield()).getValue()).toBe(""); + }); + it("When the user clicks the first mutually exclusive radio answer, Then only the first exclusive radio should be answered.", async () => { + // When + await $(TextFieldPage.textfieldExclusiveIPreferNotToSay()).click(); + await expect(await $(TextFieldPage.textfieldExclusiveIPreferNotToSay()).isSelected()).toBe(true); + await expect(await $(TextFieldPage.textfieldExclusiveIDontHaveAFavoriteColour()).isSelected()).toBe(false); + + // Then + await click(TextFieldPage.submit()); + + await expect(await $(SummaryPage.textfieldExclusiveAnswer()).getText()).toBe("I prefer not to say"); + await expect(await $(SummaryPage.textfieldExclusiveAnswer()).getText()).not.toBe("I dont have a favorite colour"); + await expect(await $(SummaryPage.textfieldExclusiveAnswer()).getText()).not.toBe("Blue"); + }); + it("When the user clicks the second mutually exclusive radio answer, Then only the second exclusive radio should be answered.", async () => { + // When + await $(TextFieldPage.textfieldExclusiveIDontHaveAFavoriteColour()).click(); + await expect(await $(TextFieldPage.textfieldExclusiveIDontHaveAFavoriteColour()).isSelected()).toBe(true); + await expect(await $(TextFieldPage.textfieldExclusiveIPreferNotToSay()).isSelected()).toBe(false); + + // Then + await click(TextFieldPage.submit()); + + await expect(await $(SummaryPage.textfieldExclusiveAnswer()).getText()).toBe("I dont have a favorite colour"); + await expect(await $(SummaryPage.textfieldExclusiveAnswer()).getText()).not.toBe("I prefer not to say"); + await expect(await $(SummaryPage.textfieldExclusiveAnswer()).getText()).not.toBe("Blue"); + }); + }); + + describe("Given the user has not answered the question and the question is optional", () => { + it("When the user clicks the Continue button, Then it should display `No answer provided`", async () => { + // Given + await expect(await $(TextFieldPage.textfield()).getValue()).toBe(""); + await expect(await $(TextFieldPage.textfieldExclusiveIPreferNotToSay()).isSelected()).toBe(false); + await expect(await $(TextFieldPage.textfieldExclusiveIDontHaveAFavoriteColour()).isSelected()).toBe(false); + + // When + await click(TextFieldPage.submit()); + + // Then + await expect(await $(SummaryPage.textfieldAnswer()).getText()).toBe("No answer provided"); + }); + }); + + describe("Given the user has clicked a mutually exclusive option", () => { + it("When the user clicks another mutually exclusive option, Then only the most recently clicked mutually exclusive option should be checked.", async () => { + // Given + await $(TextFieldPage.textfieldExclusiveIPreferNotToSay()).click(); + await expect(await $(TextFieldPage.textfieldExclusiveIPreferNotToSay()).isSelected()).toBe(true); + await expect(await $(TextFieldPage.textfieldExclusiveIDontHaveAFavoriteColour()).isSelected()).toBe(false); + + // When + await $(TextFieldPage.textfieldExclusiveIDontHaveAFavoriteColour()).click(); + await expect(await $(TextFieldPage.textfieldExclusiveIPreferNotToSay()).isSelected()).toBe(false); + await expect(await $(TextFieldPage.textfieldExclusiveIDontHaveAFavoriteColour()).isSelected()).toBe(true); + await click(TextFieldPage.submit()); + + // Then + await expect(await $(SummaryPage.textfieldExclusiveAnswer()).getText()).toBe("I dont have a favorite colour"); + await expect(await $(SummaryPage.textfieldExclusiveAnswer()).getText()).not.toBe("I prefer not to say"); + }); + }); +}); diff --git a/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_number.spec.js b/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_number.spec.js index f1b87b70d8..a0295cb499 100644 --- a/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_number.spec.js +++ b/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_number.spec.js @@ -1,99 +1,101 @@ import NumberPage from "../../../../generated_pages/mutually_exclusive/mutually-exclusive-number.page"; import SummaryPage from "../../../../generated_pages/mutually_exclusive/mutually-exclusive-number-section-summary.page"; +import { click } from "../../../../helpers"; describe("Component: Mutually Exclusive Number With Single Checkbox Override", () => { - beforeEach(() => { - browser.openQuestionnaire("test_mutually_exclusive.json"); - browser.url("/questionnaire/mutually-exclusive-number"); + beforeEach(async () => { + await browser.openQuestionnaire("test_mutually_exclusive.json"); + await browser.pause(100); + await browser.url("/questionnaire/mutually-exclusive-number"); }); describe("Given the user has entered a value for the non-exclusive number answer", () => { - it("When then user clicks the mutually exclusive checkbox answer, Then only the mutually exclusive checkbox should be answered.", () => { + it("When then user clicks the mutually exclusive checkbox answer, Then only the mutually exclusive checkbox should be answered.", async () => { // Given - $(NumberPage.number()).setValue("123"); - expect($(NumberPage.number()).getValue()).to.contain("123"); + await $(NumberPage.number()).setValue("123"); + await expect(await $(NumberPage.number()).getValue()).toBe("123"); // When - $(NumberPage.numberExclusiveIPreferNotToSay()).click(); + await $(NumberPage.numberExclusiveIPreferNotToSay()).click(); // Then - expect($(NumberPage.numberExclusiveIPreferNotToSay()).isSelected()).to.be.true; - expect($(NumberPage.number()).getValue()).to.contain(""); + await expect(await $(NumberPage.numberExclusiveIPreferNotToSay()).isSelected()).toBe(true); + await expect(await $(NumberPage.number()).getValue()).toBe(""); - $(NumberPage.submit()).click(); + await click(NumberPage.submit()); - expect($(SummaryPage.numberExclusiveAnswer()).getText()).to.have.string("I prefer not to say"); - expect($(SummaryPage.numberExclusiveAnswer()).getText()).to.not.have.string("123"); + await expect(await $(SummaryPage.numberExclusiveAnswer()).getText()).toBe("I prefer not to say"); + await expect(await $(SummaryPage.numberExclusiveAnswer()).getText()).not.toBe("123"); }); }); describe("Given the user has clicked the mutually exclusive checkbox answer", () => { - it("When the user enters a value for the non-exclusive number answer and removes focus, Then only the non-exclusive number answer should be answered.", () => { + it("When the user enters a value for the non-exclusive number answer and removes focus, Then only the non-exclusive number answer should be answered.", async () => { // Given - $(NumberPage.numberExclusiveIPreferNotToSay()).click(); - expect($(NumberPage.numberExclusiveIPreferNotToSay()).isSelected()).to.be.true; + await $(NumberPage.numberExclusiveIPreferNotToSay()).click(); + await expect(await $(NumberPage.numberExclusiveIPreferNotToSay()).isSelected()).toBe(true); // When - $(NumberPage.number()).setValue("123"); + await $(NumberPage.number()).setValue("123"); // Then - expect($(NumberPage.number()).getValue()).to.contain("123"); - expect($(NumberPage.numberExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(NumberPage.number()).getValue()).toBe("123"); + await expect(await $(NumberPage.numberExclusiveIPreferNotToSay()).isSelected()).toBe(false); - $(NumberPage.submit()).click(); + await click(NumberPage.submit()); - expect($(SummaryPage.numberAnswer()).getText()).to.have.string("123"); - expect($(SummaryPage.numberAnswer()).getText()).to.not.have.string("I prefer not to say"); + await expect(await $(SummaryPage.numberAnswer()).getText()).toBe("123"); + await expect(await $(SummaryPage.numberAnswer()).getText()).not.toBe("I prefer not to say"); }); }); describe("Given the user has not clicked the mutually exclusive checkbox answer", () => { - it("When the user enters a value for the non-exclusive number answer, Then only the non-exclusive number answer should be answered.", () => { + it("When the user enters a value for the non-exclusive number answer, Then only the non-exclusive number answer should be answered.", async () => { // Given - expect($(NumberPage.numberExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(NumberPage.numberExclusiveIPreferNotToSay()).isSelected()).toBe(false); // When - $(NumberPage.number()).setValue("123"); + await $(NumberPage.number()).setValue("123"); // Then - expect($(NumberPage.number()).getValue()).to.contain("123"); - expect($(NumberPage.numberExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(NumberPage.number()).getValue()).toBe("123"); + await expect(await $(NumberPage.numberExclusiveIPreferNotToSay()).isSelected()).toBe(false); - $(NumberPage.submit()).click(); + await click(NumberPage.submit()); - expect($(SummaryPage.numberAnswer()).getText()).to.have.string("123"); - expect($(SummaryPage.numberAnswer()).getText()).to.not.have.string("I prefer not to say"); + await expect(await $(SummaryPage.numberAnswer()).getText()).toBe("123"); + await expect(await $(SummaryPage.numberAnswer()).getText()).not.toBe("I prefer not to say"); }); }); describe("Given the user has not answered the non-exclusive number answer", () => { - it("When the user clicks the mutually exclusive checkbox answer, Then only the exclusive checkbox should be answered.", () => { + it("When the user clicks the mutually exclusive checkbox answer, Then only the exclusive checkbox should be answered.", async () => { // Given - expect($(NumberPage.number()).getValue()).to.contain(""); + await expect(await $(NumberPage.number()).getValue()).toBe(""); // When - $(NumberPage.numberExclusiveIPreferNotToSay()).click(); - expect($(NumberPage.numberExclusiveIPreferNotToSay()).isSelected()).to.be.true; + await $(NumberPage.numberExclusiveIPreferNotToSay()).click(); + await expect(await $(NumberPage.numberExclusiveIPreferNotToSay()).isSelected()).toBe(true); // Then - $(NumberPage.submit()).click(); + await click(NumberPage.submit()); - expect($(SummaryPage.numberExclusiveAnswer()).getText()).to.have.string("I prefer not to say"); - expect($(SummaryPage.numberExclusiveAnswer()).getText()).to.not.have.string("123"); + await expect(await $(SummaryPage.numberExclusiveAnswer()).getText()).toBe("I prefer not to say"); + await expect(await $(SummaryPage.numberExclusiveAnswer()).getText()).not.toBe("123"); }); }); describe("Given the user has not answered the question and the question is optional", () => { - it("When the user clicks the Continue button, Then it should display `No answer provided`", () => { + it("When the user clicks the Continue button, Then it should display `No answer provided`", async () => { // Given - expect($(NumberPage.number()).getValue()).to.contain(""); - expect($(NumberPage.numberExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(NumberPage.number()).getValue()).toBe(""); + await expect(await $(NumberPage.numberExclusiveIPreferNotToSay()).isSelected()).toBe(false); // When - $(NumberPage.submit()).click(); + await click(NumberPage.submit()); // Then - expect($(SummaryPage.numberAnswer()).getText()).to.contain("No answer provided"); + await expect(await $(SummaryPage.numberAnswer()).getText()).toBe("No answer provided"); }); }); }); diff --git a/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_percentage.spec.js b/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_percentage.spec.js index 5676f3bf07..c12e9413a9 100644 --- a/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_percentage.spec.js +++ b/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_percentage.spec.js @@ -1,99 +1,102 @@ import PercentagePage from "../../../../generated_pages/mutually_exclusive/mutually-exclusive-percentage.page"; import SummaryPage from "../../../../generated_pages/mutually_exclusive/mutually-exclusive-percentage-section-summary.page"; +import { click } from "../../../../helpers"; describe("Component: Mutually Exclusive Percentage With Single Checkbox Override", () => { - beforeEach(() => { - browser.openQuestionnaire("test_mutually_exclusive.json"); - browser.url("/questionnaire/mutually-exclusive-percentage"); + beforeEach(async () => { + await browser.openQuestionnaire("test_mutually_exclusive.json"); + await browser.pause(100); + await browser.url("/questionnaire/mutually-exclusive-percentage"); }); describe("Given the user has entered a value for the non-exclusive percentage answer", () => { - it("When then user clicks the mutually exclusive checkbox answer, Then only the mutually exclusive checkbox should be answered.", () => { + it("When then user clicks the mutually exclusive checkbox answer, Then only the mutually exclusive checkbox should be answered.", async () => { // Given - $(PercentagePage.percentage()).setValue("99"); - expect($(PercentagePage.percentage()).getValue()).to.contain("99"); + await $(PercentagePage.percentage()).setValue("99"); + await expect(await $(PercentagePage.percentage()).getValue()).toBe("99"); // When - $(PercentagePage.percentageExclusiveIPreferNotToSay()).click(); + await $(PercentagePage.percentageExclusiveIPreferNotToSay()).click(); // Then - expect($(PercentagePage.percentageExclusiveIPreferNotToSay()).isSelected()).to.be.true; - expect($(PercentagePage.percentage()).getValue()).to.contain(""); + await expect(await $(PercentagePage.percentageExclusiveIPreferNotToSay()).isSelected()).toBe(true); + await expect(await $(PercentagePage.percentage()).getValue()).toBe(""); - $(PercentagePage.submit()).click(); + await click(PercentagePage.submit()); - expect($(SummaryPage.percentageExclusiveAnswer()).getText()).to.have.string("I prefer not to say"); - expect($(SummaryPage.percentageExclusiveAnswer()).getText()).to.not.have.string("99"); + await expect(await $(SummaryPage.percentageExclusiveAnswer()).getText()).toBe("I prefer not to say"); + await expect(await $(SummaryPage.percentageExclusiveAnswer()).getText()).not.toBe("99"); }); }); describe("Given the user has clicked the mutually exclusive checkbox answer", () => { - it("When the user enters a value for the non-exclusive percentage answer and removes focus, Then only the non-exclusive percentage answer should be answered.", () => { + it("When the user enters a value for the non-exclusive percentage answer and removes focus, Then only the non-exclusive percentage answer should be answered.", async () => { // Given - $(PercentagePage.percentageExclusiveIPreferNotToSay()).click(); - expect($(PercentagePage.percentageExclusiveIPreferNotToSay()).isSelected()).to.be.true; + await browser.url("/questionnaire/mutually-exclusive-percentage"); + await $(PercentagePage.percentageExclusiveIPreferNotToSay()).click(); + await expect(await $(PercentagePage.percentageExclusiveIPreferNotToSay()).isSelected()).toBe(true); // When - $(PercentagePage.percentage()).setValue("99"); + await $(PercentagePage.percentage()).setValue("99"); // Then - expect($(PercentagePage.percentage()).getValue()).to.contain("99"); - expect($(PercentagePage.percentageExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(PercentagePage.percentage()).getValue()).toBe("99"); + await expect(await $(PercentagePage.percentageExclusiveIPreferNotToSay()).isSelected()).toBe(false); - $(PercentagePage.submit()).click(); + await click(PercentagePage.submit()); - expect($(SummaryPage.percentageAnswer()).getText()).to.have.string("99"); - expect($(SummaryPage.percentageAnswer()).getText()).to.not.have.string("I prefer not to say"); + await expect(await $(SummaryPage.percentageAnswer()).getText()).toBe("99%"); + await expect(await $(SummaryPage.percentageAnswer()).getText()).not.toBe("I prefer not to say"); }); }); describe("Given the user has not clicked the mutually exclusive checkbox answer", () => { - it("When the user enters a value for the non-exclusive percentage answer, Then only the non-exclusive percentage answer should be answered.", () => { + it("When the user enters a value for the non-exclusive percentage answer, Then only the non-exclusive percentage answer should be answered.", async () => { // Given - expect($(PercentagePage.percentageExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(PercentagePage.percentageExclusiveIPreferNotToSay()).isSelected()).toBe(false); // When - $(PercentagePage.percentage()).setValue("99"); + await $(PercentagePage.percentage()).setValue("99"); // Then - expect($(PercentagePage.percentage()).getValue()).to.contain("99"); - expect($(PercentagePage.percentageExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(PercentagePage.percentage()).getValue()).toBe("99"); + await expect(await $(PercentagePage.percentageExclusiveIPreferNotToSay()).isSelected()).toBe(false); - $(PercentagePage.submit()).click(); + await click(PercentagePage.submit()); - expect($(SummaryPage.percentageAnswer()).getText()).to.have.string("99"); - expect($(SummaryPage.percentageAnswer()).getText()).to.not.have.string("I prefer not to say"); + await expect(await $(SummaryPage.percentageAnswer()).getText()).toBe("99%"); + await expect(await $(SummaryPage.percentageAnswer()).getText()).not.toBe("I prefer not to say"); }); }); describe("Given the user has not answered the non-exclusive percentage answer", () => { - it("When the user clicks the mutually exclusive checkbox answer, Then only the exclusive checkbox should be answered.", () => { + it("When the user clicks the mutually exclusive checkbox answer, Then only the exclusive checkbox should be answered.", async () => { // Given - expect($(PercentagePage.percentage()).getValue()).to.contain(""); + await expect(await $(PercentagePage.percentage()).getValue()).toBe(""); // When - $(PercentagePage.percentageExclusiveIPreferNotToSay()).click(); - expect($(PercentagePage.percentageExclusiveIPreferNotToSay()).isSelected()).to.be.true; + await $(PercentagePage.percentageExclusiveIPreferNotToSay()).click(); + await expect(await $(PercentagePage.percentageExclusiveIPreferNotToSay()).isSelected()).toBe(true); // Then - $(PercentagePage.submit()).click(); + await click(PercentagePage.submit()); - expect($(SummaryPage.percentageExclusiveAnswer()).getText()).to.have.string("I prefer not to say"); - expect($(SummaryPage.percentageExclusiveAnswer()).getText()).to.not.have.string("British\nIrish"); + await expect(await $(SummaryPage.percentageExclusiveAnswer()).getText()).toBe("I prefer not to say"); + await expect(await $(SummaryPage.percentageExclusiveAnswer()).getText()).not.toBe("British\nIrish"); }); }); describe("Given the user has not answered the question and the question is optional", () => { - it("When the user clicks the Continue button, Then it should display `No answer provided`", () => { + it("When the user clicks the Continue button, Then it should display `No answer provided`", async () => { // Given - expect($(PercentagePage.percentage()).getValue()).to.contain(""); - expect($(PercentagePage.percentageExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(PercentagePage.percentage()).getValue()).toBe(""); + await expect(await $(PercentagePage.percentageExclusiveIPreferNotToSay()).isSelected()).toBe(false); // When - $(PercentagePage.submit()).click(); + await click(PercentagePage.submit()); // Then - expect($(SummaryPage.percentageAnswer()).getText()).to.contain("No answer provided"); + await expect(await $(SummaryPage.percentageAnswer()).getText()).toBe("No answer provided"); }); }); }); diff --git a/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_textarea.spec.js b/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_textarea.spec.js index e1b2044a19..08cb99c00c 100644 --- a/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_textarea.spec.js +++ b/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_textarea.spec.js @@ -1,59 +1,61 @@ import TextFieldPage from "../../../../generated_pages/mutually_exclusive/mutually-exclusive-textarea.page"; import SummaryPage from "../../../../generated_pages/mutually_exclusive/mutually-exclusive-textarea-section-summary.page"; +import { click } from "../../../../helpers"; describe("Component: Mutually Exclusive TextArea With Single Checkbox Override", () => { - beforeEach(() => { - browser.openQuestionnaire("test_mutually_exclusive.json"); - browser.url("/questionnaire/mutually-exclusive-textarea"); + beforeEach(async () => { + await browser.openQuestionnaire("test_mutually_exclusive.json"); + await browser.pause(100); + await browser.url("/questionnaire/mutually-exclusive-textarea"); }); describe("Given the user has not clicked the mutually exclusive checkbox answer", () => { - it("When the user enters a value for the non-exclusive textarea answer, Then only the non-exclusive textarea answer should be answered.", () => { + it("When the user enters a value for the non-exclusive textarea answer, Then only the non-exclusive textarea answer should be answered.", async () => { // Given - expect($(TextFieldPage.textareaExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(TextFieldPage.textareaExclusiveIPreferNotToSay()).isSelected()).toBe(false); // When - $(TextFieldPage.textarea()).setValue("Blue"); + await $(TextFieldPage.textarea()).setValue("Blue"); // Then - expect($(TextFieldPage.textarea()).getValue()).to.contain("Blue"); - expect($(TextFieldPage.textareaExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(TextFieldPage.textarea()).getValue()).toBe("Blue"); + await expect(await $(TextFieldPage.textareaExclusiveIPreferNotToSay()).isSelected()).toBe(false); - $(TextFieldPage.submit()).click(); + await click(TextFieldPage.submit()); - expect($(SummaryPage.textareaAnswer()).getText()).to.have.string("Blue"); - expect($(SummaryPage.textareaAnswer()).getText()).to.not.have.string("I prefer not to say"); + await expect(await $(SummaryPage.textareaAnswer()).getText()).toBe("Blue"); + await expect(await $(SummaryPage.textareaAnswer()).getText()).not.toBe("I prefer not to say"); }); }); describe("Given the user has not answered the non-exclusive textarea answer", () => { - it("When the user clicks the mutually exclusive checkbox answer, Then only the exclusive checkbox should be answered.", () => { + it("When the user clicks the mutually exclusive checkbox answer, Then only the exclusive checkbox should be answered.", async () => { // Given - expect($(TextFieldPage.textarea()).getValue()).to.contain(""); + await expect(await $(TextFieldPage.textarea()).getValue()).toBe(""); // When - $(TextFieldPage.textareaExclusiveIPreferNotToSay()).click(); - expect($(TextFieldPage.textareaExclusiveIPreferNotToSay()).isSelected()).to.be.true; + await $(TextFieldPage.textareaExclusiveIPreferNotToSay()).click(); + await expect(await $(TextFieldPage.textareaExclusiveIPreferNotToSay()).isSelected()).toBe(true); // Then - $(TextFieldPage.submit()).click(); + await click(TextFieldPage.submit()); - expect($(SummaryPage.textareaExclusiveAnswer()).getText()).to.have.string("I prefer not to say"); - expect($(SummaryPage.textareaExclusiveAnswer()).getText()).to.not.have.string("Blue"); + await expect(await $(SummaryPage.textareaExclusiveAnswer()).getText()).toBe("I prefer not to say"); + await expect(await $(SummaryPage.textareaExclusiveAnswer()).getText()).not.toBe("Blue"); }); }); describe("Given the user has not answered the question and the question is optional", () => { - it("When the user clicks the Continue button, Then it should display `No answer provided`", () => { + it("When the user clicks the Continue button, Then it should display `No answer provided`", async () => { // Given - expect($(TextFieldPage.textarea()).getValue()).to.contain(""); - expect($(TextFieldPage.textareaExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(TextFieldPage.textarea()).getValue()).toBe(""); + await expect(await $(TextFieldPage.textareaExclusiveIPreferNotToSay()).isSelected()).toBe(false); // When - $(TextFieldPage.submit()).click(); + await click(TextFieldPage.submit()); // Then - expect($(SummaryPage.textareaAnswer()).getText()).to.contain("No answer provided"); + await expect(await $(SummaryPage.textareaAnswer()).getText()).toBe("No answer provided"); }); }); }); diff --git a/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_textfield.spec.js b/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_textfield.spec.js index 3b83802a2e..cf7c1a44ed 100644 --- a/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_textfield.spec.js +++ b/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_textfield.spec.js @@ -1,99 +1,101 @@ import TextFieldPage from "../../../../generated_pages/mutually_exclusive/mutually-exclusive-textfield.page"; import SummaryPage from "../../../../generated_pages/mutually_exclusive/mutually-exclusive-textfield-section-summary.page"; +import { click } from "../../../../helpers"; describe("Component: Mutually Exclusive Textfield With Single Checkbox Override", () => { - beforeEach(() => { - browser.openQuestionnaire("test_mutually_exclusive.json"); - browser.url("/questionnaire/mutually-exclusive-textfield"); + beforeEach(async () => { + await browser.openQuestionnaire("test_mutually_exclusive.json"); + await browser.pause(100); + await browser.url("/questionnaire/mutually-exclusive-textfield"); }); describe("Given the user has entered a value for the non-exclusive textfield answer", () => { - it("When then user clicks the mutually exclusive checkbox answer, Then only the mutually exclusive checkbox should be answered.", () => { + it("When then user clicks the mutually exclusive checkbox answer, Then only the mutually exclusive checkbox should be answered.", async () => { // Given - $(TextFieldPage.textfield()).setValue("Blue"); - expect($(TextFieldPage.textfield()).getValue()).to.contain("Blue"); + await $(TextFieldPage.textfield()).setValue("Blue"); + await expect(await $(TextFieldPage.textfield()).getValue()).toBe("Blue"); // When - $(TextFieldPage.textfieldExclusiveIPreferNotToSay()).click(); + await $(TextFieldPage.textfieldExclusiveIPreferNotToSay()).click(); // Then - expect($(TextFieldPage.textfieldExclusiveIPreferNotToSay()).isSelected()).to.be.true; - expect($(TextFieldPage.textfield()).getValue()).to.contain(""); + await expect(await $(TextFieldPage.textfieldExclusiveIPreferNotToSay()).isSelected()).toBe(true); + await expect(await $(TextFieldPage.textfield()).getValue()).toBe(""); - $(TextFieldPage.submit()).click(); + await click(TextFieldPage.submit()); - expect($(SummaryPage.textfieldExclusiveAnswer()).getText()).to.have.string("I prefer not to say"); - expect($(SummaryPage.textfieldExclusiveAnswer()).getText()).to.not.have.string("Blue"); + await expect(await $(SummaryPage.textfieldExclusiveAnswer()).getText()).toBe("I prefer not to say"); + await expect(await $(SummaryPage.textfieldExclusiveAnswer()).getText()).not.toBe("Blue"); }); }); describe("Given the user has clicked the mutually exclusive checkbox answer", () => { - it("When the user enters a value for the non-exclusive textfield answer and removes focus, Then only the non-exclusive textfield answer should be answered.", () => { + it("When the user enters a value for the non-exclusive textfield answer and removes focus, Then only the non-exclusive textfield answer should be answered.", async () => { // Given - $(TextFieldPage.textfieldExclusiveIPreferNotToSay()).click(); - expect($(TextFieldPage.textfieldExclusiveIPreferNotToSay()).isSelected()).to.be.true; + await $(TextFieldPage.textfieldExclusiveIPreferNotToSay()).click(); + await expect(await $(TextFieldPage.textfieldExclusiveIPreferNotToSay()).isSelected()).toBe(true); // When - $(TextFieldPage.textfield()).setValue("Blue"); + await $(TextFieldPage.textfield()).setValue("Blue"); // Then - expect($(TextFieldPage.textfield()).getValue()).to.contain("Blue"); - expect($(TextFieldPage.textfieldExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(TextFieldPage.textfield()).getValue()).toBe("Blue"); + await expect(await $(TextFieldPage.textfieldExclusiveIPreferNotToSay()).isSelected()).toBe(false); - $(TextFieldPage.submit()).click(); + await click(TextFieldPage.submit()); - expect($(SummaryPage.textfieldAnswer()).getText()).to.have.string("Blue"); - expect($(SummaryPage.textfieldAnswer()).getText()).to.not.have.string("I prefer not to say"); + await expect(await $(SummaryPage.textfieldAnswer()).getText()).toBe("Blue"); + await expect(await $(SummaryPage.textfieldAnswer()).getText()).not.toBe("I prefer not to say"); }); }); describe("Given the user has not clicked the mutually exclusive checkbox answer", () => { - it("When the user enters a value for the non-exclusive textfield answer, Then only the non-exclusive textfield answer should be answered.", () => { + it("When the user enters a value for the non-exclusive textfield answer, Then only the non-exclusive textfield answer should be answered.", async () => { // Given - expect($(TextFieldPage.textfieldExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(TextFieldPage.textfieldExclusiveIPreferNotToSay()).isSelected()).toBe(false); // When - $(TextFieldPage.textfield()).setValue("Blue"); + await $(TextFieldPage.textfield()).setValue("Blue"); // Then - expect($(TextFieldPage.textfield()).getValue()).to.contain("Blue"); - expect($(TextFieldPage.textfieldExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(TextFieldPage.textfield()).getValue()).toBe("Blue"); + await expect(await $(TextFieldPage.textfieldExclusiveIPreferNotToSay()).isSelected()).toBe(false); - $(TextFieldPage.submit()).click(); + await click(TextFieldPage.submit()); - expect($(SummaryPage.textfieldAnswer()).getText()).to.have.string("Blue"); - expect($(SummaryPage.textfieldAnswer()).getText()).to.not.have.string("I prefer not to say"); + await expect(await $(SummaryPage.textfieldAnswer()).getText()).toBe("Blue"); + await expect(await $(SummaryPage.textfieldAnswer()).getText()).not.toBe("I prefer not to say"); }); }); describe("Given the user has not answered the non-exclusive textfield answer", () => { - it("When the user clicks the mutually exclusive checkbox answer, Then only the exclusive checkbox should be answered.", () => { + it("When the user clicks the mutually exclusive checkbox answer, Then only the exclusive checkbox should be answered.", async () => { // Given - expect($(TextFieldPage.textfield()).getValue()).to.contain(""); + await expect(await $(TextFieldPage.textfield()).getValue()).toBe(""); // When - $(TextFieldPage.textfieldExclusiveIPreferNotToSay()).click(); - expect($(TextFieldPage.textfieldExclusiveIPreferNotToSay()).isSelected()).to.be.true; + await $(TextFieldPage.textfieldExclusiveIPreferNotToSay()).click(); + await expect(await $(TextFieldPage.textfieldExclusiveIPreferNotToSay()).isSelected()).toBe(true); // Then - $(TextFieldPage.submit()).click(); + await click(TextFieldPage.submit()); - expect($(SummaryPage.textfieldExclusiveAnswer()).getText()).to.have.string("I prefer not to say"); - expect($(SummaryPage.textfieldExclusiveAnswer()).getText()).to.not.have.string("Blue"); + await expect(await $(SummaryPage.textfieldExclusiveAnswer()).getText()).toBe("I prefer not to say"); + await expect(await $(SummaryPage.textfieldExclusiveAnswer()).getText()).not.toBe("Blue"); }); }); describe("Given the user has not answered the question and the question is optional", () => { - it("When the user clicks the Continue button, Then it should display `No answer provided`", () => { + it("When the user clicks the Continue button, Then it should display `No answer provided`", async () => { // Given - expect($(TextFieldPage.textfield()).getValue()).to.contain(""); - expect($(TextFieldPage.textfieldExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(TextFieldPage.textfield()).getValue()).toBe(""); + await expect(await $(TextFieldPage.textfieldExclusiveIPreferNotToSay()).isSelected()).toBe(false); // When - $(TextFieldPage.submit()).click(); + await click(TextFieldPage.submit()); // Then - expect($(SummaryPage.textfieldAnswer()).getText()).to.contain("No answer provided"); + await expect(await $(SummaryPage.textfieldAnswer()).getText()).toBe("No answer provided"); }); }); }); diff --git a/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_unit.spec.js b/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_unit.spec.js index 9aaf41538a..b4e953a77c 100644 --- a/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_unit.spec.js +++ b/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_unit.spec.js @@ -1,99 +1,101 @@ import UnitPage from "../../../../generated_pages/mutually_exclusive/mutually-exclusive-unit.page"; import SummaryPage from "../../../../generated_pages/mutually_exclusive/mutually-exclusive-unit-section-summary.page"; +import { click } from "../../../../helpers"; describe("Component: Mutually Exclusive Unit With Single Checkbox Override", () => { - beforeEach(() => { - browser.openQuestionnaire("test_mutually_exclusive.json"); - browser.url("/questionnaire/mutually-exclusive-unit"); + beforeEach(async () => { + await browser.openQuestionnaire("test_mutually_exclusive.json"); + await browser.pause(100); + await browser.url("/questionnaire/mutually-exclusive-unit"); }); describe("Given the user has entered a value for the non-exclusive unit answer", () => { - it("When then user clicks the mutually exclusive checkbox answer, Then only the mutually exclusive checkbox should be answered.", () => { + it("When then user clicks the mutually exclusive checkbox answer, Then only the mutually exclusive checkbox should be answered.", async () => { // Given - $(UnitPage.unit()).setValue("10"); - expect($(UnitPage.unit()).getValue()).to.contain("10"); + await $(UnitPage.unit()).setValue("10"); + await expect(await $(UnitPage.unit()).getValue()).toBe("10"); // When - $(UnitPage.unitExclusiveIPreferNotToSay()).click(); + await $(UnitPage.unitExclusiveIPreferNotToSay()).click(); // Then - expect($(UnitPage.unitExclusiveIPreferNotToSay()).isSelected()).to.be.true; - expect($(UnitPage.unit()).getValue()).to.contain(""); + await expect(await $(UnitPage.unitExclusiveIPreferNotToSay()).isSelected()).toBe(true); + await expect(await $(UnitPage.unit()).getValue()).toBe(""); - $(UnitPage.submit()).click(); + await click(UnitPage.submit()); - expect($(SummaryPage.unitExclusiveAnswer()).getText()).to.have.string("I prefer not to say"); - expect($(SummaryPage.unitExclusiveAnswer()).getText()).to.not.have.string("10"); + await expect(await $(SummaryPage.unitExclusiveAnswer()).getText()).toBe("I prefer not to say"); + await expect(await $(SummaryPage.unitExclusiveAnswer()).getText()).not.toBe("10"); }); }); describe("Given the user has clicked the mutually exclusive checkbox answer", () => { - it("When the user enters a value for the non-exclusive unit answer and removes focus, Then only the non-exclusive unit answer should be answered.", () => { + it("When the user enters a value for the non-exclusive unit answer and removes focus, Then only the non-exclusive unit answer should be answered.", async () => { // Given - $(UnitPage.unitExclusiveIPreferNotToSay()).click(); - expect($(UnitPage.unitExclusiveIPreferNotToSay()).isSelected()).to.be.true; + await $(UnitPage.unitExclusiveIPreferNotToSay()).click(); + await expect(await $(UnitPage.unitExclusiveIPreferNotToSay()).isSelected()).toBe(true); // When - $(UnitPage.unit()).setValue("10"); + await $(UnitPage.unit()).setValue("10"); // Then - expect($(UnitPage.unit()).getValue()).to.contain("10"); - expect($(UnitPage.unitExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(UnitPage.unit()).getValue()).toContain("10"); + await expect(await $(UnitPage.unitExclusiveIPreferNotToSay()).isSelected()).toBe(false); - $(UnitPage.submit()).click(); + await click(UnitPage.submit()); - expect($(SummaryPage.unitAnswer()).getText()).to.have.string("10"); - expect($(SummaryPage.unitAnswer()).getText()).to.not.have.string("I prefer not to say"); + await expect(await $(SummaryPage.unitAnswer()).getText()).toContain("10"); + await expect(await $(SummaryPage.unitAnswer()).getText()).not.toBe("I prefer not to say"); }); }); describe("Given the user has not clicked the mutually exclusive checkbox answer", () => { - it("When the user enters a value for the non-exclusive unit answer, Then only the non-exclusive unit answer should be answered.", () => { + it("When the user enters a value for the non-exclusive unit answer, Then only the non-exclusive unit answer should be answered.", async () => { // Given - expect($(UnitPage.unitExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(UnitPage.unitExclusiveIPreferNotToSay()).isSelected()).toBe(false); // When - $(UnitPage.unit()).setValue("10"); + await $(UnitPage.unit()).setValue("10"); // Then - expect($(UnitPage.unit()).getValue()).to.contain("10"); - expect($(UnitPage.unitExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(UnitPage.unit()).getValue()).toBe("10"); + await expect(await $(UnitPage.unitExclusiveIPreferNotToSay()).isSelected()).toBe(false); - $(UnitPage.submit()).click(); + await click(UnitPage.submit()); - expect($(SummaryPage.unitAnswer()).getText()).to.have.string("10"); - expect($(SummaryPage.unitAnswer()).getText()).to.not.have.string("I prefer not to say"); + await expect(await $(SummaryPage.unitAnswer()).getText()).toContain("10"); + await expect(await $(SummaryPage.unitAnswer()).getText()).not.toBe("I prefer not to say"); }); }); describe("Given the user has not answered the non-exclusive unit answer", () => { - it("When the user clicks the mutually exclusive checkbox answer, Then only the exclusive checkbox should be answered.", () => { + it("When the user clicks the mutually exclusive checkbox answer, Then only the exclusive checkbox should be answered.", async () => { // Given - expect($(UnitPage.unit()).getValue()).to.contain(""); + await expect(await $(UnitPage.unit()).getValue()).toBe(""); // When - $(UnitPage.unitExclusiveIPreferNotToSay()).click(); - expect($(UnitPage.unitExclusiveIPreferNotToSay()).isSelected()).to.be.true; + await $(UnitPage.unitExclusiveIPreferNotToSay()).click(); + await expect(await $(UnitPage.unitExclusiveIPreferNotToSay()).isSelected()).toBe(true); // Then - $(UnitPage.submit()).click(); + await click(UnitPage.submit()); - expect($(SummaryPage.unitExclusiveAnswer()).getText()).to.have.string("I prefer not to say"); - expect($(SummaryPage.unitExclusiveAnswer()).getText()).to.not.have.string("10"); + await expect(await $(SummaryPage.unitExclusiveAnswer()).getText()).toBe("I prefer not to say"); + await expect(await $(SummaryPage.unitExclusiveAnswer()).getText()).not.toBe("10"); }); }); describe("Given the user has not answered the question and the question is optional", () => { - it("When the user clicks the Continue button, Then it should display `No answer provided`", () => { + it("When the user clicks the Continue button, Then it should display `No answer provided`", async () => { // Given - expect($(UnitPage.unit()).getValue()).to.contain(""); - expect($(UnitPage.unitExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(UnitPage.unit()).getValue()).toBe(""); + await expect(await $(UnitPage.unitExclusiveIPreferNotToSay()).isSelected()).toBe(false); // When - $(UnitPage.submit()).click(); + await click(UnitPage.submit()); // Then - expect($(SummaryPage.unitAnswer()).getText()).to.contain("No answer provided"); + await expect(await $(SummaryPage.unitAnswer()).getText()).toBe("No answer provided"); }); }); }); diff --git a/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_year_date.spec.js b/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_year_date.spec.js index 1ce56cd09f..14a7d8f69c 100644 --- a/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_year_date.spec.js +++ b/tests/functional/spec/components/checkbox/mutually_exclusive/mutually_exclusive_year_date.spec.js @@ -1,99 +1,101 @@ import YearDatePage from "../../../../generated_pages/mutually_exclusive/mutually-exclusive-year-date.page"; import SubmitPage from "../../../../generated_pages/mutually_exclusive/mutually-exclusive-year-date-section-summary.page"; +import { click } from "../../../../helpers"; describe("Component: Mutually Exclusive Year Date With Single Checkbox Override", () => { - beforeEach(() => { - browser.openQuestionnaire("test_mutually_exclusive.json"); - browser.url("/questionnaire/mutually-exclusive-year-date"); + beforeEach(async () => { + await browser.openQuestionnaire("test_mutually_exclusive.json"); + await browser.pause(100); + await browser.url("/questionnaire/mutually-exclusive-year-date"); }); describe("Given the user has entered a value for the non-exclusive year date answer", () => { - it("When then user clicks the mutually exclusive checkbox answer, Then only the mutually exclusive checkbox should be answered.", () => { + it("When then user clicks the mutually exclusive checkbox answer, Then only the mutually exclusive checkbox should be answered.", async () => { // Given - $(YearDatePage.yearDateYear()).setValue("2018"); - expect($(YearDatePage.yearDateYear()).getValue()).to.contain("2018"); + await $(YearDatePage.yearDateYear()).setValue("2018"); + await expect(await $(YearDatePage.yearDateYear()).getValue()).toBe("2018"); // When - $(YearDatePage.yearDateExclusiveIPreferNotToSay()).click(); + await $(YearDatePage.yearDateExclusiveIPreferNotToSay()).click(); // Then - expect($(YearDatePage.yearDateExclusiveIPreferNotToSay()).isSelected()).to.be.true; - expect($(YearDatePage.yearDateYear()).getValue()).to.contain(""); + await expect(await $(YearDatePage.yearDateExclusiveIPreferNotToSay()).isSelected()).toBe(true); + await expect(await $(YearDatePage.yearDateYear()).getValue()).toBe(""); - $(YearDatePage.submit()).click(); + await click(YearDatePage.submit()); - expect($(SubmitPage.yearDateExclusiveAnswer()).getText()).to.have.string("I prefer not to say"); - expect($(SubmitPage.yearDateExclusiveAnswer()).getText()).to.not.have.string("2018"); + await expect(await $(SubmitPage.yearDateExclusiveAnswer()).getText()).toBe("I prefer not to say"); + await expect(await $(SubmitPage.yearDateExclusiveAnswer()).getText()).not.toBe("2018"); }); }); describe("Given the user has clicked the mutually exclusive checkbox answer", () => { - it("When the user enters a value for the non-exclusive year date answer and removes focus, Then only the non-exclusive year date answer should be answered.", () => { + it("When the user enters a value for the non-exclusive year date answer and removes focus, Then only the non-exclusive year date answer should be answered.", async () => { // Given - $(YearDatePage.yearDateExclusiveIPreferNotToSay()).click(); - expect($(YearDatePage.yearDateExclusiveIPreferNotToSay()).isSelected()).to.be.true; + await $(YearDatePage.yearDateExclusiveIPreferNotToSay()).click(); + await expect(await $(YearDatePage.yearDateExclusiveIPreferNotToSay()).isSelected()).toBe(true); // When - $(YearDatePage.yearDateYear()).setValue("2018"); + await $(YearDatePage.yearDateYear()).setValue("2018"); // Then - expect($(YearDatePage.yearDateYear()).getValue()).to.contain("2018"); - expect($(YearDatePage.yearDateExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(YearDatePage.yearDateYear()).getValue()).toBe("2018"); + await expect(await $(YearDatePage.yearDateExclusiveIPreferNotToSay()).isSelected()).toBe(false); - $(YearDatePage.submit()).click(); + await click(YearDatePage.submit()); - expect($(SubmitPage.yearDateAnswer()).getText()).to.have.string("2018"); - expect($(SubmitPage.yearDateAnswer()).getText()).to.not.have.string("I prefer not to say"); + await expect(await $(SubmitPage.yearDateAnswer()).getText()).toBe("2018"); + await expect(await $(SubmitPage.yearDateAnswer()).getText()).not.toBe("I prefer not to say"); }); }); describe("Given the user has not clicked the mutually exclusive checkbox answer", () => { - it("When the user enters a value for the non-exclusive year date answer, Then only the non-exclusive year date answer should be answered.", () => { + it("When the user enters a value for the non-exclusive year date answer, Then only the non-exclusive year date answer should be answered.", async () => { // Given - expect($(YearDatePage.yearDateExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(YearDatePage.yearDateExclusiveIPreferNotToSay()).isSelected()).toBe(false); // When - $(YearDatePage.yearDateYear()).setValue("2018"); + await $(YearDatePage.yearDateYear()).setValue("2018"); // Then - expect($(YearDatePage.yearDateYear()).getValue()).to.contain("2018"); - expect($(YearDatePage.yearDateExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(YearDatePage.yearDateYear()).getValue()).toBe("2018"); + await expect(await $(YearDatePage.yearDateExclusiveIPreferNotToSay()).isSelected()).toBe(false); - $(YearDatePage.submit()).click(); + await click(YearDatePage.submit()); - expect($(SubmitPage.yearDateAnswer()).getText()).to.have.string("2018"); - expect($(SubmitPage.yearDateAnswer()).getText()).to.not.have.string("I prefer not to say"); + await expect(await $(SubmitPage.yearDateAnswer()).getText()).toBe("2018"); + await expect(await $(SubmitPage.yearDateAnswer()).getText()).not.toBe("I prefer not to say"); }); }); describe("Given the user has not answered the non-exclusive year date answer", () => { - it("When the user clicks the mutually exclusive checkbox answer, Then only the exclusive checkbox should be answered.", () => { + it("When the user clicks the mutually exclusive checkbox answer, Then only the exclusive checkbox should be answered.", async () => { // Given - expect($(YearDatePage.yearDateYear()).getValue()).to.contain(""); + await expect(await $(YearDatePage.yearDateYear()).getValue()).toBe(""); // When - $(YearDatePage.yearDateExclusiveIPreferNotToSay()).click(); - expect($(YearDatePage.yearDateExclusiveIPreferNotToSay()).isSelected()).to.be.true; + await $(YearDatePage.yearDateExclusiveIPreferNotToSay()).click(); + await expect(await $(YearDatePage.yearDateExclusiveIPreferNotToSay()).isSelected()).toBe(true); // Then - $(YearDatePage.submit()).click(); + await click(YearDatePage.submit()); - expect($(SubmitPage.yearDateExclusiveAnswer()).getText()).to.have.string("I prefer not to say"); - expect($(SubmitPage.yearDateExclusiveAnswer()).getText()).to.not.have.string("2018"); + await expect(await $(SubmitPage.yearDateExclusiveAnswer()).getText()).toBe("I prefer not to say"); + await expect(await $(SubmitPage.yearDateExclusiveAnswer()).getText()).not.toBe("2018"); }); }); describe("Given the user has not answered the question and the question is optional", () => { - it("When the user clicks the Continue button, Then it should display `No answer provided`", () => { + it("When the user clicks the Continue button, Then it should display `No answer provided`", async () => { // Given - expect($(YearDatePage.yearDateYear()).getValue()).to.contain(""); - expect($(YearDatePage.yearDateExclusiveIPreferNotToSay()).isSelected()).to.be.false; + await expect(await $(YearDatePage.yearDateYear()).getValue()).toBe(""); + await expect(await $(YearDatePage.yearDateExclusiveIPreferNotToSay()).isSelected()).toBe(false); // When - $(YearDatePage.submit()).click(); + await click(YearDatePage.submit()); // Then - expect($(SubmitPage.yearDateAnswer()).getText()).to.contain("No answer provided"); + await expect(await $(SubmitPage.yearDateAnswer()).getText()).toBe("No answer provided"); }); }); }); diff --git a/tests/functional/spec/components/dropdown/dropdown.spec.js b/tests/functional/spec/components/dropdown/dropdown.spec.js index 1c7a5c36c5..cdc1986718 100644 --- a/tests/functional/spec/components/dropdown/dropdown.spec.js +++ b/tests/functional/spec/components/dropdown/dropdown.spec.js @@ -3,96 +3,97 @@ import DropdownMandatorySummary from "../../../generated_pages/dropdown_mandator import DropdownMandatoryOverriddenPage from "../../../generated_pages/dropdown_mandatory_with_overridden_error/dropdown-mandatory-with-overridden-error.page"; import DropdownOptionalPage from "../../../generated_pages/dropdown_optional/dropdown-optional.page"; import DropdownOptionalSummary from "../../../generated_pages/dropdown_optional/submit.page"; +import { click } from "../../../helpers"; describe("Component: Dropdown", () => { // Mandatory describe("Given I start a Mandatory Dropdown survey", () => { - beforeEach(() => { - browser.openQuestionnaire("test_dropdown_mandatory.json"); + beforeEach(async () => { + await browser.openQuestionnaire("test_dropdown_mandatory.json"); }); - it("When I have selected a dropdown option, Then the selected option should be displayed in the summary", () => { - $(DropdownMandatoryPage.answer()).selectByAttribute("value", "Rugby is better!"); - $(DropdownMandatoryPage.submit()).click(); - expect($(DropdownMandatorySummary.dropdownMandatoryAnswer()).getText()).to.contain("Rugby is better!"); + it("When I have selected a dropdown option, Then the selected option should be displayed in the summary", async () => { + await $(DropdownMandatoryPage.answer()).selectByAttribute("value", "Rugby is better!"); + await click(DropdownMandatoryPage.submit()); + await expect(await $(DropdownMandatorySummary.dropdownMandatoryAnswer()).getText()).toBe("Rugby is better!"); }); - it("When I have not selected a dropdown option and click Continue, Then the default error message should be displayed", () => { - $(DropdownMandatoryPage.submit()).click(); - expect($(DropdownMandatoryPage.errorNumber(1)).getText()).to.contain("Select an answer"); + it("When I have not selected a dropdown option and click Continue, Then the default error message should be displayed", async () => { + await click(DropdownMandatoryPage.submit()); + await expect(await $(DropdownMandatoryPage.errorNumber(1)).getText()).toBe("Select an answer"); }); - it("When I have selected a dropdown option and I try to select a default (disabled) dropdown option, Then the already selected option should be displayed in summary", () => { - $(DropdownMandatoryPage.answer()).selectByAttribute("value", "Liverpool"); - $(DropdownMandatoryPage.answer()).selectByAttribute("value", ""); - $(DropdownMandatoryPage.submit()).click(); - expect($(DropdownMandatorySummary.dropdownMandatoryAnswer()).getText()).to.contain("Liverpool"); + it("When I have selected a dropdown option and I try to select a default (disabled) dropdown option, Then the already selected option should be displayed in summary", async () => { + await $(DropdownMandatoryPage.answer()).selectByAttribute("value", "Liverpool"); + await $(DropdownMandatoryPage.answer()).selectByAttribute("value", ""); + await click(DropdownMandatoryPage.submit()); + await expect(await $(DropdownMandatorySummary.dropdownMandatoryAnswer()).getText()).toBe("Liverpool"); }); - it("When I click the dropdown label, Then the dropdown should be focused", () => { - $(DropdownMandatoryPage.answerLabel()).click(); - expect($(DropdownMandatoryPage.answer()).isFocused()).to.be.true; + it("When I click the dropdown label, Then the dropdown should be focused", async () => { + await $(DropdownMandatoryPage.answerLabel()).click(); + await expect(await $(DropdownMandatoryPage.answer()).isFocused()).toBe(true); }); - it("When I'm on the summary page and I click Edit then Continue, Then the answer on the summary page should be unchanged", () => { - $(DropdownMandatoryPage.answer()).selectByAttribute("value", "Rugby is better!"); - $(DropdownMandatoryPage.submit()).click(); - expect($(DropdownMandatorySummary.dropdownMandatoryAnswer()).getText()).to.contain("Rugby is better!"); - $(DropdownMandatorySummary.dropdownMandatoryAnswerEdit()).click(); - $(DropdownMandatoryPage.submit()).click(); - expect($(DropdownMandatorySummary.dropdownMandatoryAnswer()).getText()).to.contain("Rugby is better!"); + it("When I'm on the summary page and I click Edit then Continue, Then the answer on the summary page should be unchanged", async () => { + await $(DropdownMandatoryPage.answer()).selectByAttribute("value", "Rugby is better!"); + await click(DropdownMandatoryPage.submit()); + await expect(await $(DropdownMandatorySummary.dropdownMandatoryAnswer()).getText()).toBe("Rugby is better!"); + await $(DropdownMandatorySummary.dropdownMandatoryAnswerEdit()).click(); + await click(DropdownMandatoryPage.submit()); + await expect(await $(DropdownMandatorySummary.dropdownMandatoryAnswer()).getText()).toBe("Rugby is better!"); }); - it("When I'm on the summary page and I click Edit and change the answer, Then the newly selected answer should be displayed in the summary", () => { - $(DropdownMandatoryPage.answer()).selectByAttribute("value", "Rugby is better!"); - $(DropdownMandatoryPage.submit()).click(); - expect($(DropdownMandatorySummary.dropdownMandatoryAnswer()).getText()).to.contain("Rugby is better!"); - $(DropdownMandatorySummary.dropdownMandatoryAnswerEdit()).click(); - $(DropdownMandatoryPage.submit()).click(); - expect($(DropdownMandatorySummary.dropdownMandatoryAnswer()).getText()).to.contain("Rugby is better!"); - $(DropdownMandatorySummary.dropdownMandatoryAnswerEdit()).click(); - $(DropdownMandatoryPage.answer()).selectByAttribute("value", "Liverpool"); - $(DropdownMandatoryPage.submit()).click(); - expect($(DropdownMandatorySummary.dropdownMandatoryAnswer()).getText()).to.contain("Liverpool"); + it("When I'm on the summary page and I click Edit and change the answer, Then the newly selected answer should be displayed in the summary", async () => { + await $(DropdownMandatoryPage.answer()).selectByAttribute("value", "Rugby is better!"); + await click(DropdownMandatoryPage.submit()); + await expect(await $(DropdownMandatorySummary.dropdownMandatoryAnswer()).getText()).toBe("Rugby is better!"); + await $(DropdownMandatorySummary.dropdownMandatoryAnswerEdit()).click(); + await click(DropdownMandatoryPage.submit()); + await expect(await $(DropdownMandatorySummary.dropdownMandatoryAnswer()).getText()).toBe("Rugby is better!"); + await $(DropdownMandatorySummary.dropdownMandatoryAnswerEdit()).click(); + await $(DropdownMandatoryPage.answer()).selectByAttribute("value", "Liverpool"); + await click(DropdownMandatoryPage.submit()); + await expect(await $(DropdownMandatorySummary.dropdownMandatoryAnswer()).getText()).toBe("Liverpool"); }); }); describe("Given I start a Mandatory With Overridden Error Dropdown survey", () => { - before(() => { - browser.openQuestionnaire("test_dropdown_mandatory_with_overridden_error.json"); + before(async () => { + await browser.openQuestionnaire("test_dropdown_mandatory_with_overridden_error.json"); }); - it("When I have not selected a dropdown option and click Continue, Then the overridden error message should be displayed", () => { - $(DropdownMandatoryOverriddenPage.submit()).click(); - expect($(DropdownMandatoryOverriddenPage.errorNumber(1)).getText()).to.contain("Overridden test error message."); + it("When I have not selected a dropdown option and click Continue, Then the overridden error message should be displayed", async () => { + await click(DropdownMandatoryOverriddenPage.submit()); + await expect(await $(DropdownMandatoryOverriddenPage.errorNumber(1)).getText()).toBe("Overridden test error message."); }); }); // Optional describe("Given I start a Optional Dropdown survey", () => { - beforeEach(() => { - browser.openQuestionnaire("test_dropdown_optional.json"); + beforeEach(async () => { + await browser.openQuestionnaire("test_dropdown_optional.json"); }); - it('When I have not selected a dropdown option, Then the summary should display "No answer provided"', () => { - $(DropdownOptionalPage.submit()).click(); - expect($(DropdownOptionalSummary.dropdownOptionalAnswer()).getText()).to.contain("No answer provided"); + it('When I have not selected a dropdown option, Then the summary should display "No answer provided"', async () => { + await click(DropdownOptionalPage.submit()); + await expect(await $(DropdownOptionalSummary.dropdownOptionalAnswer()).getText()).toBe("No answer provided"); }); - it("When I have selected a dropdown option, Then the selected option should be displayed in the summary", () => { - $(DropdownOptionalPage.answer()).selectByAttribute("value", "Rugby is better!"); - $(DropdownOptionalPage.submit()).click(); - expect($(DropdownOptionalSummary.dropdownOptionalAnswer()).getText()).to.contain("Rugby is better!"); + it("When I have selected a dropdown option, Then the selected option should be displayed in the summary", async () => { + await $(DropdownOptionalPage.answer()).selectByAttribute("value", "Rugby is better!"); + await click(DropdownOptionalPage.submit()); + await expect(await $(DropdownOptionalSummary.dropdownOptionalAnswer()).getText()).toBe("Rugby is better!"); }); - it('When I have selected a dropdown option and I reselect the default option (Select an answer), Then the summary should display "No answer provided"', () => { - $(DropdownOptionalPage.answer()).selectByAttribute("value", "Chelsea"); - $(DropdownOptionalPage.submit()).click(); - expect($(DropdownOptionalSummary.dropdownOptionalAnswer()).getText()).to.contain("Chelsea"); - $(DropdownOptionalSummary.dropdownOptionalAnswerEdit()).click(); - $(DropdownOptionalPage.answer()).selectByAttribute("value", ""); - $(DropdownOptionalPage.submit()).click(); - expect($(DropdownOptionalSummary.dropdownOptionalAnswer()).getText()).to.contain("No answer provided"); + it('When I have selected a dropdown option and I reselect the default option (Select an answer), Then the summary should display "No answer provided"', async () => { + await $(DropdownOptionalPage.answer()).selectByAttribute("value", "Chelsea"); + await click(DropdownOptionalPage.submit()); + await expect(await $(DropdownOptionalSummary.dropdownOptionalAnswer()).getText()).toBe("Chelsea"); + await $(DropdownOptionalSummary.dropdownOptionalAnswerEdit()).click(); + await $(DropdownOptionalPage.answer()).selectByAttribute("value", ""); + await click(DropdownOptionalPage.submit()); + await expect(await $(DropdownOptionalSummary.dropdownOptionalAnswer()).getText()).toBe("No answer provided"); }); }); }); diff --git a/tests/functional/spec/components/radio/radio.js b/tests/functional/spec/components/radio/radio.js index d2bbcba1b6..01b1c08b4c 100644 --- a/tests/functional/spec/components/radio/radio.js +++ b/tests/functional/spec/components/radio/radio.js @@ -15,131 +15,131 @@ import RadioNonMandatoryDetailAnswerOverriddenPage from "../../../generated_page import RadioNonMandatoryDetailAnswerPage from "../../../generated_pages/radio_optional_with_detail_answer_mandatory/radio-non-mandatory.page"; import RadioNonMandatoryDetailAnswerSummary from "../../../generated_pages/radio_optional_with_detail_answer_mandatory/submit.page"; - +import { click, verifyUrlContains } from "../../../helpers"; describe("Component: Radio", () => { describe("Given I start a Mandatory Radio survey", () => { - before(() => { - browser.openQuestionnaire("test_radio_mandatory.json"); + before(async () => { + await browser.openQuestionnaire("test_radio_mandatory.json"); }); - it("When I have selected a radio option that contains an escaped character, Then the selected option should be displayed in the summary", () => { - $(RadioMandatoryPage.teaCoffee()).click(); - $(RadioMandatoryPage.submit()).click(); - expect(browser.getUrl()).to.contain(RadioMandatorySummary.pageName); - expect($(RadioMandatorySummary.radioMandatoryAnswer()).getText()).to.contain("Tea & Coffee"); + it("When I have selected a radio option that contains an escaped character, Then the selected option should be displayed in the summary", async () => { + await $(RadioMandatoryPage.teaCoffee()).click(); + await click(RadioMandatoryPage.submit()); + await verifyUrlContains(RadioMandatorySummary.pageName); + await expect(await $(RadioMandatorySummary.radioMandatoryAnswer()).getText()).toBe("Tea & Coffee"); }); }); describe("Given I start a Mandatory Radio survey", () => { - before(() => { - browser.openQuestionnaire("test_radio_mandatory.json"); + before(async () => { + await browser.openQuestionnaire("test_radio_mandatory.json"); }); - it("When I have selected a radio option, Then the selected option should be displayed in the summary", () => { - $(RadioMandatoryPage.coffee()).click(); - $(RadioMandatoryPage.submit()).click(); - expect(browser.getUrl()).to.contain(RadioMandatorySummary.pageName); - expect($(RadioMandatorySummary.radioMandatoryAnswer()).getText()).to.contain("Coffee"); + it("When I have selected a radio option, Then the selected option should be displayed in the summary", async () => { + await $(RadioMandatoryPage.coffee()).click(); + await click(RadioMandatoryPage.submit()); + await verifyUrlContains(RadioMandatorySummary.pageName); + await expect(await $(RadioMandatorySummary.radioMandatoryAnswer()).getText()).toBe("Coffee"); }); }); describe("Given I start a Mandatory Radio survey ", () => { - before(() => { - browser.openQuestionnaire("test_radio_mandatory.json"); + before(async () => { + await browser.openQuestionnaire("test_radio_mandatory.json"); }); - it("When I have submitted the page without any option, Then the question text is hidden in the error message using a span element", () => { - $(RadioMandatoryOverriddenPage.submit()).click(); - expect($(RadioMandatoryOverriddenPage.errorNumber(1)).getHTML()).to.contain( - 'Select an answer to ‘What do you prefer for breakfast?’' + it("When I have submitted the page without any option, Then the question text is hidden in the error message using a span element", async () => { + await click(RadioMandatoryOverriddenPage.submit()); + await expect(await $(RadioMandatoryOverriddenPage.errorNumber(1)).getHTML()).toContain( + 'Select an answer to ‘What do you prefer for breakfast?’', ); }); }); describe("Given I start a Mandatory Radio DetailAnswer survey", () => { - before(() => { - browser.openQuestionnaire("test_radio_mandatory_with_detail_answer_mandatory.json"); + before(async () => { + await browser.openQuestionnaire("test_radio_mandatory_with_detail_answer_mandatory.json"); }); - it("When I have selected a other text field, Then the selected option should be displayed in the summary", () => { - $(RadioMandatoryOptionalDetailAnswerPage.other()).click(); - $(RadioMandatoryOptionalDetailAnswerPage.otherDetail()).setValue("Hello World"); - $(RadioMandatoryOptionalDetailAnswerPage.submit()).click(); - expect(browser.getUrl()).to.contain(RadioMandatoryOptionDetailAnswerSummary.pageName); - expect($(RadioMandatoryOptionDetailAnswerSummary.radioMandatoryAnswer()).getText()).to.contain("Hello World"); + it("When I have selected a other text field, Then the selected option should be displayed in the summary", async () => { + await $(RadioMandatoryOptionalDetailAnswerPage.other()).click(); + await $(RadioMandatoryOptionalDetailAnswerPage.otherDetail()).setValue("Hello World"); + await click(RadioMandatoryOptionalDetailAnswerPage.submit()); + await verifyUrlContains(RadioMandatoryOptionDetailAnswerSummary.pageName); + await expect(await $(RadioMandatoryOptionDetailAnswerSummary.radioMandatoryAnswer()).getText()).toContain("Hello World"); }); }); describe("Given I start a Mandatory Radio DetailAnswer Overridden Error survey ", () => { - before(() => { - browser.openQuestionnaire("test_radio_mandatory_with_detail_answer_mandatory_with_overridden_error.json"); + before(async () => { + await browser.openQuestionnaire("test_radio_mandatory_with_detail_answer_mandatory_with_overridden_error.json"); }); - it("When I submit without any data in the other text field it should Then throw an overridden error", () => { - $(RadioMandatoryDetailAnswerOverriddenPage.other()).click(); - $(RadioMandatoryDetailAnswerOverriddenPage.submit()).click(); - expect($(RadioMandatoryDetailAnswerOverriddenPage.errorNumber(1)).getText()).to.contain("Test error message is overridden"); + it("When I submit without any data in the other text field it should Then throw an overridden error", async () => { + await $(RadioMandatoryDetailAnswerOverriddenPage.other()).click(); + await click(RadioMandatoryDetailAnswerOverriddenPage.submit()); + await expect(await $(RadioMandatoryDetailAnswerOverriddenPage.errorNumber(1)).getText()).toBe("Test error message is overridden"); }); }); describe("Given I start a Mandatory Radio DetailAnswer survey ", () => { - before(() => { - browser.openQuestionnaire("test_radio_mandatory_with_detail_answer_optional.json"); + before(async () => { + await browser.openQuestionnaire("test_radio_mandatory_with_detail_answer_optional.json"); }); - it("When I submit without any data in the other text field is selected, Then the selected option should be displayed in the summary", () => { - $(RadioMandatoryOptionalDetailAnswerPage.submit()).click(); - expect(browser.getUrl()).to.contain(RadioMandatoryOptionDetailAnswerSummary.pageName); - expect($(RadioMandatoryOptionDetailAnswerSummary.radioMandatoryAnswer()).getText()).to.contain("No answer provided"); + it("When I submit without any data in the other text field is selected, Then the selected option should be displayed in the summary", async () => { + await click(RadioMandatoryOptionalDetailAnswerPage.submit()); + await verifyUrlContains(RadioMandatoryOptionDetailAnswerSummary.pageName); + await expect(await $(RadioMandatoryOptionDetailAnswerSummary.radioMandatoryAnswer()).getText()).toContain("No answer provided"); }); }); describe("Given I start a Mandatory Radio DetailAnswer Overridden error survey ", () => { - before(() => { - browser.openQuestionnaire("test_radio_mandatory_with_overridden_error.json"); + before(async () => { + await browser.openQuestionnaire("test_radio_mandatory_with_overridden_error.json"); }); - it("When I have submitted the page without any option, Then an overridden error is displayed", () => { - $(RadioMandatoryOverriddenPage.submit()).click(); - expect($(RadioMandatoryOverriddenPage.errorNumber(1)).getText()).to.contain("Test error message is overridden"); + it("When I have submitted the page without any option, Then an overridden error is displayed", async () => { + await click(RadioMandatoryOverriddenPage.submit()); + await expect(await $(RadioMandatoryOverriddenPage.errorNumber(1)).getText()).toBe("Test error message is overridden"); }); }); describe("Given I start a Optional survey", () => { - before(() => { - browser.openQuestionnaire("test_radio_optional.json"); + before(async () => { + await browser.openQuestionnaire("test_radio_optional.json"); }); - it("When I have selected no option, Then the selected option should be displayed in the summary", () => { - $(RadioNonMandatoryPage.submit()).click(); - expect(browser.getUrl()).to.contain(RadioNonMandatorySummary.pageName); - expect($(RadioNonMandatorySummary.radioNonMandatoryAnswer()).getText()).to.contain("No answer provided"); + it("When I have selected no option, Then the selected option should be displayed in the summary", async () => { + await click(RadioNonMandatoryPage.submit()); + await verifyUrlContains(RadioNonMandatorySummary.pageName); + await expect(await $(RadioNonMandatorySummary.radioNonMandatoryAnswer()).getText()).toBe("No answer provided"); }); }); describe("Given I start a Optional DetailAnswer Overridden error survey", () => { - before(() => { - browser.openQuestionnaire("test_radio_optional_with_detail_answer_mandatory_with_overridden_error.json"); + before(async () => { + await browser.openQuestionnaire("test_radio_optional_with_detail_answer_mandatory_with_overridden_error.json"); }); - it("When I have submitted an other option with an empty text field, Then an overridden error is displayed", () => { - $(RadioNonMandatoryDetailAnswerOverriddenPage.other()).click(); - $(RadioNonMandatoryDetailAnswerOverriddenPage.submit()).click(); - expect($(RadioNonMandatoryDetailAnswerOverriddenPage.errorNumber(1)).getText()).to.contain("Test error message is overridden"); + it("When I have submitted an other option with an empty text field, Then an overridden error is displayed", async () => { + await $(RadioNonMandatoryDetailAnswerOverriddenPage.other()).click(); + await click(RadioNonMandatoryDetailAnswerOverriddenPage.submit()); + await expect(await $(RadioNonMandatoryDetailAnswerOverriddenPage.errorNumber(1)).getText()).toBe("Test error message is overridden"); }); }); describe("Given I Start a Optional Mandatory DetailAnswer survey", () => { - before(() => { - browser.openQuestionnaire("test_radio_optional_with_detail_answer_mandatory.json"); + before(async () => { + await browser.openQuestionnaire("test_radio_optional_with_detail_answer_mandatory.json"); }); - it("When I submit data in the other text field it should be persisted and Then displayed on the summary", () => { - $(RadioNonMandatoryDetailAnswerPage.other()).click(); - $(RadioNonMandatoryDetailAnswerPage.otherDetail()).setValue("Hello World"); - $(RadioNonMandatoryDetailAnswerPage.submit()).click(); - expect(browser.getUrl()).to.contain(RadioNonMandatoryDetailAnswerSummary.pageName); - expect($(RadioNonMandatoryDetailAnswerSummary.radioNonMandatoryAnswer()).getText()).to.contain("Hello World"); + it("When I submit data in the other text field it should be persisted and Then displayed on the summary", async () => { + await $(RadioNonMandatoryDetailAnswerPage.other()).click(); + await $(RadioNonMandatoryDetailAnswerPage.otherDetail()).setValue("Hello World"); + await click(RadioNonMandatoryDetailAnswerPage.submit()); + await verifyUrlContains(RadioNonMandatoryDetailAnswerSummary.pageName); + await expect(await $(RadioNonMandatoryDetailAnswerSummary.radioNonMandatoryAnswer()).getText()).toContain("Hello World"); }); }); }); diff --git a/tests/functional/spec/components/radio/radio_detail_answer_dropdown.spec.js b/tests/functional/spec/components/radio/radio_detail_answer_dropdown.spec.js index d68d3ce3b2..c7f26ad488 100644 --- a/tests/functional/spec/components/radio/radio_detail_answer_dropdown.spec.js +++ b/tests/functional/spec/components/radio/radio_detail_answer_dropdown.spec.js @@ -1,81 +1,81 @@ import RadioDropdownPage from "../../../generated_pages/radio_detail_answer_dropdown/optional-radio-with-dropdown-detail-answer-block.page"; import SubmitPage from "../../../generated_pages/radio_detail_answer_dropdown/submit.page"; import DropdownMandatoryPage from "../../../generated_pages/dropdown_mandatory/dropdown-mandatory.page"; - +import { click } from "../../../helpers"; describe("Optional Radio with a Dropdown detail answer", () => { - beforeEach(() => { - browser.openQuestionnaire("test_radio_detail_answer_dropdown.json"); + beforeEach(async () => { + await browser.openQuestionnaire("test_radio_detail_answer_dropdown.json"); }); describe("Given an optional radio with a dropdown detail answer", () => { - it("When a placeholder is set for the detail answer, Then that value should be displayed as the first option", () => { - $(RadioDropdownPage.fruit()).click(); + it("When a placeholder is set for the detail answer, Then that value should be displayed as the first option", async () => { + await $(RadioDropdownPage.fruit()).click(); - expect($(RadioDropdownPage.fruitDetail()).getText()).to.contain("Select fruit"); + await expect(await $(RadioDropdownPage.fruitDetail()).getText()).toContain("Select fruit"); }); - it("When a placeholder is not set for the detail answer, Then the default placeholder should be displayed as the first option", () => { - $(RadioDropdownPage.jam()).click(); + it("When a placeholder is not set for the detail answer, Then the default placeholder should be displayed as the first option", async () => { + await $(RadioDropdownPage.jam()).click(); - expect($(RadioDropdownPage.jamDetail()).getText()).to.contain("Select an answer"); + await expect(await $(RadioDropdownPage.jamDetail()).getText()).toContain("Select an answer"); }); - it("When the user does not provide an answer and submits, Then the summary should display 'No answer provided'", () => { - $(RadioDropdownPage.submit()).click(); + it("When the user does not provide an answer and submits, Then the summary should display 'No answer provided'", async () => { + await click(RadioDropdownPage.submit()); - expect($(SubmitPage.optionalRadioWithDropdownDetailAnswer()).getText()).to.equal("No answer provided"); + await expect(await $(SubmitPage.optionalRadioWithDropdownDetailAnswer()).getText()).toBe("No answer provided"); }); - it("When the user selects an option with an optional detail answer but does not provide a detail answer, Then the summary should display the chosen option without the detail answer", () => { - $(RadioDropdownPage.fruit()).click(); - $(RadioDropdownPage.submit()).click(); + it("When the user selects an option with an optional detail answer but does not provide a detail answer, Then the summary should display the chosen option without the detail answer", async () => { + await $(RadioDropdownPage.fruit()).click(); + await click(RadioDropdownPage.submit()); - expect($(SubmitPage.optionalRadioWithDropdownDetailAnswer()).getText()).to.equal("Fruit"); + await expect(await $(SubmitPage.optionalRadioWithDropdownDetailAnswer()).getText()).toBe("Fruit"); }); - it("When the user selects an option with an optional detail answer and provides a detail answer, Then the summary should display the chosen option and the detail answer", () => { - $(RadioDropdownPage.fruit()).click(); - $(RadioDropdownPage.fruitDetail()).selectByAttribute("value", "Mango"); - $(RadioDropdownPage.submit()).click(); + it("When the user selects an option with an optional detail answer and provides a detail answer, Then the summary should display the chosen option and the detail answer", async () => { + await $(RadioDropdownPage.fruit()).click(); + await $(RadioDropdownPage.fruitDetail()).selectByAttribute("value", "Mango"); + await click(RadioDropdownPage.submit()); - expect($(SubmitPage.optionalRadioWithDropdownDetailAnswer()).getText()).to.equal("Fruit\nMango"); + await expect(await $(SubmitPage.optionalRadioWithDropdownDetailAnswer()).getText()).toBe("Fruit\nMango"); }); - it("When the user selects the default dropdown option after submitting a detail answer, Then the summary should not display the detail answer", () => { - $(RadioDropdownPage.fruit()).click(); - $(RadioDropdownPage.fruitDetail()).selectByAttribute("value", "Mango"); - $(RadioDropdownPage.submit()).click(); - $(SubmitPage.previous()).click(); - $(RadioDropdownPage.fruitDetail()).selectByVisibleText("Select fruit"); - $(RadioDropdownPage.submit()).click(); + it("When the user selects the default dropdown option after submitting a detail answer, Then the summary should not display the detail answer", async () => { + await $(RadioDropdownPage.fruit()).click(); + await $(RadioDropdownPage.fruitDetail()).selectByAttribute("value", "Mango"); + await click(RadioDropdownPage.submit()); + await $(SubmitPage.previous()).click(); + await $(RadioDropdownPage.fruitDetail()).selectByVisibleText("Select fruit"); + await click(RadioDropdownPage.submit()); - expect($(SubmitPage.optionalRadioWithDropdownDetailAnswer()).getText()).to.equal("Fruit"); + await expect(await $(SubmitPage.optionalRadioWithDropdownDetailAnswer()).getText()).toBe("Fruit"); }); - it("When the user selects an option with an mandatory detail answer but does not provide a detail answer, Then an error should be displayed when the user submits", () => { - $(RadioDropdownPage.jam()).click(); - $(RadioDropdownPage.submit()).click(); + it("When the user selects an option with an mandatory detail answer but does not provide a detail answer, Then an error should be displayed when the user submits", async () => { + await $(RadioDropdownPage.jam()).click(); + await click(RadioDropdownPage.submit()); - expect($(DropdownMandatoryPage.errorNumber(1)).getText()).to.equal("Please select the type of Jam"); + await expect(await $(DropdownMandatoryPage.errorNumber(1)).getText()).toBe("Please select the type of Jam"); }); - it("When the user selects an option with an mandatory detail answer and provides a detail answer, Then the summary should display the chosen option and its detail answer", () => { - $(RadioDropdownPage.jam()).click(); - $(RadioDropdownPage.jamDetail()).selectByAttribute("value", "Strawberry"); - $(RadioDropdownPage.submit()).click(); + it("When the user selects an option with an mandatory detail answer and provides a detail answer, Then the summary should display the chosen option and its detail answer", async () => { + await $(RadioDropdownPage.jam()).click(); + await $(RadioDropdownPage.jamDetail()).selectByAttribute("value", "Strawberry"); + await click(RadioDropdownPage.submit()); - expect($(SubmitPage.optionalRadioWithDropdownDetailAnswer()).getText()).to.equal("Jam\nStrawberry"); + await expect(await $(SubmitPage.optionalRadioWithDropdownDetailAnswer()).getText()).toBe("Jam\nStrawberry"); }); - it("When the user removes a previously submitted detail answer by selecting another radio option, Then the summary should only display the new radio option", () => { - $(RadioDropdownPage.jam()).click(); - $(RadioDropdownPage.jamDetail()).selectByAttribute("value", "Raspberry"); - $(RadioDropdownPage.submit()).click(); - $(SubmitPage.previous()).click(); - $(RadioDropdownPage.fruit()).click(); - $(RadioDropdownPage.submit()).click(); + it("When the user removes a previously submitted detail answer by selecting another radio option, Then the summary should only display the new radio option", async () => { + await $(RadioDropdownPage.jam()).click(); + await $(RadioDropdownPage.jamDetail()).selectByAttribute("value", "Raspberry"); + await click(RadioDropdownPage.submit()); + await $(SubmitPage.previous()).click(); + await $(RadioDropdownPage.fruit()).click(); + await click(RadioDropdownPage.submit()); - expect($(SubmitPage.optionalRadioWithDropdownDetailAnswer()).getText()).to.equal("Fruit"); + await expect(await $(SubmitPage.optionalRadioWithDropdownDetailAnswer()).getText()).toBe("Fruit"); }); }); }); diff --git a/tests/functional/spec/components/radio/radio_detail_answer_multiple.spec.js b/tests/functional/spec/components/radio/radio_detail_answer_multiple.spec.js index cc3e888f47..4079fcde7b 100644 --- a/tests/functional/spec/components/radio/radio_detail_answer_multiple.spec.js +++ b/tests/functional/spec/components/radio/radio_detail_answer_multiple.spec.js @@ -1,76 +1,76 @@ import MandatoryRadioPage from "../../../generated_pages/radio_detail_answer_multiple/radio-mandatory.page"; import SubmitPage from "../../../generated_pages/radio_detail_answer_multiple/submit.page"; - +import { click, verifyUrlContains } from "../../../helpers"; describe('Radio with multiple "detail_answer" options', () => { const radioSchema = "test_radio_detail_answer_multiple.json"; - it("Given detail answer options are available, When the user clicks an option, Then the detail answer input should be visible.", () => { - browser.openQuestionnaire(radioSchema); - $(MandatoryRadioPage.eggs()).click(); - expect($(MandatoryRadioPage.eggsDetail()).isDisplayed()).to.be.true; - $(MandatoryRadioPage.favouriteNotListed()).click(); - expect($(MandatoryRadioPage.favouriteNotListedDetail()).isDisplayed()).to.be.true; + it("Given detail answer options are available, When the user clicks an option, Then the detail answer input should be visible.", async () => { + await browser.openQuestionnaire(radioSchema); + await $(MandatoryRadioPage.eggs()).click(); + await expect(await $(MandatoryRadioPage.eggsDetail()).isDisplayed()).toBe(true); + await $(MandatoryRadioPage.favouriteNotListed()).click(); + await expect(await $(MandatoryRadioPage.favouriteNotListedDetail()).isDisplayed()).toBe(true); }); - it("Given a mandatory detail answer, When I select the option but leave the input field empty and submit, Then an error should be displayed.", () => { + it("Given a mandatory detail answer, When I select the option but leave the input field empty and submit, Then an error should be displayed.", async () => { // Given - browser.openQuestionnaire(radioSchema); + await browser.openQuestionnaire(radioSchema); // When - $(MandatoryRadioPage.favouriteNotListed()).click(); - $(MandatoryRadioPage.submit()).click(); + await $(MandatoryRadioPage.favouriteNotListed()).click(); + await click(MandatoryRadioPage.submit()); // Then - expect($(MandatoryRadioPage.error()).isDisplayed()).to.be.true; - expect($(MandatoryRadioPage.errorNumber(1)).getText()).to.contain("Enter your favourite to continue"); + await expect(await $(MandatoryRadioPage.error()).isDisplayed()).toBe(true); + await expect(await $(MandatoryRadioPage.errorNumber(1)).getText()).toBe("Enter your favourite to continue"); }); - it("Given a selected radio answer with an error for a mandatory detail answer, When I enter valid value and submit the page, Then the error is cleared and I navigate to next page.", () => { + it("Given a selected radio answer with an error for a mandatory detail answer, When I enter valid value and submit the page, Then the error is cleared and I navigate to next page.", async () => { // Given - browser.openQuestionnaire(radioSchema); - $(MandatoryRadioPage.favouriteNotListed()).click(); - $(MandatoryRadioPage.submit()).click(); - expect($(MandatoryRadioPage.error()).isDisplayed()).to.be.true; + await browser.openQuestionnaire(radioSchema); + await $(MandatoryRadioPage.favouriteNotListed()).click(); + await click(MandatoryRadioPage.submit()); + await expect(await $(MandatoryRadioPage.error()).isDisplayed()).toBe(true); // When - $(MandatoryRadioPage.favouriteNotListedDetail()).setValue("Bacon"); - $(MandatoryRadioPage.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); + await $(MandatoryRadioPage.favouriteNotListedDetail()).setValue("Bacon"); + await click(MandatoryRadioPage.submit()); + await verifyUrlContains(SubmitPage.pageName); }); - it("Given a non-mandatory detail answer, When the user does not provide any text, Then just the option value should be displayed on the summary screen", () => { + it("Given a non-mandatory detail answer, When the user does not provide any text, Then just the option value should be displayed on the summary screen", async () => { // Given - browser.openQuestionnaire(radioSchema); + await browser.openQuestionnaire(radioSchema); // When - $(MandatoryRadioPage.eggs()).click(); - expect($(MandatoryRadioPage.eggsDetail()).isDisplayed()).to.be.true; - $(MandatoryRadioPage.submit()).click(); + await $(MandatoryRadioPage.eggs()).click(); + await expect(await $(MandatoryRadioPage.eggsDetail()).isDisplayed()).toBe(true); + await click(MandatoryRadioPage.submit()); // Then - expect($(SubmitPage.radioMandatoryAnswer()).getText()).to.equal("Eggs"); + await expect(await $(SubmitPage.radioMandatoryAnswer()).getText()).toBe("Eggs"); }); - it("Given a detail answer, When the user provides text, Then that text should be displayed on the summary screen", () => { + it("Given a detail answer, When the user provides text, Then that text should be displayed on the summary screen", async () => { // Given - browser.openQuestionnaire(radioSchema); + await browser.openQuestionnaire(radioSchema); // When - $(MandatoryRadioPage.eggs()).click(); - $(MandatoryRadioPage.eggsDetail()).setValue("Scrambled"); - $(MandatoryRadioPage.submit()).click(); + await $(MandatoryRadioPage.eggs()).click(); + await $(MandatoryRadioPage.eggsDetail()).setValue("Scrambled"); + await click(MandatoryRadioPage.submit()); // Then - expect($(SubmitPage.radioMandatoryAnswer()).getText()).to.equal("Eggs\nScrambled"); + await expect(await $(SubmitPage.radioMandatoryAnswer()).getText()).toBe("Eggs\nScrambled"); }); - it("Given I have previously added text in a detail answer and saved, When I select a different radio and save, Then the text entered in the detail answer field should be empty.", () => { + it("Given I have previously added text in a detail answer and saved, When I select a different radio and save, Then the text entered in the detail answer field should be empty.", async () => { // Given - browser.openQuestionnaire(radioSchema); + await browser.openQuestionnaire(radioSchema); // When - $(MandatoryRadioPage.favouriteNotListed()).click(); - $(MandatoryRadioPage.favouriteNotListedDetail()).setValue("Bacon"); - $(MandatoryRadioPage.submit()).click(); - $(SubmitPage.previous()).click(); - $(MandatoryRadioPage.eggs()).click(); - $(MandatoryRadioPage.submit()).click(); - $(SubmitPage.previous()).click(); + await $(MandatoryRadioPage.favouriteNotListed()).click(); + await $(MandatoryRadioPage.favouriteNotListedDetail()).setValue("Bacon"); + await click(MandatoryRadioPage.submit()); + await $(SubmitPage.previous()).click(); + await $(MandatoryRadioPage.eggs()).click(); + await click(MandatoryRadioPage.submit()); + await $(SubmitPage.previous()).click(); // Then - $(MandatoryRadioPage.favouriteNotListed()).click(); - expect($(MandatoryRadioPage.favouriteNotListedDetail()).getValue()).to.equal(""); + await $(MandatoryRadioPage.favouriteNotListed()).click(); + await expect(await $(MandatoryRadioPage.favouriteNotListedDetail()).getValue()).toBe(""); }); }); diff --git a/tests/functional/spec/components/radio/radio_detail_answer_numeric.spec.js b/tests/functional/spec/components/radio/radio_detail_answer_numeric.spec.js index c3c00675bd..22afcb6fa2 100644 --- a/tests/functional/spec/components/radio/radio_detail_answer_numeric.spec.js +++ b/tests/functional/spec/components/radio/radio_detail_answer_numeric.spec.js @@ -1,74 +1,74 @@ import RadioNumericDetailPage from "../../../generated_pages/radio_detail_answer_numeric/radio-numeric-detail.page"; import SubmitPage from "../../../generated_pages/radio_detail_answer_numeric/submit.page"; - +import { click } from "../../../helpers"; describe('Radio with a numeric "detail_answer" option', () => { - beforeEach(() => { - browser.openQuestionnaire("test_radio_detail_answer_numeric.json"); - $(RadioNumericDetailPage.other()).click(); + beforeEach(async () => { + await browser.openQuestionnaire("test_radio_detail_answer_numeric.json"); + await $(RadioNumericDetailPage.other()).click(); }); - it("Given a numeric detail answer options are available, When the user clicks an option, Then the detail answer input should be visible.", () => { - expect($(RadioNumericDetailPage.otherDetail()).isDisplayed()).to.be.true; + it("Given a numeric detail answer options are available, When the user clicks an option, Then the detail answer input should be visible.", async () => { + await expect(await $(RadioNumericDetailPage.otherDetail()).isDisplayed()).toBe(true); }); - it("Given a numeric detail answer, When the user does not provide any text, Then just the option value should be displayed on the summary screen", () => { + it("Given a numeric detail answer, When the user does not provide any text, Then just the option value should be displayed on the summary screen", async () => { // When - expect($(RadioNumericDetailPage.otherDetail()).isDisplayed()).to.be.true; - $(RadioNumericDetailPage.submit()).click(); + await expect(await $(RadioNumericDetailPage.otherDetail()).isDisplayed()).toBe(true); + await click(RadioNumericDetailPage.submit()); // Then - expect($(SubmitPage.radioAnswerNumericDetail()).getText()).to.contain("Other"); + await expect(await $(SubmitPage.radioAnswerNumericDetail()).getText()).toBe("Other"); }); - it("Given a numeric detail answer, When the user provides text, Then that text should be displayed on the summary screen", () => { + it("Given a numeric detail answer, When the user provides text, Then that text should be displayed on the summary screen", async () => { // When - $(RadioNumericDetailPage.otherDetail()).setValue("15"); - $(RadioNumericDetailPage.submit()).click(); + await $(RadioNumericDetailPage.otherDetail()).setValue("15"); + await click(RadioNumericDetailPage.submit()); // Then - expect($(SubmitPage.radioAnswerNumericDetail()).getText()).to.contain("15"); + await expect(await $(SubmitPage.radioAnswerNumericDetail()).getText()).toContain("15"); }); - it("Given a numeric detail answer, When the user provides text, An error should be displayed", () => { + it("Given a numeric detail answer, When the user provides text, An error should be displayed", async () => { // When - $(RadioNumericDetailPage.otherDetail()).setValue("fhdjkshfjkds"); - $(RadioNumericDetailPage.submit()).click(); + await $(RadioNumericDetailPage.otherDetail()).setValue("fhdjkshfjkds"); + await click(RadioNumericDetailPage.submit()); // Then - expect($(RadioNumericDetailPage.error()).isDisplayed()).to.be.true; - expect($(RadioNumericDetailPage.errorNumber(1)).getText()).to.contain("Please enter an integer"); + await expect(await $(RadioNumericDetailPage.error()).isDisplayed()).toBe(true); + await expect(await $(RadioNumericDetailPage.errorNumber(1)).getText()).toBe("Please enter an integer"); }); - it("Given a numeric detail answer, When the user provides a number larger than 20, An error should be displayed", () => { + it("Given a numeric detail answer, When the user provides a number larger than 20, An error should be displayed", async () => { // When - $(RadioNumericDetailPage.otherDetail()).setValue("250"); - $(RadioNumericDetailPage.submit()).click(); + await $(RadioNumericDetailPage.otherDetail()).setValue("250"); + await click(RadioNumericDetailPage.submit()); // Then - expect($(RadioNumericDetailPage.error()).isDisplayed()).to.be.true; - expect($(RadioNumericDetailPage.errorNumber(1)).getText()).to.contain("Number is too large"); + await expect(await $(RadioNumericDetailPage.error()).isDisplayed()).toBe(true); + await expect(await $(RadioNumericDetailPage.errorNumber(1)).getText()).toBe("Number is too large"); }); - it("Given a numeric detail answer, When the user provides a number less than 0, An error should be displayed", () => { + it("Given a numeric detail answer, When the user provides a number less than 0, An error should be displayed", async () => { // When - $(RadioNumericDetailPage.otherDetail()).setValue("-1"); - $(RadioNumericDetailPage.submit()).click(); + await $(RadioNumericDetailPage.otherDetail()).setValue("-1"); + await click(RadioNumericDetailPage.submit()); // Then - expect($(RadioNumericDetailPage.error()).isDisplayed()).to.be.true; - expect($(RadioNumericDetailPage.errorNumber(1)).getText()).to.contain("Number cannot be less than zero"); + await expect(await $(RadioNumericDetailPage.error()).isDisplayed()).toBe(true); + await expect(await $(RadioNumericDetailPage.errorNumber(1)).getText()).toBe("Number cannot be less than zero"); }); - it("Given a numeric detail answer, When the user provides text, An error should be displayed and the text in the textbox should be kept", () => { + it("Given a numeric detail answer, When the user provides text, An error should be displayed and the text in the textbox should be kept", async () => { // When - $(RadioNumericDetailPage.otherDetail()).setValue("biscuits"); - $(RadioNumericDetailPage.submit()).click(); + await $(RadioNumericDetailPage.otherDetail()).setValue("biscuits"); + await click(RadioNumericDetailPage.submit()); // Then - expect($(RadioNumericDetailPage.error()).isDisplayed()).to.be.true; - expect($(RadioNumericDetailPage.errorNumber(1)).getText()).to.contain("Please enter an integer"); - expect($(RadioNumericDetailPage.otherDetail()).getValue()).to.contain("biscuits"); + await expect(await $(RadioNumericDetailPage.error()).isDisplayed()).toBe(true); + await expect(await $(RadioNumericDetailPage.errorNumber(1)).getText()).toBe("Please enter an integer"); + await expect(await $(RadioNumericDetailPage.otherDetail()).getValue()).toBe("biscuits"); }); - it('Given a numeric detail answer, When the user enters "0" and submits, Then "0" should be displayed on the summary screen', () => { + it('Given a numeric detail answer, When the user enters "0" and submits, Then "0" should be displayed on the summary screen', async () => { // When - $(RadioNumericDetailPage.otherDetail()).setValue("0"); - $(RadioNumericDetailPage.submit()).click(); + await $(RadioNumericDetailPage.otherDetail()).setValue("0"); + await click(RadioNumericDetailPage.submit()); // Then - expect($(SubmitPage.radioAnswerNumericDetail()).getText()).to.contain("0"); + await expect(await $(SubmitPage.radioAnswerNumericDetail()).getText()).toContain("0"); }); }); diff --git a/tests/functional/spec/components/radio/radio_visible_answers.spec.js b/tests/functional/spec/components/radio/radio_visible_answers.spec.js index 7f477b0a65..9f5d8271c8 100644 --- a/tests/functional/spec/components/radio/radio_visible_answers.spec.js +++ b/tests/functional/spec/components/radio/radio_visible_answers.spec.js @@ -1,32 +1,32 @@ import RadioVisibleTruePage from "../../../generated_pages/radio_detail_answer_visible/radio-visible-true.page.js"; import RadioVisibleFalsePage from "../../../generated_pages/radio_detail_answer_visible/radio-visible-false.page.js"; import RadioVisibleNonePage from "../../../generated_pages/radio_detail_answer_visible/radio-visible-none.page.js"; - +import { click } from "../../../helpers"; describe("Given I start a Radio survey with a write-in option", () => { - beforeEach(() => { - browser.openQuestionnaire("test_radio_detail_answer_visible.json"); + beforeEach(async () => { + await browser.openQuestionnaire("test_radio_detail_answer_visible.json"); }); - it("When I view a write-in radio and the visible option is set to true, Then the detail answer label should be displayed", () => { - expect($(RadioVisibleTruePage.otherDetail()).isDisplayed()).to.equal(true); + it("When I view a write-in radio and the visible option is set to true, Then the detail answer label should be displayed", async () => { + await expect(await $(RadioVisibleTruePage.otherDetail()).isDisplayed()).toBe(true); }); - it("When I view a write-in radio and the visible option is set to true, Then after choosing non write-in option the detail answer label should be displayed", () => { - $(RadioVisibleTruePage.coffee()).click(); - expect($(RadioVisibleTruePage.otherDetail()).isDisplayed()).to.equal(true); + it("When I view a write-in radio and the visible option is set to true, Then after choosing non write-in option the detail answer label should be displayed", async () => { + await $(RadioVisibleTruePage.coffee()).click(); + await expect(await $(RadioVisibleTruePage.otherDetail()).isDisplayed()).toBe(true); }); - it("When I view a write-in radio and the visible option is set to false, Then the detail answer label should not be displayed", () => { - $(RadioVisibleTruePage.coffee()).click(); - $(RadioVisibleTruePage.submit()).click(); - expect($(RadioVisibleFalsePage.otherDetail()).isDisplayed()).to.equal(false); + it("When I view a write-in radio and the visible option is set to false, Then the detail answer label should not be displayed", async () => { + await $(RadioVisibleTruePage.coffee()).click(); + await click(RadioVisibleTruePage.submit()); + await expect(await $(RadioVisibleFalsePage.otherDetail()).isDisplayed()).toBe(false); }); - it("When I view a write-in radio and the visible option is not set, Then the detail answer label should not be displayed", () => { - $(RadioVisibleTruePage.coffee()).click(); - $(RadioVisibleFalsePage.submit()).click(); - $(RadioVisibleFalsePage.iceCream()).click(); - $(RadioVisibleFalsePage.submit()).click(); - expect($(RadioVisibleNonePage.otherDetail()).isDisplayed()).to.equal(false); + it("When I view a write-in radio and the visible option is not set, Then the detail answer label should not be displayed", async () => { + await $(RadioVisibleTruePage.coffee()).click(); + await click(RadioVisibleFalsePage.submit()); + await $(RadioVisibleFalsePage.iceCream()).click(); + await click(RadioVisibleFalsePage.submit()); + await expect(await $(RadioVisibleNonePage.otherDetail()).isDisplayed()).toBe(false); }); }); diff --git a/tests/functional/spec/components/radio/radio_voluntary_answers.spec.js b/tests/functional/spec/components/radio/radio_voluntary_answers.spec.js index 180cf105ae..0fe2c8b8bd 100644 --- a/tests/functional/spec/components/radio/radio_voluntary_answers.spec.js +++ b/tests/functional/spec/components/radio/radio_voluntary_answers.spec.js @@ -1,39 +1,39 @@ import RadioVoluntaryTruePage from "../../../generated_pages/radio_voluntary/radio-voluntary-true.page.js"; import RadioVoluntaryFalsePage from "../../../generated_pages/radio_voluntary/radio-voluntary-false.page.js"; - +import { click } from "../../../helpers"; describe("Component: Radio", () => { describe("Given I start a Voluntary Radio survey", () => { - before(() => { - browser.openQuestionnaire("test_radio_voluntary.json"); + before(async () => { + await browser.openQuestionnaire("test_radio_voluntary.json"); }); - it("When I select a voluntary radio option, Then the clear button should be displayed", () => { - $(RadioVoluntaryTruePage.coffee()).click(); - expect($(RadioVoluntaryTruePage.clearSelectionButton()).isDisplayed()).to.equal(true); + it("When I select a voluntary radio option, Then the clear button should be displayed", async () => { + await $(RadioVoluntaryTruePage.coffee()).click(); + await expect(await $(RadioVoluntaryTruePage.clearSelectionButton()).isDisplayed()).toBe(true); }); - it("When I select a voluntary radio option and click the clear button, Then the radio option should not be selected and the clear button should not be displayed", () => { - $(RadioVoluntaryTruePage.coffee()).click(); - $(RadioVoluntaryTruePage.clearSelectionButton()).click(); - expect($(RadioVoluntaryTruePage.coffee()).isSelected()).to.equal(false); - expect($(RadioVoluntaryTruePage.clearSelectionButton()).isDisplayed()).to.equal(false); + it("When I select a voluntary radio option and click the clear button, Then the radio option should not be selected and the clear button should not be displayed", async () => { + await $(RadioVoluntaryTruePage.coffee()).click(); + await $(RadioVoluntaryTruePage.clearSelectionButton()).click(); + await expect(await $(RadioVoluntaryTruePage.coffee()).isSelected()).toBe(false); + await expect(await $(RadioVoluntaryTruePage.clearSelectionButton()).isDisplayed()).toBe(false); }); - it("When I clear a previously saved voluntary radio option and submit, Then when returning to the page the radio option is no longer selected", () => { - $(RadioVoluntaryTruePage.coffee()).click(); - $(RadioVoluntaryTruePage.submit()).click(); - $(RadioVoluntaryTruePage.previous()).click(); - $(RadioVoluntaryTruePage.clearSelectionButton()).click(); - $(RadioVoluntaryTruePage.submit()).click(); - $(RadioVoluntaryTruePage.previous()).click(); - expect($(RadioVoluntaryTruePage.coffee()).isSelected()).to.equal(false); - expect($(RadioVoluntaryTruePage.clearSelectionButton()).isDisplayed()).to.equal(false); + it("When I clear a previously saved voluntary radio option and submit, Then when returning to the page the radio option is no longer selected", async () => { + await $(RadioVoluntaryTruePage.coffee()).click(); + await click(RadioVoluntaryTruePage.submit()); + await $(RadioVoluntaryTruePage.previous()).click(); + await $(RadioVoluntaryTruePage.clearSelectionButton()).click(); + await click(RadioVoluntaryTruePage.submit()); + await $(RadioVoluntaryTruePage.previous()).click(); + await expect(await $(RadioVoluntaryTruePage.coffee()).isSelected()).toBe(false); + await expect(await $(RadioVoluntaryTruePage.clearSelectionButton()).isDisplayed()).toBe(false); }); - it("When I select a non-voluntary radio option, Then the clear button should not be displayed on the page", () => { - $(RadioVoluntaryTruePage.submit()).click(); - $(RadioVoluntaryFalsePage.iceCream()).click(); - expect($(RadioVoluntaryFalsePage.clearSelectionButton()).isDisplayed()).to.equal(false); + it("When I select a non-voluntary radio option, Then the clear button should not be displayed on the page", async () => { + await click(RadioVoluntaryTruePage.submit()); + await $(RadioVoluntaryFalsePage.iceCream()).click(); + await expect(await $(RadioVoluntaryFalsePage.clearSelectionButton()).isDisplayed()).toBe(false); }); }); }); diff --git a/tests/functional/spec/conditional_combined_routing.spec.js b/tests/functional/spec/conditional_combined_routing.spec.js deleted file mode 100644 index 6e2ce6fa65..0000000000 --- a/tests/functional/spec/conditional_combined_routing.spec.js +++ /dev/null @@ -1,44 +0,0 @@ -import ConditionalCombinedRoutingPage from "../generated_pages/conditional_combined_routing/conditional-routing-block.page"; -import ResponseAny from "../generated_pages/conditional_combined_routing/response-any.page"; -import ResponseNotAny from "../generated_pages/conditional_combined_routing/response-not-any.page"; -import SubmitPage from "../generated_pages/conditional_combined_routing/submit.page"; - -describe("Conditional combined routing.", () => { - beforeEach(() => { - browser.openQuestionnaire("test_conditional_combined_routing.json"); - }); - - it('Given a list of radio options, when I choose the option "Yes" or the option "Sometimes" then I should be routed to the relevant page', () => { - // When - $(ConditionalCombinedRoutingPage.yes()).click(); - $(ConditionalCombinedRoutingPage.submit()).click(); - // Then - expect(browser.getUrl()).to.contain(ResponseAny.pageName); - - // Or - $(ResponseAny.previous()).click(); - - // When - $(ConditionalCombinedRoutingPage.sometimes()).click(); - $(ConditionalCombinedRoutingPage.submit()).click(); - - // Then - expect(browser.getUrl()).to.contain(ResponseAny.pageName); - }); - - it('Given a list of radio options, when I choose the option "No, I prefer tea" then I should be routed to the relevant page', () => { - // When - $(ConditionalCombinedRoutingPage.noIPreferTea()).click(); - $(ConditionalCombinedRoutingPage.submit()).click(); - // Then - expect(browser.getUrl()).to.contain(ResponseNotAny.pageName); - }); - - it('Given a list of radio options, when I choose the option "No, I don\'t drink any hot drinks" then I should be routed to the submit page', () => { - // When - $(ConditionalCombinedRoutingPage.noIDonTDrinkAnyHotDrinks()).click(); - $(ConditionalCombinedRoutingPage.submit()).click(); - // Then - expect(browser.getUrl()).to.contain(SubmitPage.pageName); - }); -}); diff --git a/tests/functional/spec/confirmation_email.spec.js b/tests/functional/spec/confirmation_email.spec.js index a3a693232d..d46b767db3 100644 --- a/tests/functional/spec/confirmation_email.spec.js +++ b/tests/functional/spec/confirmation_email.spec.js @@ -4,109 +4,112 @@ import ThankYouPage from "../base_pages/thank-you.page"; import ConfirmationEmailPage from "../base_pages/confirmation-email.page"; import ConfirmationEmailSentPage from "../base_pages/confirmation-email-sent.page"; import ConfirmEmailPage from "../base_pages/confirm-email.page"; +import { click, verifyUrlContains } from "../helpers"; + +const errorPanel = '[data-ga="error"]'; describe("Email confirmation", () => { describe("Given I launch the test email confirmation survey", () => { - before(() => { - browser.openQuestionnaire("test_confirmation_email.json"); + before(async () => { + await browser.openQuestionnaire("test_confirmation_email.json"); }); - it("When I complete the survey and am on the thank you page, Then there is option to enter an email address", () => { - $(SubmitPage.submit()).click(); - $(SubmitPage.submit()).click(); - expect(browser.getUrl()).to.contain(ThankYouPage.pageName); - expect($(ThankYouPage.email()).isExisting()).to.be.true; + it("When I complete the survey and am on the thank you page, Then there is option to enter an email address", async () => { + await click(SubmitPage.submit()); + await click(SubmitPage.submit()); + await verifyUrlContains(ThankYouPage.pageName); + await expect(await $(ThankYouPage.email()).isExisting()).toBe(true); }); - it("When I submit the form without providing an email address, Then I get an error message", () => { - $(ThankYouPage.submit()).click(); - expect(browser.getUrl()).to.contain(ThankYouPage.pageName); - expect($(ThankYouPage.errorPanel()).isExisting()).to.be.true; - expect($(ThankYouPage.errorPanel()).getText()).to.contain("Enter an email address"); + it("When I submit the form without providing an email address, Then I get an error message", async () => { + await click(ThankYouPage.submit()); + await verifyUrlContains(ThankYouPage.pageName); + await expect(await $(errorPanel).isExisting()).toBe(true); + await expect(await $(errorPanel).getText()).toBe("Enter an email address"); }); - it("When I submit the form without providing a correctly formatted email address, Then I get an error message", () => { - $(ThankYouPage.email()).setValue("incorrect-format"); - $(ThankYouPage.submit()).click(); - expect(browser.getUrl()).to.contain(ThankYouPage.pageName); - expect($(ThankYouPage.errorPanel()).isExisting()).to.be.true; - expect($(ThankYouPage.errorPanel()).getText()).to.contain("Enter an email address in a valid format, for example name@example.com"); + it("When I submit the form without providing a correctly formatted email address, Then I get an error message", async () => { + await $(ThankYouPage.email()).setValue("incorrect-format"); + await click(ThankYouPage.submit()); + await verifyUrlContains(ThankYouPage.pageName); + await expect(await $(errorPanel).isExisting()).toBe(true); + await expect(await $(errorPanel).getText()).toBe("Enter an email address in a valid format, for example name@example.com"); }); - it("When I submit the form with a valid email address, Then I go to the confirm email page", () => { - $(ThankYouPage.email()).setValue("name@example.com"); - $(ThankYouPage.submit()).click(); - expect(browser.getUrl()).to.contain("confirmation-email/confirm"); - expect($(ConfirmEmailPage.questionTitle()).getText()).to.equal("Is this email address correct?"); + it("When I submit the form with a valid email address, Then I go to the confirm email page", async () => { + await $(ThankYouPage.email()).setValue("name@example.com"); + await click(ThankYouPage.submit()); + await verifyUrlContains("confirmation-email/confirm"); + await expect(await $(ConfirmEmailPage.questionTitle()).getText()).toBe("Is this email address correct?"); }); - it("When I submit the confirm email page without providing an answer, Then I get an error message", () => { - $(ConfirmEmailPage.submit()).click(); - expect(browser.getUrl()).to.contain("confirmation-email/confirm"); - expect($(ConfirmEmailPage.errorPanel()).isExisting()).to.be.true; - expect($(ConfirmEmailPage.errorPanel()).getText()).to.contain("Select an answer"); + it("When I submit the confirm email page without providing an answer, Then I get an error message", async () => { + await click(ConfirmEmailPage.submit()); + await verifyUrlContains("confirmation-email/confirm"); + await expect(await $(ConfirmEmailPage.errorPanel()).isExisting()).toBe(true); + await expect(await $(ConfirmEmailPage.errorPanel()).getText()).toContain("Select an answer"); }); - it("When I answer 'Yes' and submit the confirm email page, Then I go to email sent page", () => { - $(ConfirmEmailPage.yes()).click(); - $(ConfirmEmailPage.submit()).click(); - expect(browser.getUrl()).to.contain("confirmation-email/sent"); - expect($(ConfirmationEmailSentPage.confirmationText()).getText()).to.equal("A confirmation email has been sent to name@example.com"); + it("When I answer 'Yes' and submit the confirm email page, Then I go to email sent page", async () => { + await $(ConfirmEmailPage.yes()).click(); + await click(ConfirmEmailPage.submit()); + await verifyUrlContains("confirmation-email/sent"); + await expect(await $(ConfirmationEmailSentPage.confirmationText()).getText()).toBe("A confirmation email has been sent to name@example.com"); }); - it("when I go to the confirmation email page and submit without providing an email address, Then I get an error message", () => { - $(ConfirmationEmailSentPage.sendAnotherEmail()).click(); - $(ConfirmationEmailPage.submit()).click(); - expect(browser.getUrl()).to.contain("confirmation-email/send"); - expect($(ConfirmationEmailPage.errorPanel()).isExisting()).to.be.true; - expect($(ConfirmationEmailPage.errorPanel()).getText()).to.equal("Enter an email address"); + it("When I go to the confirmation email page and submit without providing an email address, Then I get an error message", async () => { + await $(ConfirmationEmailSentPage.sendAnotherEmail()).click(); + await click(ConfirmationEmailPage.submit()); + await verifyUrlContains("confirmation-email/send"); + await expect(await $(ConfirmationEmailPage.errorPanel()).isExisting()).toBe(true); + await expect(await $(ConfirmationEmailPage.errorPanel()).getText()).toBe("Enter an email address"); }); - it("when I submit the form without providing a correctly formatted email address, Then I get an error message", () => { - $(ConfirmationEmailPage.email()).setValue("incorrect-format"); - $(ConfirmationEmailPage.submit()).click(); - expect(browser.getUrl()).to.contain("confirmation-email/send"); - expect($(ConfirmationEmailPage.errorPanel()).isExisting()).to.be.true; - expect($(ConfirmationEmailPage.errorPanel()).getText()).to.equal("Enter an email address in a valid format, for example name@example.com"); + it("When I submit the form without providing a correctly formatted email address, Then I get an error message", async () => { + await $(ConfirmationEmailPage.email()).setValue("incorrect-format"); + await click(ConfirmationEmailPage.submit()); + await verifyUrlContains("confirmation-email/send"); + await expect(await $(ConfirmationEmailPage.errorPanel()).isExisting()).toBe(true); + await expect(await $(ConfirmationEmailPage.errorPanel()).getText()).toBe("Enter an email address in a valid format, for example name@example.com"); }); - it("When I submit the form with a valid email and confirm it is correct, Then I go to the email confirmation page", () => { - $(ConfirmationEmailPage.email()).setValue("name@example.com"); - $(ConfirmationEmailPage.submit()).click(); - $(ConfirmEmailPage.yes()).click(); - $(ConfirmEmailPage.submit()).click(); - expect(browser.getUrl()).to.contain("confirmation-email/sent"); - expect($(ConfirmationEmailSentPage.confirmationText()).getText()).to.equal("A confirmation email has been sent to name@example.com"); + it("When I submit the form with a valid email and confirm it is correct, Then I go to the email confirmation page", async () => { + await $(ConfirmationEmailPage.email()).setValue("name@example.com"); + await click(ConfirmationEmailPage.submit()); + await $(ConfirmEmailPage.yes()).click(); + await click(ConfirmEmailPage.submit()); + await verifyUrlContains("confirmation-email/sent"); + await expect(await $(ConfirmationEmailSentPage.confirmationText()).getText()).toBe("A confirmation email has been sent to name@example.com"); }); }); describe("Given I launch the test email confirmation survey", () => { - before(() => { - browser.openQuestionnaire("test_confirmation_email.json"); + before(async () => { + await browser.openQuestionnaire("test_confirmation_email.json"); }); - it("When I enter an email and answer 'No' on the confirm email page, Then I go the confirmation send page with the email pre-filled", () => { - $(SubmitPage.submit()).click(); - $(SubmitPage.submit()).click(); - $(ThankYouPage.email()).setValue("name@example.com"); - $(ThankYouPage.submit()).click(); - $(ConfirmEmailPage.no()).click(); - $(ConfirmEmailPage.submit()).click(); - expect(browser.getUrl()).to.contain("confirmation-email/send"); - expect($(ConfirmationEmailPage.email()).getValue()).to.equal("name@example.com"); + it("When I enter an email and answer 'No' on the confirm email page, Then I go the confirmation send page with the email pre-filled", async () => { + await click(SubmitPage.submit()); + await click(SubmitPage.submit()); + await $(ThankYouPage.email()).setValue("name@example.com"); + await click(ThankYouPage.submit()); + await $(ConfirmEmailPage.no()).click(); + await click(ConfirmEmailPage.submit()); + await verifyUrlContains("confirmation-email/send"); + await expect(await $(ConfirmationEmailPage.email()).getValue()).toBe("name@example.com"); }); }); }); describe("Email confirmation", () => { describe("Given I launch the test email confirmation survey", () => { - before(() => { - browser.openQuestionnaire("test_confirmation_email.json"); + before(async () => { + await browser.openQuestionnaire("test_confirmation_email.json"); }); - it("When I view the email confirmation page, Then I should not see the feedback call to action", () => { - $(SubmitPage.submit()).click(); - $(SubmitPage.submit()).click(); - $(ThankYouPage.email()).setValue("name@example.com"); - $(ThankYouPage.submit()).click(); - expect($(ConfirmationEmailSentPage.feedbackLink()).isExisting()).to.equal(false); + it("When I view the email confirmation page, Then I should not see the feedback call to action", async () => { + await click(SubmitPage.submit()); + await click(SubmitPage.submit()); + await $(ThankYouPage.email()).setValue("name@example.com"); + await click(ThankYouPage.submit()); + await expect(await $(ConfirmationEmailSentPage.feedbackLink()).isExisting()).toBe(false); }); }); }); diff --git a/tests/functional/spec/content_variants.spec.js b/tests/functional/spec/content_variants.spec.js index 073e367d5e..28fe119254 100644 --- a/tests/functional/spec/content_variants.spec.js +++ b/tests/functional/spec/content_variants.spec.js @@ -1,19 +1,20 @@ import ageQuestionBlock from "../generated_pages/variants_content/age-question-block.page.js"; +import { click } from "../helpers"; describe("QuestionVariants", () => { - beforeEach(() => { - browser.openQuestionnaire("test_new_variants_content.json"); + beforeEach(async () => { + await browser.openQuestionnaire("test_variants_content.json"); }); - it("Given I am completing the survey, then the correct content is shown based on my previous answers when i am under 16", () => { - $(ageQuestionBlock.age()).setValue(12); - $(ageQuestionBlock.submit()).click(); - expect($("main.ons-page__main h1").getText()).to.contain("You are 16 or younger"); + it("Given I am completing the survey, then the correct content is shown based on my previous answers when i am under 16", async () => { + await $(ageQuestionBlock.age()).setValue(12); + await click(ageQuestionBlock.submit()); + await expect(await $("main.ons-page__main h1").getText()).toBe("You are 16 or younger"); }); - it("Given I am completing the survey, then the correct content is shown based on my previous answers when i am under 16", () => { - $(ageQuestionBlock.age()).setValue(22); - $(ageQuestionBlock.submit()).click(); - expect($("main.ons-page__main h1").getText()).to.contain("You are 16 or older"); + it("Given I am completing the survey, then the correct content is shown based on my previous answers when i am under 16", async () => { + await $(ageQuestionBlock.age()).setValue(22); + await click(ageQuestionBlock.submit()); + await expect(await $("main.ons-page__main h1").getText()).toBe("You are 16 or older"); }); }); diff --git a/tests/functional/spec/cookie_banner.spec.js b/tests/functional/spec/cookie_banner.spec.js new file mode 100644 index 0000000000..0cbe1fd9ec --- /dev/null +++ b/tests/functional/spec/cookie_banner.spec.js @@ -0,0 +1,59 @@ +import InitialPage from "../generated_pages/checkbox/mandatory-checkbox.page"; +import HubPage from "../base_pages/hub.page.js"; + +describe("Given I am not authenticated and have no cookie,", () => { + it("When I visit a page in runner, Then the cookie banner shouldn‘t be displayed", async () => { + await browser.url("/"); + await expect(await $(InitialPage.acceptCookies()).isDisplayed()).toBe(false); + }); +}); + +describe("Given I start a survey,", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_checkbox.json"); + }); + it("When I open the page, Then the cookie banner should be displayed", async () => { + await expect(await $(InitialPage.acceptCookies()).isDisplayed()).toBe(true); + }); + it("When I delete all cookies from the browser and refresh the page, Then the cookie banner shouldn‘t be displayed", async () => { + await browser.deleteAllCookies(); + await browser.refresh(); + await expect(await $(InitialPage.acceptCookies()).isDisplayed()).toBe(false); + }); + it("When I sign out and click the browser back button, Then the cookie banner should be displayed", async () => { + await $(InitialPage.saveSignOut()).click(); + await browser.back(); + await expect(await $(InitialPage.acceptCookies()).isDisplayed()).toBe(true); + }); + it("When I accept the cookies and refresh the page, Then the cookie banner shouldn‘t be displayed", async () => { + await $(InitialPage.acceptCookies()).click(); + await browser.refresh(); + await expect(await $(InitialPage.acceptCookies()).isDisplayed()).toBe(false); + }); +}); + +describe("Given I start a survey with multiple languages,", () => { + beforeEach(async () => { + await browser.deleteAllCookies(); + }); + it("When I open the page in english, Then the cookie banner should be displayed in english", async () => { + await browser.openQuestionnaire("test_language.json", { + language: "en", + }); + await expect(await $(HubPage.acceptCookies()).getText()).toBe("Accept additional cookies"); + }); + it("When I open the page in welsh, Then the cookie banner should be displayed in welsh", async () => { + await browser.openQuestionnaire("test_language.json", { + language: "cy", + }); + await expect(await $(HubPage.acceptCookies()).getText()).toBe("Derbyn cwcis ychwanegol"); + }); + it("When I open the page in english, Then change the language to welsh the cookie banner should be displayed in welsh", async () => { + await browser.openQuestionnaire("test_language.json", { + language: "en", + }); + await expect(await $(HubPage.acceptCookies()).getText()).toBe("Accept additional cookies"); + await $(HubPage.switchLanguage("cy")).click(); + await expect(await $(HubPage.acceptCookies()).getText()).toBe("Derbyn cwcis ychwanegol"); + }); +}); diff --git a/tests/functional/spec/custom_page_titles.spec.js b/tests/functional/spec/custom_page_titles.spec.js index 764a9d3e15..dc04f6fc2c 100644 --- a/tests/functional/spec/custom_page_titles.spec.js +++ b/tests/functional/spec/custom_page_titles.spec.js @@ -6,86 +6,87 @@ import ListCollectorEditPage from "../generated_pages/custom_page_titles/list-co import ListCollectorPage from "../generated_pages/custom_page_titles/list-collector.page.js"; import ProxyPage from "../generated_pages/custom_page_titles/proxy.page.js"; import RelationshipsPage from "../generated_pages/custom_page_titles/relationships.page.js"; +import { click } from "../helpers"; describe("Feature: Custom Page Titles", () => { const schema = "test_custom_page_titles.json"; describe("Given I am completing the test_custom_page_titles survey,", () => { - before("load the survey", () => { - browser.openQuestionnaire(schema); + before("load the survey", async () => { + await browser.openQuestionnaire(schema); }); - it("When I navigate to the list collector page, Then I should see the custom page title", () => { - $(HubPage.submit()).click(); - const expectedPageTitle = browser.getTitle(); - expect(expectedPageTitle).to.equal("Custom page title - Test Custom Page Titles"); + it("When I navigate to the list collector page, Then I should see the custom page title", async () => { + await click(HubPage.submit()); + const expectedPageTitle = await browser.getTitle(); + await expect(expectedPageTitle).toBe("Custom page title - Test Custom Page Titles"); }); - it("When I navigate to the add person page, Then I should see the custom page title", () => { - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - let expectedPageTitle = browser.getTitle(); - expect(expectedPageTitle).to.equal("Add person 1 - Test Custom Page Titles"); + it("When I navigate to the add person page, Then I should see the custom page title", async () => { + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + let expectedPageTitle = await browser.getTitle(); + await expect(expectedPageTitle).toBe("Add person 1 - Test Custom Page Titles"); - $(ListCollectorAddPage.firstName()).setValue("Marcus"); - $(ListCollectorAddPage.lastName()).setValue("Twin"); - $(ListCollectorAddPage.submit()).click(); - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - expectedPageTitle = browser.getTitle(); - expect(expectedPageTitle).to.equal("Add person 2 - Test Custom Page Titles"); + await $(ListCollectorAddPage.firstName()).setValue("Marcus"); + await $(ListCollectorAddPage.lastName()).setValue("Twin"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + expectedPageTitle = await browser.getTitle(); + await expect(expectedPageTitle).toBe("Add person 2 - Test Custom Page Titles"); }); - it("When I navigate to relationship collector pages, Then I should see the custom page titles", () => { - $(ListCollectorAddPage.firstName()).setValue("Samuel"); - $(ListCollectorAddPage.lastName()).setValue("Clemens"); - $(ListCollectorAddPage.submit()).click(); - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("Olivia"); - $(ListCollectorAddPage.lastName()).setValue("Clemens"); - $(ListCollectorAddPage.submit()).click(); - $(ListCollectorPage.no()).click(); - $(ListCollectorPage.submit()).click(); - let expectedPageTitle = browser.getTitle(); - expect(expectedPageTitle).to.equal("How Person 1 is related to Person 2 - Test Custom Page Titles"); + it("When I navigate to relationship collector pages, Then I should see the custom page titles", async () => { + await $(ListCollectorAddPage.firstName()).setValue("Samuel"); + await $(ListCollectorAddPage.lastName()).setValue("Clemens"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Olivia"); + await $(ListCollectorAddPage.lastName()).setValue("Clemens"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + let expectedPageTitle = await browser.getTitle(); + await expect(await expectedPageTitle).toBe("How Person 1 is related to Person 2 - Test Custom Page Titles"); - $(RelationshipsPage.husbandOrWife()).click(); - $(RelationshipsPage.submit()).click(); - expectedPageTitle = browser.getTitle(); - expect(expectedPageTitle).to.equal("How Person 1 is related to Person 3 - Test Custom Page Titles"); + await $(RelationshipsPage.husbandOrWife()).click(); + await click(RelationshipsPage.submit()); + expectedPageTitle = await browser.getTitle(); + await expect(expectedPageTitle).toBe("How Person 1 is related to Person 3 - Test Custom Page Titles"); - $(RelationshipsPage.sonOrDaughter()).click(); - $(RelationshipsPage.submit()).click(); - expectedPageTitle = browser.getTitle(); - expect(expectedPageTitle).to.equal("How Person 2 is related to Person 3 - Test Custom Page Titles"); + await $(RelationshipsPage.sonOrDaughter()).click(); + await click(RelationshipsPage.submit()); + expectedPageTitle = await browser.getTitle(); + await expect(expectedPageTitle).toBe("How Person 2 is related to Person 3 - Test Custom Page Titles"); - $(RelationshipsPage.sonOrDaughter()).click(); - $(RelationshipsPage.submit()).click(); - expectedPageTitle = browser.getTitle(); - expect(expectedPageTitle).to.equal("Custom section summary page title - Test Custom Page Titles"); + await $(RelationshipsPage.sonOrDaughter()).click(); + await click(RelationshipsPage.submit()); + expectedPageTitle = await browser.getTitle(); + await expect(expectedPageTitle).toBe("Custom section summary page title - Test Custom Page Titles"); }); - it("When I navigate to list edit and remove pages Then I should see the custom page titles", () => { - $(ListCollectorPage.listEditLink(1)).click(); - let expectedPageTitle = browser.getTitle(); - expect(expectedPageTitle).to.equal("Edit person 1 - Test Custom Page Titles"); - $(ListCollectorEditPage.previous()).click(); - $(ListCollectorPage.listRemoveLink(1)).click(); - expectedPageTitle = browser.getTitle(); - expect(expectedPageTitle).to.equal("Remove person 1 - Test Custom Page Titles"); + it("When I navigate to list edit and remove pages Then I should see the custom page titles", async () => { + await $(ListCollectorPage.listEditLink(1)).click(); + let expectedPageTitle = await browser.getTitle(); + await expect(expectedPageTitle).toBe("Edit person 1 - Test Custom Page Titles"); + await $(ListCollectorEditPage.previous()).click(); + await $(ListCollectorPage.listRemoveLink(1)).click(); + expectedPageTitle = await browser.getTitle(); + await expect(expectedPageTitle).toBe("Remove person 1 - Test Custom Page Titles"); }); - it("When I navigate to a repeating section which has custom page title, Then all page titles in the section should have the correct prefix", () => { - browser.url(HubPage.url()); - $(HubPage.submit()).click(); - expect(browser.getTitle()).to.equal("Individual interstitial: Person 1 - Test Custom Page Titles"); - $(IndividualInterstitialPage.submit()).click(); - expect(browser.getTitle()).to.equal("Proxy question: Person 1 - Test Custom Page Titles"); - $(ProxyPage.submit()).click(); - expect(browser.getTitle()).to.equal("What is your date of birth?: Person 1 - Test Custom Page Titles"); - $(DateOfBirthPage.submit()).click(); - expect(browser.getTitle()).to.equal("Summary: Person 1 - Test Custom Page Titles"); + it("When I navigate to a repeating section which has custom page title, Then all page titles in the section should have the correct prefix", async () => { + await browser.url(HubPage.url()); + await click(HubPage.submit()); + await expect(await browser.getTitle()).toBe("Individual interstitial: Person 1 - Test Custom Page Titles"); + await click(IndividualInterstitialPage.submit()); + await expect(await browser.getTitle()).toBe("Proxy question: Person 1 - Test Custom Page Titles"); + await click(ProxyPage.submit()); + await expect(await browser.getTitle()).toBe("What is your date of birth?: Person 1 - Test Custom Page Titles"); + await click(DateOfBirthPage.submit()); + await expect(await browser.getTitle()).toBe("Summary: Person 1 - Test Custom Page Titles"); }); }); }); diff --git a/tests/functional/spec/dates.spec.js b/tests/functional/spec/dates.spec.js index 998bb834fb..72751edcc6 100644 --- a/tests/functional/spec/dates.spec.js +++ b/tests/functional/spec/dates.spec.js @@ -4,209 +4,210 @@ import DateSinglePage from "../generated_pages/dates/date-single-block.page"; import DateNonMandatoryPage from "../generated_pages/dates/date-non-mandatory-block.page"; import DateYearDatePage from "../generated_pages/dates/date-year-date-block.page"; import SubmitPage from "../generated_pages/dates/submit.page"; +import { click, verifyUrlContains } from "../helpers"; describe("Date checks", () => { - beforeEach("Load the survey", () => { - browser.openQuestionnaire("test_dates.json"); + beforeEach("Load the survey", async () => { + await browser.openQuestionnaire("test_dates.json"); }); - it("Given an answer label is provided for a date question then the label should be displayed ", () => { - expect($(DateRangePage.legend()).getText()).to.contain("Period from"); + it("Given an answer label is provided for a date question then the label should be displayed ", async () => { + await expect(await $(DateRangePage.legend()).getText()).toBe("Period from"); }); - it("Given an answer label is not provided for a date question then the question title should be used within the legend ", () => { - $(DateRangePage.dateRangeFromday()).setValue(1); - $(DateRangePage.dateRangeFrommonth()).setValue(1); - $(DateRangePage.dateRangeFromyear()).setValue(1901); + it("Given an answer label is not provided for a date question then the question title should be used within the legend ", async () => { + await $(DateRangePage.dateRangeFromday()).setValue(1); + await $(DateRangePage.dateRangeFrommonth()).setValue(1); + await $(DateRangePage.dateRangeFromyear()).setValue(1901); - $(DateRangePage.dateRangeToday()).setValue(3); - $(DateRangePage.dateRangeTomonth()).setValue(5); - $(DateRangePage.dateRangeToyear()).setValue(2017); + await $(DateRangePage.dateRangeToday()).setValue(3); + await $(DateRangePage.dateRangeTomonth()).setValue(5); + await $(DateRangePage.dateRangeToyear()).setValue(2017); - $(DateRangePage.submit()).click(); + await click(DateRangePage.submit()); - expect($(DateMonthYearPage.legend()).getText()).to.contain("Date with month and year"); + await expect(await $(DateMonthYearPage.legend()).getText()).toBe("Date with month and year"); }); - it("Given the test_dates survey is selected when dates are entered then the summary screen shows the dates entered formatted", () => { + it("Given the test_dates survey is selected when dates are entered then the summary screen shows the dates entered formatted", async () => { // When dates are entered - $(DateRangePage.dateRangeFromday()).setValue(1); - $(DateRangePage.dateRangeFrommonth()).setValue(1); - $(DateRangePage.dateRangeFromyear()).setValue(1901); + await $(DateRangePage.dateRangeFromday()).setValue(1); + await $(DateRangePage.dateRangeFrommonth()).setValue(1); + await $(DateRangePage.dateRangeFromyear()).setValue(1901); - $(DateRangePage.dateRangeToday()).setValue(3); - $(DateRangePage.dateRangeTomonth()).setValue(5); - $(DateRangePage.dateRangeToyear()).setValue(2017); + await $(DateRangePage.dateRangeToday()).setValue(3); + await $(DateRangePage.dateRangeTomonth()).setValue(5); + await $(DateRangePage.dateRangeToyear()).setValue(2017); - $(DateRangePage.submit()).click(); + await click(DateRangePage.submit()); - $(DateMonthYearPage.Month()).setValue(4); - $(DateMonthYearPage.Year()).setValue(2018); + await $(DateMonthYearPage.Month()).setValue(4); + await $(DateMonthYearPage.Year()).setValue(2018); - $(DateMonthYearPage.submit()).click(); + await click(DateMonthYearPage.submit()); - $(DateSinglePage.day()).setValue(4); - $(DateSinglePage.month()).setValue(1); - $(DateSinglePage.year()).setValue(1999); + await $(DateSinglePage.day()).setValue(4); + await $(DateSinglePage.month()).setValue(1); + await $(DateSinglePage.year()).setValue(1999); - $(DateSinglePage.submit()).click(); + await click(DateSinglePage.submit()); - $(DateNonMandatoryPage.submit()).click(); + await click(DateNonMandatoryPage.submit()); - $(DateYearDatePage.Year()).setValue(2005); + await $(DateYearDatePage.Year()).setValue(2005); - $(DateYearDatePage.submit()).click(); + await click(DateYearDatePage.submit()); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); + await verifyUrlContains(SubmitPage.pageName); // Then the summary screen shows the dates entered formatted - expect($(SubmitPage.dateRangeFromAnswer()).getText()).to.contain("1 January 1901 to 3 May 2017"); - expect($(SubmitPage.monthYearAnswer()).getText()).to.contain("April 2018"); - expect($(SubmitPage.singleDateAnswer()).getText()).to.contain("4 January 1999"); - expect($(SubmitPage.nonMandatoryDateAnswer()).getText()).to.contain("No answer provided"); - expect($(SubmitPage.yearDateAnswer()).getText()).to.contain("2005"); + await expect(await $(SubmitPage.dateRangeFromAnswer()).getText()).toBe("1 January 1901 to 3 May 2017"); + await expect(await $(SubmitPage.monthYearAnswer()).getText()).toBe("April 2018"); + await expect(await $(SubmitPage.singleDateAnswer()).getText()).toBe("4 January 1999"); + await expect(await $(SubmitPage.nonMandatoryDateAnswer()).getText()).toBe("No answer provided"); + await expect(await $(SubmitPage.yearDateAnswer()).getText()).toBe("2005"); }); - it("Given the test_dates survey is selected when the from date is greater than the to date then an error message is shown", () => { + it("Given the test_dates survey is selected when the from date is greater than the to date then an error message is shown", async () => { // When the from date is greater than the to date - $(DateRangePage.dateRangeFromday()).setValue(1); - $(DateRangePage.dateRangeFrommonth()).setValue(1); - $(DateRangePage.dateRangeFromyear()).setValue(2016); + await $(DateRangePage.dateRangeFromday()).setValue(1); + await $(DateRangePage.dateRangeFrommonth()).setValue(1); + await $(DateRangePage.dateRangeFromyear()).setValue(2016); - $(DateRangePage.dateRangeToday()).setValue(1); - $(DateRangePage.dateRangeTomonth()).setValue(1); - $(DateRangePage.dateRangeToyear()).setValue(2015); + await $(DateRangePage.dateRangeToday()).setValue(1); + await $(DateRangePage.dateRangeTomonth()).setValue(1); + await $(DateRangePage.dateRangeToyear()).setValue(2015); - $(DateRangePage.submit()).click(); + await click(DateRangePage.submit()); // Then an error message is shown and the question panel is highlighted - expect($(DateRangePage.errorNumber(1)).getText()).to.contain("Enter a 'period to' date later than the 'period from' date"); - expect($(DateRangePage.dateRangeQuestionErrorPanel()).isExisting()).to.be.true; + await expect(await $(DateRangePage.errorNumber(1)).getText()).toBe("Enter a 'period to' date later than the 'period from' date"); + await expect(await $(DateRangePage.dateRangeQuestionErrorPanel()).isExisting()).toBe(true); // Then clicking error should focus on first input field - $(DateRangePage.errorNumber(1)).click(); - expect($(DateRangePage.dateRangeFromday()).isFocused()).to.be.true; + await $(DateRangePage.errorNumber(1)).click(); + await expect(await $(DateRangePage.dateRangeFromday()).isFocused()).toBe(true); }); - it("Given the test_dates survey is selected when the from date and the to date are the same then an error message is shown", () => { + it("Given the test_dates survey is selected when the from date and the to date are the same then an error message is shown", async () => { // When the from date is greater than the to date - $(DateRangePage.dateRangeFromday()).setValue(1); - $(DateRangePage.dateRangeFrommonth()).setValue(1); - $(DateRangePage.dateRangeFromyear()).setValue(2016); + await $(DateRangePage.dateRangeFromday()).setValue(1); + await $(DateRangePage.dateRangeFrommonth()).setValue(1); + await $(DateRangePage.dateRangeFromyear()).setValue(2016); - $(DateRangePage.dateRangeToday()).setValue(1); - $(DateRangePage.dateRangeTomonth()).setValue(1); - $(DateRangePage.dateRangeToyear()).setValue(2016); + await $(DateRangePage.dateRangeToday()).setValue(1); + await $(DateRangePage.dateRangeTomonth()).setValue(1); + await $(DateRangePage.dateRangeToyear()).setValue(2016); - $(DateRangePage.submit()).click(); + await click(DateRangePage.submit()); // Then an error message is shown and the question panel is highlighted - expect($(DateRangePage.errorNumber(1)).getText()).to.contain("Enter a 'period to' date later than the 'period from' date"); - expect($(DateRangePage.dateRangeQuestionErrorPanel()).isExisting()).to.be.true; + await expect(await $(DateRangePage.errorNumber(1)).getText()).toBe("Enter a 'period to' date later than the 'period from' date"); + await expect(await $(DateRangePage.dateRangeQuestionErrorPanel()).isExisting()).toBe(true); }); - it("Given the test_dates survey is selected when an invalid date is entered in a date range then an error message is shown", () => { + it("Given the test_dates survey is selected when an invalid date is entered in a date range then an error message is shown", async () => { // When the from date is greater than the to date - $(DateRangePage.dateRangeFromday()).setValue(1); - $(DateRangePage.dateRangeFrommonth()).setValue(1); - $(DateRangePage.dateRangeFromyear()).setValue(2016); + await $(DateRangePage.dateRangeFromday()).setValue(1); + await $(DateRangePage.dateRangeFrommonth()).setValue(1); + await $(DateRangePage.dateRangeFromyear()).setValue(2016); - $(DateRangePage.dateRangeToday()).setValue(1); - $(DateRangePage.dateRangeTomonth()).setValue(1); - $(DateRangePage.dateRangeToyear()).setValue(""); + await $(DateRangePage.dateRangeToday()).setValue(1); + await $(DateRangePage.dateRangeTomonth()).setValue(1); + await $(DateRangePage.dateRangeToyear()).setValue(""); - $(DateRangePage.submit()).click(); + await click(DateRangePage.submit()); // Then an error message is shown - expect($(DateRangePage.errorNumber(1)).getText()).to.contain("Enter a valid date"); + await expect(await $(DateRangePage.errorNumber(1)).getText()).toBe("Enter a valid date"); }); - it("Given the test_dates survey is selected when the year (month year type) is left empty then an error message is shown", () => { - $(DateRangePage.dateRangeFromday()).setValue(1); - $(DateRangePage.dateRangeFrommonth()).setValue(1); - $(DateRangePage.dateRangeFromyear()).setValue(2016); - $(DateRangePage.dateRangeToday()).setValue(1); - $(DateRangePage.dateRangeTomonth()).setValue(1); - $(DateRangePage.dateRangeToyear()).setValue(2017); - $(DateRangePage.submit()).click(); + it("Given the test_dates survey is selected when the year (month year type) is left empty then an error message is shown", async () => { + await $(DateRangePage.dateRangeFromday()).setValue(1); + await $(DateRangePage.dateRangeFrommonth()).setValue(1); + await $(DateRangePage.dateRangeFromyear()).setValue(2016); + await $(DateRangePage.dateRangeToday()).setValue(1); + await $(DateRangePage.dateRangeTomonth()).setValue(1); + await $(DateRangePage.dateRangeToyear()).setValue(2017); + await click(DateRangePage.submit()); // When the year (month year type) is left empty - $(DateMonthYearPage.Month()).setValue(4); - $(DateMonthYearPage.Year()).setValue(""); + await $(DateMonthYearPage.Month()).setValue(4); + await $(DateMonthYearPage.Year()).setValue(""); - $(DateMonthYearPage.submit()).click(); + await click(DateMonthYearPage.submit()); // Then an error message is shown - expect($(DateMonthYearPage.errorNumber(1)).getText()).to.contain("Enter a valid date"); + await expect(await $(DateMonthYearPage.errorNumber(1)).getText()).toBe("Enter a valid date"); }); - it("Given the test_dates survey is selected, " + "When an error message is shown and it is corrected, " + "Then the next question is displayed", () => { - $(DateRangePage.dateRangeFromday()).setValue(1); - $(DateRangePage.dateRangeFrommonth()).setValue(1); - $(DateRangePage.dateRangeFromyear()).setValue(2016); - $(DateRangePage.dateRangeToday()).setValue(1); - $(DateRangePage.dateRangeTomonth()).setValue(1); - $(DateRangePage.dateRangeToyear()).setValue(2017); - $(DateRangePage.submit()).click(); + it("Given the test_dates survey is selected, " + "When an error message is shown and it is corrected, " + "Then the next question is displayed", async () => { + await $(DateRangePage.dateRangeFromday()).setValue(1); + await $(DateRangePage.dateRangeFrommonth()).setValue(1); + await $(DateRangePage.dateRangeFromyear()).setValue(2016); + await $(DateRangePage.dateRangeToday()).setValue(1); + await $(DateRangePage.dateRangeTomonth()).setValue(1); + await $(DateRangePage.dateRangeToyear()).setValue(2017); + await click(DateRangePage.submit()); // When an error message is shown - $(DateMonthYearPage.Month()).setValue(4); - $(DateMonthYearPage.Year()).setValue(""); - $(DateMonthYearPage.submit()).click(); + await $(DateMonthYearPage.Month()).setValue(4); + await $(DateMonthYearPage.Year()).setValue(""); + await click(DateMonthYearPage.submit()); - expect($(DateMonthYearPage.error()).getText()).to.contain("Enter a valid date"); + await expect(await $(DateMonthYearPage.error()).getText()).toBe("Enter a valid date"); // Then when it is corrected, it goes to the next question - $(DateMonthYearPage.Year()).setValue(2018); - $(DateMonthYearPage.submit()).click(); + await $(DateMonthYearPage.Year()).setValue(2018); + await click(DateMonthYearPage.submit()); - expect(browser.getUrl()).to.contain(DateSinglePage.url()); + await verifyUrlContains(DateSinglePage.url()); }); - it("Given the test_dates survey is selected when an error message is shown then when it is corrected, it goes to the summary page and the information is correct", () => { - $(DateRangePage.dateRangeFromday()).setValue(1); - $(DateRangePage.dateRangeFrommonth()).setValue(1); - $(DateRangePage.dateRangeFromyear()).setValue(2016); - $(DateRangePage.dateRangeToday()).setValue(1); - $(DateRangePage.dateRangeTomonth()).setValue(1); - $(DateRangePage.dateRangeToyear()).setValue(2017); - $(DateRangePage.submit()).click(); + it("Given the test_dates survey is selected when an error message is shown then when it is corrected, it goes to the summary page and the information is correct", async () => { + await $(DateRangePage.dateRangeFromday()).setValue(1); + await $(DateRangePage.dateRangeFrommonth()).setValue(1); + await $(DateRangePage.dateRangeFromyear()).setValue(2016); + await $(DateRangePage.dateRangeToday()).setValue(1); + await $(DateRangePage.dateRangeTomonth()).setValue(1); + await $(DateRangePage.dateRangeToyear()).setValue(2017); + await click(DateRangePage.submit()); - $(DateMonthYearPage.Month()).setValue(1); - $(DateMonthYearPage.Year()).setValue(2016); - $(DateMonthYearPage.submit()).click(); + await $(DateMonthYearPage.Month()).setValue(1); + await $(DateMonthYearPage.Year()).setValue(2016); + await click(DateMonthYearPage.submit()); - $(DateSinglePage.day()).setValue(1); - $(DateSinglePage.month()).setValue(1); - $(DateSinglePage.year()).setValue(2016); - $(DateMonthYearPage.submit()).click(); + await $(DateSinglePage.day()).setValue(1); + await $(DateSinglePage.month()).setValue(1); + await $(DateSinglePage.year()).setValue(2016); + await click(DateMonthYearPage.submit()); // When non-mandatory is partially completed - $(DateNonMandatoryPage.day()).setValue(4); - $(DateNonMandatoryPage.month()).setValue(1); - $(DateNonMandatoryPage.submit()).click(); + await $(DateNonMandatoryPage.day()).setValue(4); + await $(DateNonMandatoryPage.month()).setValue(1); + await click(DateNonMandatoryPage.submit()); // Then an error message is shown - expect($(DateNonMandatoryPage.errorNumber(1)).getText()).to.contain("Enter a valid date"); + await expect(await $(DateNonMandatoryPage.errorNumber(1)).getText()).toBe("Enter a valid date"); }); - it("Given the test_dates survey is selected, when a user clicks the day label then the day subfield should gain the focus", () => { - $(DateRangePage.dateRangeFromday()).setValue(1); - $(DateRangePage.dateRangeFrommonth()).setValue(1); - $(DateRangePage.dateRangeFromyear()).setValue(2016); - $(DateRangePage.dateRangeToday()).setValue(1); - $(DateRangePage.dateRangeTomonth()).setValue(1); - $(DateRangePage.dateRangeToyear()).setValue(2017); - $(DateRangePage.submit()).click(); + it("Given the test_dates survey is selected, when a user clicks the day label then the day subfield should gain the focus", async () => { + await $(DateRangePage.dateRangeFromday()).setValue(1); + await $(DateRangePage.dateRangeFrommonth()).setValue(1); + await $(DateRangePage.dateRangeFromyear()).setValue(2016); + await $(DateRangePage.dateRangeToday()).setValue(1); + await $(DateRangePage.dateRangeTomonth()).setValue(1); + await $(DateRangePage.dateRangeToyear()).setValue(2017); + await click(DateRangePage.submit()); - $(DateMonthYearPage.Month()).setValue(1); - $(DateMonthYearPage.Year()).setValue(2016); - $(DateMonthYearPage.submit()).click(); + await $(DateMonthYearPage.Month()).setValue(1); + await $(DateMonthYearPage.Year()).setValue(2016); + await click(DateMonthYearPage.submit()); // When a user clicks the day label - $(DateSinglePage.dayLabel()).click(); + await $(DateSinglePage.dayLabel()).click(); // Then the day subfield should gain the focus - expect($(DateSinglePage.day()).isFocused()).to.be.true; + await expect(await $(DateSinglePage.day()).isFocused()).toBe(true); }); }); diff --git a/tests/functional/spec/dob_date.spec.js b/tests/functional/spec/dob_date.spec.js index 208e1b405e..73b71c0836 100644 --- a/tests/functional/spec/dob_date.spec.js +++ b/tests/functional/spec/dob_date.spec.js @@ -1,22 +1,23 @@ import DateOfBirthPage from "../generated_pages/dob_date/date-of-birth.page"; import UnderSixteenPage from "../generated_pages/dob_date/under-sixteen.page"; +import { click } from "../helpers"; describe("Date of birth check", () => { - beforeEach("Load the survey", () => { - browser.openQuestionnaire("test_dob_date.json"); + beforeEach("Load the survey", async () => { + await browser.openQuestionnaire("test_dob_date.json"); }); - it("Given I am completing a date question, When I enter a value less than 16 years, Then I am routed to under 16 page", () => { - $(DateOfBirthPage.day()).setValue(12); - $(DateOfBirthPage.month()).setValue(4); - $(DateOfBirthPage.year()).setValue(2021); - $(DateOfBirthPage.submit()).click(); - expect($(UnderSixteenPage.legend()).getText()).to.contain("You are under 16!"); + it("Given I am completing a date question, When I enter a value less than 16 years, Then I am routed to under 16 page", async () => { + await $(DateOfBirthPage.day()).setValue(12); + await $(DateOfBirthPage.month()).setValue(4); + await $(DateOfBirthPage.year()).setValue(2021); + await click(DateOfBirthPage.submit()); + await expect(await $(UnderSixteenPage.legend()).getText()).toBe("You are under 16!"); }); - it("Given I am completing a date question, When I enter a value less than 16 years, Then I am routed to over 16 page", () => { - $(DateOfBirthPage.day()).setValue(12); - $(DateOfBirthPage.month()).setValue(4); - $(DateOfBirthPage.year()).setValue(1980); - $(DateOfBirthPage.submit()).click(); - expect($(UnderSixteenPage.legend()).getText()).to.contain("You are over 16!"); + it("Given I am completing a date question, When I enter a value less than 16 years, Then I am routed to over 16 page", async () => { + await $(DateOfBirthPage.day()).setValue(12); + await $(DateOfBirthPage.month()).setValue(4); + await $(DateOfBirthPage.year()).setValue(1980); + await click(DateOfBirthPage.submit()); + await expect(await $(UnderSixteenPage.legend()).getText()).toBe("You are over 16!"); }); }); diff --git a/tests/functional/spec/durations.spec.js b/tests/functional/spec/durations.spec.js index 32adbdc280..59dc2c22b3 100644 --- a/tests/functional/spec/durations.spec.js +++ b/tests/functional/spec/durations.spec.js @@ -1,93 +1,101 @@ import DurationPage from "../generated_pages/durations/duration-block.page.js"; import SubmitPage from "../generated_pages/durations/submit.page.js"; +import { click, verifyUrlContains } from "../helpers"; describe("Durations", () => { - beforeEach("Load the survey", () => { - browser.openQuestionnaire("test_durations.json"); + beforeEach("Load the survey", async () => { + await browser.openQuestionnaire("test_durations.json"); }); - it("Given the test_durations survey is selected when durations are entered then the summary screen shows the durations entered formatted", () => { - $(DurationPage.yearMonthYears()).setValue(1); - $(DurationPage.yearMonthMonths()).setValue(2); - $(DurationPage.mandatoryYearMonthYears()).setValue(1); - $(DurationPage.mandatoryYearMonthMonths()).setValue(2); - $(DurationPage.mandatoryYearYears()).setValue(1); - $(DurationPage.mandatoryMonthMonths()).setValue(1); - $(DurationPage.submit()).click(); - - expect(browser.getUrl()).to.contain(SubmitPage.pageName); - expect($(SubmitPage.yearMonthAnswer()).getText()).to.equal("1 year 2 months"); - $(SubmitPage.submit()).click(); + it("Given the test_durations survey is selected durations suffixes are visible", async () => { + await expect(await $(DurationPage.yearMonthYearsSuffix()).getText()).toBe("Years"); + await expect(await $(DurationPage.mandatoryYearMonthMonthsSuffix()).getText()).toBe("Months"); + await expect(await $(DurationPage.yearYearsSuffix()).getText()).toBe("Years"); + await expect(await $(DurationPage.mandatoryMonthMonthsSuffix()).getText()).toBe("Months"); }); - it("Given the test_durations survey is selected when one of the units is 0 it is excluded from the summary", () => { - $(DurationPage.yearMonthYears()).setValue(0); - $(DurationPage.yearMonthMonths()).setValue(2); - $(DurationPage.mandatoryYearMonthYears()).setValue(1); - $(DurationPage.mandatoryYearMonthMonths()).setValue(2); - $(DurationPage.mandatoryYearYears()).setValue(1); - $(DurationPage.mandatoryMonthMonths()).setValue(1); - $(DurationPage.submit()).click(); - - expect(browser.getUrl()).to.contain(SubmitPage.pageName); - expect($(SubmitPage.yearMonthAnswer()).getText()).to.equal("2 months"); - $(SubmitPage.submit()).click(); + it("Given the test_durations survey is selected when durations are entered then the summary screen shows the durations entered formatted", async () => { + await $(DurationPage.yearMonthYears()).setValue(1); + await $(DurationPage.yearMonthMonths()).setValue(2); + await $(DurationPage.mandatoryYearMonthYears()).setValue(1); + await $(DurationPage.mandatoryYearMonthMonths()).setValue(2); + await $(DurationPage.mandatoryYearYears()).setValue(1); + await $(DurationPage.mandatoryMonthMonths()).setValue(1); + await click(DurationPage.submit()); + + await verifyUrlContains(SubmitPage.pageName); + await expect(await $(SubmitPage.yearMonthAnswer()).getText()).toBe("1 year 2 months"); + await click(SubmitPage.submit()); + }); + + it("Given the test_durations survey is selected when one of the units is 0 it is excluded from the summary", async () => { + await $(DurationPage.yearMonthYears()).setValue(0); + await $(DurationPage.yearMonthMonths()).setValue(2); + await $(DurationPage.mandatoryYearMonthYears()).setValue(1); + await $(DurationPage.mandatoryYearMonthMonths()).setValue(2); + await $(DurationPage.mandatoryYearYears()).setValue(1); + await $(DurationPage.mandatoryMonthMonths()).setValue(1); + await click(DurationPage.submit()); + + await verifyUrlContains(SubmitPage.pageName); + await expect(await $(SubmitPage.yearMonthAnswer()).getText()).toBe("2 months"); + await click(SubmitPage.submit()); }); - it("Given the test_durations survey is selected when no duration is entered the summary shows no answer provided", () => { - $(DurationPage.mandatoryYearMonthYears()).setValue(1); - $(DurationPage.mandatoryYearMonthMonths()).setValue(2); - $(DurationPage.mandatoryYearYears()).setValue(1); - $(DurationPage.mandatoryMonthMonths()).setValue(1); - $(DurationPage.submit()).click(); + it("Given the test_durations survey is selected when no duration is entered the summary shows no answer provided", async () => { + await $(DurationPage.mandatoryYearMonthYears()).setValue(1); + await $(DurationPage.mandatoryYearMonthMonths()).setValue(2); + await $(DurationPage.mandatoryYearYears()).setValue(1); + await $(DurationPage.mandatoryMonthMonths()).setValue(1); + await click(DurationPage.submit()); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); - expect($(SubmitPage.yearMonthAnswer()).getText()).to.equal("No answer provided"); - $(SubmitPage.submit()).click(); + await verifyUrlContains(SubmitPage.pageName); + await expect(await $(SubmitPage.yearMonthAnswer()).getText()).toBe("No answer provided"); + await click(SubmitPage.submit()); }); - it("Given the test_durations survey is selected when one of the units is missing an error is shown", () => { - $(DurationPage.yearMonthMonths()).setValue(2); - $(DurationPage.mandatoryYearMonthMonths()).setValue(2); - $(DurationPage.mandatoryYearYears()).setValue(1); - $(DurationPage.mandatoryMonthMonths()).setValue(1); - $(DurationPage.submit()).click(); + it("Given the test_durations survey is selected when one of the units is missing an error is shown", async () => { + await $(DurationPage.yearMonthMonths()).setValue(2); + await $(DurationPage.mandatoryYearMonthMonths()).setValue(2); + await $(DurationPage.mandatoryYearYears()).setValue(1); + await $(DurationPage.mandatoryMonthMonths()).setValue(1); + await click(DurationPage.submit()); - expect($(DurationPage.errorNumber(1)).getText()).to.contain("Enter a valid duration"); - expect($(DurationPage.errorNumber(2)).getText()).to.contain("Enter a valid duration"); + await expect(await $(DurationPage.errorNumber(1)).getText()).toBe("Enter a valid duration"); + await expect(await $(DurationPage.errorNumber(2)).getText()).toBe("Enter a valid duration"); }); - it("Given the test_durations survey is selected when one of the units not a number an error is shown", () => { - $(DurationPage.yearMonthYears()).setValue("word"); - $(DurationPage.yearMonthMonths()).setValue(2); - $(DurationPage.mandatoryYearMonthYears()).setValue("word"); - $(DurationPage.mandatoryYearMonthMonths()).setValue(2); - $(DurationPage.mandatoryYearYears()).setValue(1); - $(DurationPage.mandatoryMonthMonths()).setValue(1); - $(DurationPage.submit()).click(); - - expect($(DurationPage.errorNumber(1)).getText()).to.contain("Enter a valid duration"); - expect($(DurationPage.errorNumber(2)).getText()).to.contain("Enter a valid duration"); + it("Given the test_durations survey is selected when one of the units not a number an error is shown", async () => { + await $(DurationPage.yearMonthYears()).setValue("word"); + await $(DurationPage.yearMonthMonths()).setValue(2); + await $(DurationPage.mandatoryYearMonthYears()).setValue("word"); + await $(DurationPage.mandatoryYearMonthMonths()).setValue(2); + await $(DurationPage.mandatoryYearYears()).setValue(1); + await $(DurationPage.mandatoryMonthMonths()).setValue(1); + await click(DurationPage.submit()); + + await expect(await $(DurationPage.errorNumber(1)).getText()).toBe("Enter a valid duration"); + await expect(await $(DurationPage.errorNumber(2)).getText()).toBe("Enter a valid duration"); }); - it("Given the test_durations survey is selected when the number of months is more than 11 an error is shown", () => { - $(DurationPage.yearMonthYears()).setValue(1); - $(DurationPage.yearMonthMonths()).setValue(12); - $(DurationPage.mandatoryYearMonthYears()).setValue(1); - $(DurationPage.mandatoryYearMonthMonths()).setValue(12); - $(DurationPage.mandatoryYearYears()).setValue(1); - $(DurationPage.mandatoryMonthMonths()).setValue(1); - $(DurationPage.submit()).click(); - - expect($(DurationPage.errorNumber(1)).getText()).to.contain("Enter a valid duration"); - expect($(DurationPage.errorNumber(2)).getText()).to.contain("Enter a valid duration"); + it("Given the test_durations survey is selected when the number of months is more than 11 an error is shown", async () => { + await $(DurationPage.yearMonthYears()).setValue(1); + await $(DurationPage.yearMonthMonths()).setValue(12); + await $(DurationPage.mandatoryYearMonthYears()).setValue(1); + await $(DurationPage.mandatoryYearMonthMonths()).setValue(12); + await $(DurationPage.mandatoryYearYears()).setValue(1); + await $(DurationPage.mandatoryMonthMonths()).setValue(1); + await click(DurationPage.submit()); + + await expect(await $(DurationPage.errorNumber(1)).getText()).toBe("Enter a valid duration"); + await expect(await $(DurationPage.errorNumber(2)).getText()).toBe("Enter a valid duration"); }); - it("Given the test_durations survey is selected when the mandatory duration is missing an error is shown", () => { - $(DurationPage.mandatoryYearYears()).setValue(1); - $(DurationPage.mandatoryMonthMonths()).setValue(1); - $(DurationPage.submit()).click(); + it("Given the test_durations survey is selected when the mandatory duration is missing an error is shown", async () => { + await $(DurationPage.mandatoryYearYears()).setValue(1); + await $(DurationPage.mandatoryMonthMonths()).setValue(1); + await click(DurationPage.submit()); - expect($(DurationPage.errorNumber(1)).getText()).to.contain("Enter a duration"); + await expect(await $(DurationPage.errorNumber(1)).getText()).toBe("Enter a duration"); }); }); diff --git a/tests/functional/spec/error_messages.spec.js b/tests/functional/spec/error_messages.spec.js index 7d23f9d2bc..2f8ea17e9c 100644 --- a/tests/functional/spec/error_messages.spec.js +++ b/tests/functional/spec/error_messages.spec.js @@ -1,31 +1,33 @@ import AboutYou from "../generated_pages/multiple_answers/about-you-block.page"; - -function answerAllButOne() { - $(AboutYou.textfield()).setValue("John Doe"); - $(AboutYou.dateday()).setValue("1"); - $(AboutYou.datemonth()).setValue("1"); - $(AboutYou.dateyear()).setValue("1995"); - $(AboutYou.checkboxBmw()).click(); - $(AboutYou.radioYes()).click(); - $(AboutYou.currency()).setValue("50000"); - $(AboutYou.monthYearDateMonth()).setValue("10"); - $(AboutYou.monthYearDateYear()).setValue("2021"); - $(AboutYou.dropdown()).selectByAttribute("value", "Silver"); - $(AboutYou.unit()).setValue("10000"); - $(AboutYou.durationMonths()).setValue("3"); - $(AboutYou.durationYears()).setValue("3"); - $(AboutYou.yearDateYear()).setValue("2019"); - $(AboutYou.number()).setValue("5"); - $(AboutYou.percentage()).setValue("3"); - $(AboutYou.mobileNumber()).setValue("07700900111"); +import BlockPage from "../generated_pages/percentage/block.page"; +import { click } from "../helpers"; + +async function answerAllButOne() { + await $(AboutYou.textfield()).setValue("John Doe"); + await $(AboutYou.dateday()).setValue("1"); + await $(AboutYou.datemonth()).setValue("1"); + await $(AboutYou.dateyear()).setValue("1995"); + await $(AboutYou.checkboxBmw()).click(); + await $(AboutYou.radioYes()).click(); + await $(AboutYou.currency()).setValue("50000"); + await $(AboutYou.monthYearDateMonth()).setValue("10"); + await $(AboutYou.monthYearDateYear()).setValue("2021"); + await $(AboutYou.dropdown()).selectByAttribute("value", "Silver"); + await $(AboutYou.unit()).setValue("10000"); + await $(AboutYou.durationMonths()).setValue("3"); + await $(AboutYou.durationYears()).setValue("3"); + await $(AboutYou.yearDateYear()).setValue("2019"); + await $(AboutYou.number()).setValue("5"); + await $(AboutYou.percentage()).setValue("3"); + await $(AboutYou.mobileNumber()).setValue("07700900111"); } describe("Error Messages", () => { - beforeEach(() => { - browser.openQuestionnaire("test_multiple_answers.json"); + beforeEach(async () => { + await browser.openQuestionnaire("test_multiple_answers.json"); }); - it("Given a question has errors, When errors are displayed, Then the error messages are correct", () => { + it("Given a question has errors, When errors are displayed, Then the error messages are correct", async () => { const errorMessageMap = { 1: "Enter an answer", 2: "Enter a date", @@ -43,83 +45,112 @@ describe("Error Messages", () => { 14: "Enter an answer", }; - $(AboutYou.submit()).click(); - expect($(AboutYou.errorHeader()).getText()).to.equal("There are 14 problems with your answer"); + await click(AboutYou.submit()); + await expect(await $(AboutYou.errorHeader()).getText()).toBe("There are 14 problems with your answer"); for (const [index, errorMessage] of Object.entries(errorMessageMap)) { - expect($(AboutYou.errorNumber(index)).getText()).to.contain(errorMessage); + await expect(await $(AboutYou.errorNumber(index)).getText()).toContain(errorMessage); } }); - it("Given a question has errors, When errors are displayed, Then the error message for each answer is correct", () => { - $(AboutYou.submit()).click(); - - expect($(AboutYou.textfieldErrorItem()).getText()).to.equal("Enter an answer"); - expect($(AboutYou.dateErrorItem()).getText()).to.equal("Enter a date"); - expect($(AboutYou.checkboxErrorItem()).getText()).to.contain("Select at least one answer"); - expect($(AboutYou.radioErrorItem()).getText()).to.contain("Select an answer"); - expect($(AboutYou.currencyErrorItem()).getText()).to.equal("Enter an answer"); - expect($(AboutYou.monthYearDateErrorItem()).getText()).to.equal("Enter a date"); - expect($(AboutYou.dropdownErrorItem()).getText()).to.equal("Select an answer"); - expect($(AboutYou.unitErrorItem()).getText()).to.equal("Enter an answer"); - expect($(AboutYou.durationErrorItem()).getText()).to.equal("Enter a duration"); - expect($(AboutYou.yearDateErrorItem()).getText()).to.equal("Enter a date"); - expect($(AboutYou.numberErrorItem()).getText()).to.equal("Enter an answer"); - expect($(AboutYou.percentageErrorItem()).getText()).to.equal("Enter an answer"); - expect($(AboutYou.mobileNumberErrorItem()).getText()).to.equal("Enter a UK mobile number"); - expect($(AboutYou.textareaErrorItem()).getText()).to.equal("Enter an answer"); + it("Given a question has errors, When errors are displayed, Then the error message for each answer is correct", async () => { + await click(AboutYou.submit()); + + await expect(await $(AboutYou.textfieldErrorItem()).getText()).toBe("Enter an answer"); + await expect(await $(AboutYou.dateErrorItem()).getText()).toBe("Enter a date"); + await expect(await $(AboutYou.checkboxErrorItem()).getText()).toContain("Select at least one answer"); + await expect(await $(AboutYou.radioErrorItem()).getText()).toContain("Select an answer"); + await expect(await $(AboutYou.currencyErrorItem()).getText()).toBe("Enter an answer"); + await expect(await $(AboutYou.monthYearDateErrorItem()).getText()).toBe("Enter a date"); + await expect(await $(AboutYou.dropdownErrorItem()).getText()).toBe("Select an answer"); + await expect(await $(AboutYou.unitErrorItem()).getText()).toBe("Enter an answer"); + await expect(await $(AboutYou.durationErrorItem()).getText()).toBe("Enter a duration"); + await expect(await $(AboutYou.yearDateErrorItem()).getText()).toBe("Enter a date"); + await expect(await $(AboutYou.numberErrorItem()).getText()).toBe("Enter an answer"); + await expect(await $(AboutYou.percentageErrorItem()).getText()).toBe("Enter an answer"); + await expect(await $(AboutYou.mobileNumberErrorItem()).getText()).toBe("Enter a UK mobile number"); + await expect(await $(AboutYou.textareaErrorItem()).getText()).toBe("Enter an answer"); + }); + + it("Given a question has multiple errors, When the errors are displayed, Then the error messages are in a numbered list", async () => { + await click(AboutYou.submit()); + await expect(await $(AboutYou.errorList()).isDisplayed()).toBe(true); + }); + + it("Given a question has 1 error, When the error is displayed, Then error message isn't in a numbered list", async () => { + await answerAllButOne(); + + await click(AboutYou.submit()); + await expect(await $(AboutYou.singleErrorLink()).isDisplayed()).toBe(true); }); - it("Given a question has 1 error, When the error is displayed, Then error header is correct", () => { - answerAllButOne(); + it("Given a question has 1 error, When the error is displayed, Then error header is correct", async () => { + await answerAllButOne(); - $(AboutYou.submit()).click(); - expect($(AboutYou.errorHeader()).getText()).to.equal("There is a problem with your answer"); + await click(AboutYou.submit()); + await expect(await $(AboutYou.errorHeader()).getText()).toBe("There is a problem with your answer"); }); - it("Given a question has errors, When an error message is clicked, Then the correct answer is focused", () => { - $(AboutYou.submit()).click(); + it("Given a question has errors, When an error message is clicked, Then the correct answer is focused", async () => { + await click(AboutYou.submit()); - $(AboutYou.errorNumber(1)).click(); - expect($(AboutYou.textfield()).isFocused()).to.be.true; + await $(AboutYou.errorNumber(1)).click(); + await expect(await $(AboutYou.textfield()).isFocused()).toBe(true); - $(AboutYou.errorNumber(2)).click(); - expect($(AboutYou.dateday()).isFocused()).to.be.true; + await $(AboutYou.errorNumber(2)).click(); + await expect(await $(AboutYou.dateday()).isFocused()).toBe(true); - $(AboutYou.errorNumber(3)).click(); - expect($(AboutYou.checkboxBmw()).isFocused()).to.be.true; + await $(AboutYou.errorNumber(3)).click(); + await expect(await $(AboutYou.checkboxBmw()).isFocused()).toBe(true); - $(AboutYou.errorNumber(4)).click(); - expect($(AboutYou.radioYes()).isFocused()).to.be.true; + await $(AboutYou.errorNumber(4)).click(); + await expect(await $(AboutYou.radioYes()).isFocused()).toBe(true); - $(AboutYou.errorNumber(5)).click(); - expect($(AboutYou.currency()).isFocused()).to.be.true; + await $(AboutYou.errorNumber(5)).click(); + await expect(await $(AboutYou.currency()).isFocused()).toBe(true); - $(AboutYou.errorNumber(6)).click(); - expect($(AboutYou.monthYearDateMonth()).isFocused()).to.be.true; + await $(AboutYou.errorNumber(6)).click(); + await expect(await $(AboutYou.monthYearDateMonth()).isFocused()).toBe(true); - $(AboutYou.errorNumber(7)).click(); - expect($(AboutYou.dropdown()).isFocused()).to.be.true; + await $(AboutYou.errorNumber(7)).click(); + await expect(await $(AboutYou.dropdown()).isFocused()).toBe(true); - $(AboutYou.errorNumber(8)).click(); - expect($(AboutYou.unit()).isFocused()).to.be.true; + await $(AboutYou.errorNumber(8)).click(); + await expect(await $(AboutYou.unit()).isFocused()).toBe(true); - $(AboutYou.errorNumber(9)).click(); - expect($(AboutYou.durationYears()).isFocused()).to.be.true; + await $(AboutYou.errorNumber(9)).click(); + await expect(await $(AboutYou.durationYears()).isFocused()).toBe(true); - $(AboutYou.errorNumber(10)).click(); - expect($(AboutYou.yearDateYear()).isFocused()).to.be.true; + await $(AboutYou.errorNumber(10)).click(); + await expect(await $(AboutYou.yearDateYear()).isFocused()).toBe(true); - $(AboutYou.errorNumber(11)).click(); - expect($(AboutYou.number()).isFocused()).to.be.true; + await $(AboutYou.errorNumber(11)).click(); + await expect(await $(AboutYou.number()).isFocused()).toBe(true); - $(AboutYou.errorNumber(12)).click(); - expect($(AboutYou.percentage()).isFocused()).to.be.true; + await $(AboutYou.errorNumber(12)).click(); + await expect(await $(AboutYou.percentage()).isFocused()).toBe(true); - $(AboutYou.errorNumber(13)).click(); - expect($(AboutYou.mobileNumber()).isFocused()).to.be.true; + await $(AboutYou.errorNumber(13)).click(); + await expect(await $(AboutYou.mobileNumber()).isFocused()).toBe(true); - $(AboutYou.errorNumber(14)).click(); - expect($(AboutYou.textarea()).isFocused()).to.be.true; + await $(AboutYou.errorNumber(14)).click(); + await expect(await $(AboutYou.textarea()).isFocused()).toBe(true); + }); +}); +describe("Error Message NaN value", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_percentage.json"); + }); + it("Given a NaN value was entered on percentage question, When the error is displayed, Then the error message is correct", async () => { + await $(BlockPage.answer()).setValue("NaN"); + await click(BlockPage.submit()); + await expect(await $(BlockPage.errorHeader()).getText()).toBe("There is a problem with your answer"); + await expect(await $(BlockPage.answerErrorItem()).getText()).toBe("Enter a number"); + }); + it("Given a NaN value with separators was entered on percentage question, When the error is displayed, Then the error message is correct", async () => { + await $(BlockPage.answer()).setValue(",NaN_"); + await click(BlockPage.submit()); + await expect(await $(BlockPage.errorHeader()).getText()).toBe("There is a problem with your answer"); + await expect(await $(BlockPage.answerErrorItem()).getText()).toBe("Enter a number"); }); }); diff --git a/tests/functional/spec/exit.spec.js b/tests/functional/spec/exit.spec.js deleted file mode 100644 index caeb849cfb..0000000000 --- a/tests/functional/spec/exit.spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import CensusThankYouPage from "../base_pages/census-thank-you.page.js"; -import HubPage from "../base_pages/hub.page"; -import { SubmitPage } from "../base_pages/submit.page.js"; - -describe("Post submission exit", () => { - beforeEach("Load the survey", () => { - browser.openQuestionnaire("test_thank_you_census_household.json"); - }); - - it("Given I click the exit button from the thank you page which has no session cookie, When I am redirected, Then I should be redirected to the correct log out url", () => { - $(SubmitPage.submit()).click(); - $(HubPage.submit()).click(); - browser.deleteAllCookies(); - $(CensusThankYouPage.exit()).click(); - expect(browser.getUrl()).to.equal("https://surveys.ons.gov.uk/sign-in/"); - }); - - it("Given I click the exit button from the thank you page, When I am redirected, Then I should be redirected to the correct log out url", () => { - $(SubmitPage.submit()).click(); - $(HubPage.submit()).click(); - $(CensusThankYouPage.exit()).click(); - expect(browser.getUrl()).to.equal("https://census.gov.uk/en/start"); - }); - - it("Given I have clicked the exit button, When I navigate back, Then I am taken to the session timed out page", () => { - $(SubmitPage.submit()).click(); - $(HubPage.submit()).click(); - $(CensusThankYouPage.exit()).click(); - browser.back(); - expect(browser.getUrl()).to.contain("submitted/thank-you"); - expect($("body").getHTML()).to.contain("Sorry, you need to sign in again"); - }); -}); diff --git a/tests/functional/spec/features/calculated_summary.spec.js b/tests/functional/spec/features/calculated_summary.spec.js deleted file mode 100644 index 24c8ebfac2..0000000000 --- a/tests/functional/spec/features/calculated_summary.spec.js +++ /dev/null @@ -1,234 +0,0 @@ -import FirstNumberBlockPage from "../../generated_pages/calculated_summary/first-number-block.page.js"; -import SecondNumberBlockPage from "../../generated_pages/calculated_summary/second-number-block.page.js"; -import ThirdNumberBlockPage from "../../generated_pages/calculated_summary/third-number-block.page.js"; -import ThirdAndAHalfNumberBlockPage from "../../generated_pages/calculated_summary/third-and-a-half-number-block.page.js"; -import SkipFourthBlockPage from "../../generated_pages/calculated_summary/skip-fourth-block.page.js"; -import FourthNumberBlockPage from "../../generated_pages/calculated_summary/fourth-number-block.page.js"; -import FourthAndAHalfNumberBlockPage from "../../generated_pages/calculated_summary/fourth-and-a-half-number-block.page.js"; -import FifthNumberBlockPage from "../../generated_pages/calculated_summary/fifth-number-block.page.js"; -import SixthNumberBlockPage from "../../generated_pages/calculated_summary/sixth-number-block.page.js"; -import CurrencyTotalPlaybackPageWithFourth from "../../generated_pages/calculated_summary/currency-total-playback-with-fourth.page.js"; -import CurrencyTotalPlaybackPageSkippedFourth from "../../generated_pages/calculated_summary/currency-total-playback-skipped-fourth.page.js"; -import UnitTotalPlaybackPage from "../../generated_pages/calculated_summary/unit-total-playback.page.js"; -import PercentageTotalPlaybackPage from "../../generated_pages/calculated_summary/percentage-total-playback.page.js"; -import NumberTotalPlaybackPage from "../../generated_pages/calculated_summary/number-total-playback.page.js"; -import CalculatedSummaryTotalConfirmation from "../../generated_pages/calculated_summary/calculated-summary-total-confirmation.page"; -import SubmitPage from "../../generated_pages/calculated_summary/submit.page"; -import ThankYouPage from "../../base_pages/thank-you.page.js"; - -describe("Feature: Calculated Summary", () => { - describe("Given I have a Calculated Summary", () => { - before("Get to Calculated Summary", () => { - browser.openQuestionnaire("test_calculated_summary.json"); - - $(FirstNumberBlockPage.firstNumber()).setValue(1.23); - $(FirstNumberBlockPage.submit()).click(); - - $(SecondNumberBlockPage.secondNumber()).setValue(4.56); - $(SecondNumberBlockPage.secondNumberUnitTotal()).setValue(789); - $(SecondNumberBlockPage.secondNumberAlsoInTotal()).setValue(0.12); - $(SecondNumberBlockPage.submit()).click(); - - $(ThirdNumberBlockPage.thirdNumber()).setValue(3.45); - $(ThirdNumberBlockPage.submit()).click(); - $(ThirdAndAHalfNumberBlockPage.thirdAndAHalfNumberUnitTotal()).setValue(678); - $(ThirdAndAHalfNumberBlockPage.submit()).click(); - - $(SkipFourthBlockPage.no()).click(); - $(SkipFourthBlockPage.submit()).click(); - - $(FourthNumberBlockPage.fourthNumber()).setValue(9.01); - $(FourthNumberBlockPage.submit()).click(); - $(FourthAndAHalfNumberBlockPage.fourthAndAHalfNumberAlsoInTotal()).setValue(2.34); - $(FourthAndAHalfNumberBlockPage.submit()).click(); - - $(FifthNumberBlockPage.fifthPercent()).setValue(56); - $(FifthNumberBlockPage.fifthNumber()).setValue(78.91); - $(FifthNumberBlockPage.submit()).click(); - - $(SixthNumberBlockPage.sixthPercent()).setValue(23); - $(SixthNumberBlockPage.sixthNumber()).setValue(45.67); - $(SixthNumberBlockPage.submit()).click(); - - const browserUrl = browser.getUrl(); - - expect(browserUrl).to.contain(CurrencyTotalPlaybackPageWithFourth.pageName); - }); - - it("Given I complete every question, When I get to the currency summary, Then I should see the correct total", () => { - // Totals and titles should be shown - expect($(CurrencyTotalPlaybackPageWithFourth.calculatedSummaryTitle()).getText()).to.contain( - "We calculate the total of currency values entered to be ÂŖ20.71. Is this correct?" - ); - expect($(CurrencyTotalPlaybackPageWithFourth.calculatedSummaryQuestion()).getText()).to.contain("Grand total of previous values"); - expect($(CurrencyTotalPlaybackPageWithFourth.calculatedSummaryAnswer()).getText()).to.contain("ÂŖ20.71"); - - // Answers included in calculation should be shown - expect($(CurrencyTotalPlaybackPageWithFourth.firstNumberAnswerLabel()).getText()).to.contain("First answer label"); - expect($(CurrencyTotalPlaybackPageWithFourth.firstNumberAnswer()).getText()).to.contain("ÂŖ1.23"); - expect($(CurrencyTotalPlaybackPageWithFourth.secondNumberAnswerLabel()).getText()).to.contain("Second answer in currency label"); - expect($(CurrencyTotalPlaybackPageWithFourth.secondNumberAnswer()).getText()).to.contain("ÂŖ4.56"); - expect($(CurrencyTotalPlaybackPageWithFourth.secondNumberAnswerAlsoInTotalLabel()).getText()).to.contain( - "Second answer label also in currency total (optional)" - ); - expect($(CurrencyTotalPlaybackPageWithFourth.secondNumberAnswerAlsoInTotal()).getText()).to.contain("ÂŖ0.12"); - expect($(CurrencyTotalPlaybackPageWithFourth.thirdNumberAnswerLabel()).getText()).to.contain("Third answer label"); - expect($(CurrencyTotalPlaybackPageWithFourth.thirdNumberAnswer()).getText()).to.contain("ÂŖ3.45"); - expect($(CurrencyTotalPlaybackPageWithFourth.fourthNumberAnswerLabel()).getText()).to.contain("Fourth answer label (optional)"); - expect($(CurrencyTotalPlaybackPageWithFourth.fourthNumberAnswer()).getText()).to.contain("ÂŖ9.01"); - expect($(CurrencyTotalPlaybackPageWithFourth.fourthAndAHalfNumberAnswerAlsoInTotalLabel()).getText()).to.contain( - "Fourth answer label also in total (optional)" - ); - expect($(CurrencyTotalPlaybackPageWithFourth.fourthAndAHalfNumberAnswerAlsoInTotal()).getText()).to.contain("ÂŖ2.34"); - - // Answers not included in calculation should not be shown - expect($$(UnitTotalPlaybackPage.secondNumberAnswerUnitTotal())).to.be.empty; - expect($$(UnitTotalPlaybackPage.thirdAndAHalfNumberAnswerUnitTotal())).to.be.empty; - expect($$(NumberTotalPlaybackPage.fifthNumberAnswer())).to.be.empty; - expect($$(NumberTotalPlaybackPage.sixthNumberAnswer())).to.be.empty; - }); - - it("Given change an answer, When I get to the currency summary, Then I should see the new total", () => { - $(CurrencyTotalPlaybackPageWithFourth.fourthNumberAnswerEdit()).click(); - $(FourthNumberBlockPage.fourthNumber()).setValue(19.01); - $(FourthNumberBlockPage.submit()).click(); - $(FourthAndAHalfNumberBlockPage.fourthAndAHalfNumberAlsoInTotal()).setValue(12.34); - $(FourthAndAHalfNumberBlockPage.submit()).click(); - - $(FifthNumberBlockPage.submit()).click(); - $(SixthNumberBlockPage.submit()).click(); - - expect(browser.getUrl()).to.contain(CurrencyTotalPlaybackPageWithFourth.pageName); - expect($(CurrencyTotalPlaybackPageWithFourth.calculatedSummaryTitle()).getText()).to.contain( - "We calculate the total of currency values entered to be ÂŖ40.71. Is this correct?" - ); - expect($(CurrencyTotalPlaybackPageWithFourth.calculatedSummaryAnswer()).getText()).to.contain("ÂŖ40.71"); - }); - - it("Given I leave an answer empty, When I get to the currency summary, Then I should see no answer provided and new total", () => { - $(CurrencyTotalPlaybackPageWithFourth.fourthAndAHalfNumberAnswerAlsoInTotalEdit()).click(); - $(FourthAndAHalfNumberBlockPage.fourthAndAHalfNumberAlsoInTotal()).setValue(""); - $(FourthAndAHalfNumberBlockPage.submit()).click(); - $(FifthNumberBlockPage.submit()).click(); - $(SixthNumberBlockPage.submit()).click(); - - expect(browser.getUrl()).to.contain(CurrencyTotalPlaybackPageWithFourth.pageName); - expect($(CurrencyTotalPlaybackPageWithFourth.calculatedSummaryTitle()).getText()).to.contain( - "We calculate the total of currency values entered to be ÂŖ28.37. Is this correct?" - ); - expect($(CurrencyTotalPlaybackPageWithFourth.calculatedSummaryAnswer()).getText()).to.contain("ÂŖ28.37"); - expect($(CurrencyTotalPlaybackPageWithFourth.fourthAndAHalfNumberAnswerAlsoInTotal()).getText()).to.contain("No answer provided"); - }); - - it("Given I skip the fourth page, When I get to the playback, Then I can should not see it in the total", () => { - $(CurrencyTotalPlaybackPageWithFourth.thirdNumberAnswerEdit()).click(); - $(ThirdNumberBlockPage.submit()).click(); - $(ThirdAndAHalfNumberBlockPage.submit()).click(); - - $(SkipFourthBlockPage.yes()).click(); - $(SkipFourthBlockPage.submit()).click(); - - $(FifthNumberBlockPage.submit()).click(); - $(SixthNumberBlockPage.submit()).click(); - - const expectedUrl = browser.getUrl(); - - expect(expectedUrl).to.contain(CurrencyTotalPlaybackPageSkippedFourth.pageName); - expect($$(CurrencyTotalPlaybackPageWithFourth.fourthNumberAnswer())).to.be.empty; - expect($$(CurrencyTotalPlaybackPageWithFourth.fourthAndAHalfNumberAnswerAlsoInTotal())).to.be.empty; - expect($(CurrencyTotalPlaybackPageSkippedFourth.calculatedSummaryTitle()).getText()).to.contain( - "We calculate the total of currency values entered to be ÂŖ9.36. Is this correct?" - ); - expect($(CurrencyTotalPlaybackPageSkippedFourth.calculatedSummaryAnswer()).getText()).to.contain("ÂŖ9.36"); - }); - - it("Given I complete every question, When I get to the unit summary, Then I should see the correct total", () => { - // Totals and titles should be shown - $(CurrencyTotalPlaybackPageWithFourth.submit()).click(); - expect($(UnitTotalPlaybackPage.calculatedSummaryTitle()).getText()).to.contain( - "We calculate the total of unit values entered to be 1,467 cm. Is this correct?" - ); - expect($(UnitTotalPlaybackPage.calculatedSummaryQuestion()).getText()).to.contain("Grand total of previous values"); - expect($(UnitTotalPlaybackPage.calculatedSummaryAnswer()).getText()).to.contain("1,467 cm"); - - // Answers included in calculation should be shown - expect($(UnitTotalPlaybackPage.secondNumberAnswerUnitTotalLabel()).getText()).to.contain("Second answer label in unit total"); - expect($(UnitTotalPlaybackPage.secondNumberAnswerUnitTotal()).getText()).to.contain("789 cm"); - expect($(UnitTotalPlaybackPage.thirdAndAHalfNumberAnswerUnitTotalLabel()).getText()).to.contain("Third answer label in unit total"); - expect($(UnitTotalPlaybackPage.thirdAndAHalfNumberAnswerUnitTotal()).getText()).to.contain("678 cm"); - }); - - it("Given I complete every question, When I get to the percentage summary, Then I should see the correct total", () => { - // Totals and titles should be shown - $(UnitTotalPlaybackPage.submit()).click(); - expect($(UnitTotalPlaybackPage.calculatedSummaryTitle()).getText()).to.contain( - "We calculate the total of percentage values entered to be 79%. Is this correct?" - ); - expect($(UnitTotalPlaybackPage.calculatedSummaryQuestion()).getText()).to.contain("Grand total of previous values"); - expect($(UnitTotalPlaybackPage.calculatedSummaryAnswer()).getText()).to.contain("79%"); - - // Answers included in calculation should be shown - expect($(PercentageTotalPlaybackPage.fifthPercentAnswerLabel()).getText()).to.contain("Fifth answer label percentage tota"); - expect($(PercentageTotalPlaybackPage.fifthPercentAnswer()).getText()).to.contain("56%"); - expect($(PercentageTotalPlaybackPage.sixthPercentAnswerLabel()).getText()).to.contain("Sixth answer label percentage tota"); - expect($(PercentageTotalPlaybackPage.sixthPercentAnswer()).getText()).to.contain("23%"); - }); - - it("Given I complete every question, When I get to the number summary, Then I should see the correct total", () => { - // Totals and titles should be shown - $(UnitTotalPlaybackPage.submit()).click(); - expect($(UnitTotalPlaybackPage.calculatedSummaryTitle()).getText()).to.contain( - "We calculate the total of number values entered to be 124.58. Is this correct?" - ); - expect($(UnitTotalPlaybackPage.calculatedSummaryQuestion()).getText()).to.contain("Grand total of previous values"); - expect($(UnitTotalPlaybackPage.calculatedSummaryAnswer()).getText()).to.contain("124.58"); - - // Answers included in calculation should be shown - expect($(NumberTotalPlaybackPage.fifthNumberAnswerLabel()).getText()).to.contain("Fifth answer label number total"); - expect($(NumberTotalPlaybackPage.fifthNumberAnswer()).getText()).to.contain("78.91"); - expect($(NumberTotalPlaybackPage.sixthNumberAnswerLabel()).getText()).to.contain("Sixth answer label number total"); - expect($(NumberTotalPlaybackPage.sixthNumberAnswer()).getText()).to.contain("45.67"); - }); - - it("Given I complete every calculated summary, When I go to a page with calculated summary piping, Then I should the see the piped calculated summary total for each summary", () => { - $(NumberTotalPlaybackPage.submit()).click(); - - const content = $("h1 + ul").getText(); - const textsToAssert = [ - "Total currency values (if Q4 not skipped): ÂŖ28.37", - "Total currency values (if Q4 skipped)): ÂŖ9.36", - "Total unit values: 1,467", - "Total percentage values: 79", - "Total number values: 124.58", - ]; - - textsToAssert.forEach((text) => expect(content).to.contain(text)); - }); - - it("Given I confirm the totals and am on the summary, When I edit and change an answer, Then I must re-confirm the calculated summary page which is dependent on the change before I can return to the summary", () => { - $(CalculatedSummaryTotalConfirmation.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); - - $(SubmitPage.thirdNumberAnswerEdit()).click(); - $(ThirdNumberBlockPage.thirdNumber()).setValue(3.5); - $(ThirdNumberBlockPage.submit()).click(); - $(ThirdAndAHalfNumberBlockPage.submit()).click(); - $(SkipFourthBlockPage.submit()).click(); - $(FifthNumberBlockPage.submit()).click(); - $(SixthNumberBlockPage.submit()).click(); - - expect($(CurrencyTotalPlaybackPageSkippedFourth.calculatedSummaryTitle()).getText()).to.contain( - "We calculate the total of currency values entered to be ÂŖ9.41. Is this correct?" - ); - - $(CurrencyTotalPlaybackPageSkippedFourth.submit()).click(); - - expect(browser.getUrl()).to.contain(SubmitPage.pageName); - }); - - it("Given I am on the summary, When I submit the questionnaire, Then I should see the thank you page", () => { - $(SubmitPage.submit()).click(); - expect(browser.getUrl()).to.contain(ThankYouPage.pageName); - }); - }); -}); diff --git a/tests/functional/spec/features/conditional_question_title/conditional_checkbox_title.spec.js b/tests/functional/spec/features/conditional_question_title/conditional_checkbox_title.spec.js index 5f43fabbc8..1b8f1fa8aa 100644 --- a/tests/functional/spec/features/conditional_question_title/conditional_checkbox_title.spec.js +++ b/tests/functional/spec/features/conditional_question_title/conditional_checkbox_title.spec.js @@ -2,45 +2,45 @@ import CheckBoxPage from "../../../generated_pages/titles_radio_and_checkbox/che import NameEntryPage from "../../../generated_pages/titles_radio_and_checkbox/preamble-block.page"; import RadioButtonsPage from "../../../generated_pages/titles_radio_and_checkbox/radio-block.page"; import SubmitPage from "../../../generated_pages/titles_radio_and_checkbox/submit.page"; - +import { click } from "../../../helpers"; describe("Feature: Conditional checkbox and radio question titles", () => { - beforeEach(() => { - browser.openQuestionnaire("test_titles_radio_and_checkbox.json"); + beforeEach(async () => { + await browser.openQuestionnaire("test_titles_radio_and_checkbox.json"); }); describe("Given I start the test_titles_radio_and_checkbox survey", () => { - it("When I enter an expected name and submit", () => { - $(NameEntryPage.name()).setValue("Peter"); - $(NameEntryPage.submit()).click(); - expect($(CheckBoxPage.questionText()).getText()).to.contain("Did Peter make changes to this business?"); + it("When I enter an expected name and submit", async () => { + await $(NameEntryPage.name()).setValue("Peter"); + await click(NameEntryPage.submit()); + await expect(await $(CheckBoxPage.questionText()).getText()).toBe("Did Peter make changes to this business?"); }); - it("When I enter an unknown name and go to the checkbox page", () => { - $(NameEntryPage.name()).setValue("Fred"); - $(NameEntryPage.submit()).click(); - expect($(CheckBoxPage.questionText()).getText()).to.contain("Did this business make major changes in the following areas"); - $(CheckBoxPage.checkboxImplementationOfChangesToMarketingConceptsOrStrategies()).click(); - expect($(RadioButtonsPage.questionText()).getText()).to.contain("Did this business make major changes in the following areas"); + it("When I enter an unknown name and go to the checkbox page", async () => { + await $(NameEntryPage.name()).setValue("Fred"); + await click(NameEntryPage.submit()); + await expect(await $(CheckBoxPage.questionText()).getText()).toBe("Did this business make major changes in the following areas?"); + await $(CheckBoxPage.checkboxImplementationOfChangesToMarketingConceptsOrStrategies()).click(); + await expect(await $(RadioButtonsPage.questionText()).getText()).toBe("Did this business make major changes in the following areas?"); }); - it("When I enter another known name page title should include selected title", () => { - $(NameEntryPage.name()).setValue("Mary"); - $(NameEntryPage.submit()).click(); + it("When I enter another known name page title should include selected title", async () => { + await $(NameEntryPage.name()).setValue("Mary"); + await click(NameEntryPage.submit()); - expect(browser.getTitle()).to.contain("Did Mary make changes to this business? - Test Survey - Checkbox and Radio titles"); + await expect(await browser.getTitle()).toBe("Did Mary make changes to this business? - Test Survey - Checkbox and Radio titles"); }); - it("When I enter another known name and go to the summary", () => { - $(NameEntryPage.name()).setValue("Mary"); - $(NameEntryPage.submit()).click(); - expect($(CheckBoxPage.questionText()).getText()).to.contain("Did Mary make changes to this business"); - $(CheckBoxPage.checkboxImplementationOfChangesToMarketingConceptsOrStrategiesLabel()).click(); - $(CheckBoxPage.submit()).click(); - expect($(RadioButtonsPage.questionText()).getText()).to.contain("Is Mary the boss?"); - $(RadioButtonsPage.radioMaybe()).click(); - $(RadioButtonsPage.submit()).click(); - expect($(SubmitPage.nameAnswer()).getText()).to.contain("Mary"); - expect($(SubmitPage.checkboxQuestion()).getText()).to.contain("Did Mary make changes to this business?"); + it("When I enter another known name and go to the summary", async () => { + await $(NameEntryPage.name()).setValue("Mary"); + await click(NameEntryPage.submit()); + await expect(await $(CheckBoxPage.questionText()).getText()).toBe("Did Mary make changes to this business?"); + await $(CheckBoxPage.checkboxImplementationOfChangesToMarketingConceptsOrStrategiesLabel()).click(); + await click(CheckBoxPage.submit()); + await expect(await $(RadioButtonsPage.questionText()).getText()).toBe("Is Mary the boss?"); + await $(RadioButtonsPage.radioMaybe()).click(); + await click(RadioButtonsPage.submit()); + await expect(await $(SubmitPage.nameAnswer()).getText()).toBe("Mary"); + await expect(await $(SubmitPage.checkboxQuestion()).getText()).toBe("Did Mary make changes to this business?"); }); }); }); diff --git a/tests/functional/spec/features/confirmation_question.spec.js b/tests/functional/spec/features/confirmation_question.spec.js index f67c3dacff..418038019e 100644 --- a/tests/functional/spec/features/confirmation_question.spec.js +++ b/tests/functional/spec/features/confirmation_question.spec.js @@ -1,39 +1,40 @@ import NumberOfEmployeesTotalBlockPage from "../../generated_pages/confirmation_question/number-of-employees-total-block.page.js"; import ConfirmZeroEmployeesBlockPage from "../../generated_pages/confirmation_question/confirm-zero-employees-block.page.js"; import SubmitPage from "../../generated_pages/confirmation_question/submit.page.js"; +import { click, verifyUrlContains } from "../../helpers"; describe("Feature: Confirmation Question", () => { describe("Given I have a completed the confirmation question", () => { - before("Get to summary", () => { - browser.openQuestionnaire("test_confirmation_question.json"); + before("Get to summary", async () => { + await browser.openQuestionnaire("test_confirmation_question.json"); }); - it("When I view the summary, Then the confirmation question should not be displayed", () => { - $(NumberOfEmployeesTotalBlockPage.numberOfEmployeesTotal()).setValue(0); - $(NumberOfEmployeesTotalBlockPage.submit()).click(); - $(ConfirmZeroEmployeesBlockPage.yesThisIsCorrect()).click(); - $(ConfirmZeroEmployeesBlockPage.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); - expect($(SubmitPage.numberOfEmployeesTotal()).getText()).to.contain("0"); - expect($$(SubmitPage.confirmZeroEmployeesAnswer())).to.be.empty; + it("When I view the summary, Then the confirmation question should not be displayed", async () => { + await $(NumberOfEmployeesTotalBlockPage.numberOfEmployeesTotal()).setValue(0); + await click(NumberOfEmployeesTotalBlockPage.submit()); + await $(ConfirmZeroEmployeesBlockPage.yesThisIsCorrect()).click(); + await click(ConfirmZeroEmployeesBlockPage.submit()); + await verifyUrlContains(SubmitPage.pageName); + await expect(await $(SubmitPage.numberOfEmployeesTotal()).getText()).toBe("0"); + await expect(await $$(SubmitPage.confirmZeroEmployeesAnswer())).toHaveLength(0); }); }); describe("Given a confirmation Question", () => { - it("When I answer 'No' to the confirmation question, Then I should be routed back to the source question", () => { - browser.openQuestionnaire("test_confirmation_question.json"); - $(NumberOfEmployeesTotalBlockPage.submit()).click(); - $(ConfirmZeroEmployeesBlockPage.noINeedToChangeThis()).click(); - $(ConfirmZeroEmployeesBlockPage.submit()).click(); - expect(browser.getUrl()).to.contain(NumberOfEmployeesTotalBlockPage.pageName); + it("When I answer 'No' to the confirmation question, Then I should be routed back to the source question", async () => { + await browser.openQuestionnaire("test_confirmation_question.json"); + await click(NumberOfEmployeesTotalBlockPage.submit()); + await $(ConfirmZeroEmployeesBlockPage.noINeedToCorrectThis()).click(); + await click(ConfirmZeroEmployeesBlockPage.submit()); + await verifyUrlContains(NumberOfEmployeesTotalBlockPage.pageName); }); }); describe("Given a number of employees Question", () => { - it("When I don't answer the number of employees question and go to summary, Then default value should be displayed for the the number of employees question", () => { - browser.openQuestionnaire("test_confirmation_question.json"); - $(NumberOfEmployeesTotalBlockPage.submit()).click(); - $(ConfirmZeroEmployeesBlockPage.yesThisIsCorrect()).click(); - $(ConfirmZeroEmployeesBlockPage.submit()).click(); - expect($(SubmitPage.numberOfEmployeesTotal()).getText()).to.contain("0"); + it("When I don't answer the number of employees question and go to summary, Then default value should be displayed for the the number of employees question", async () => { + await browser.openQuestionnaire("test_confirmation_question.json"); + await click(NumberOfEmployeesTotalBlockPage.submit()); + await $(ConfirmZeroEmployeesBlockPage.yesThisIsCorrect()).click(); + await click(ConfirmZeroEmployeesBlockPage.submit()); + await expect(await $(SubmitPage.numberOfEmployeesTotal()).getText()).toBe("0"); }); }); }); diff --git a/tests/functional/spec/features/confirmation_question_within_repeating_section.spec.js b/tests/functional/spec/features/confirmation_question_within_repeating_section.spec.js index f33ae93edc..e9d713d467 100644 --- a/tests/functional/spec/features/confirmation_question_within_repeating_section.spec.js +++ b/tests/functional/spec/features/confirmation_question_within_repeating_section.spec.js @@ -4,60 +4,60 @@ import CarerPage from "../../generated_pages/confirmation_question_within_repeat import DateOfBirthPage from "../../generated_pages/confirmation_question_within_repeating_section/dob-block.page"; import ConfirmDateOfBirthPage from "../../generated_pages/confirmation_question_within_repeating_section/confirm-dob-block.page"; import DefaultSectionSummary from "../../generated_pages/confirmation_question_within_repeating_section/default-section-summary.page"; - +import { click, verifyUrlContains } from "../../helpers"; describe("Feature: Confirmation Question Within A Repeating Section", () => { describe("Given I am in a repeating section", () => { - beforeEach("Add a person", () => { - browser.openQuestionnaire("test_confirmation_question_within_repeating_section.json"); - $(DoesAnyoneLiveHerePage.yes()).click(); - $(DoesAnyoneLiveHerePage.submit()).click(); - $(AddPersonPage.firstName()).setValue("John"); - $(AddPersonPage.lastName()).setValue("Doe"); - $(AddPersonPage.submit()).click(); - $(DoesAnyoneLiveHerePage.no()).click(); - $(DoesAnyoneLiveHerePage.submit()).click(); - expect(browser.getUrl()).to.contain(DateOfBirthPage.url().split("/").slice(-1)[0]); + beforeEach("Add a person", async () => { + await browser.openQuestionnaire("test_confirmation_question_within_repeating_section.json"); + await $(DoesAnyoneLiveHerePage.yes()).click(); + await click(DoesAnyoneLiveHerePage.submit()); + await $(AddPersonPage.firstName()).setValue("John"); + await $(AddPersonPage.lastName()).setValue("Doe"); + await click(AddPersonPage.submit()); + await $(DoesAnyoneLiveHerePage.no()).click(); + await click(DoesAnyoneLiveHerePage.submit()); + await verifyUrlContains(DateOfBirthPage.url().split("/").slice(-1)[0]); }); describe("Given a confirmation question", () => { - it("When I answer 'No' to the confirmation question, Then I should be routed back to the source question", () => { + it("When I answer 'No' to the confirmation question, Then I should be routed back to the source question", async () => { // Answer question preceding confirmation question - $(DateOfBirthPage.day()).setValue("01"); - $(DateOfBirthPage.month()).setValue("01"); - $(DateOfBirthPage.year()).setValue("2007"); - $(DateOfBirthPage.submit()).click(); + await $(DateOfBirthPage.day()).setValue("01"); + await $(DateOfBirthPage.month()).setValue("01"); + await $(DateOfBirthPage.year()).setValue("2015"); + await click(DateOfBirthPage.submit()); // Answer 'No' to confirmation question - $(ConfirmDateOfBirthPage.noINeedToChangeTheirDateOfBirth()).click(); - $(ConfirmDateOfBirthPage.submit()).click(); - expect(browser.getUrl()).to.contain(DateOfBirthPage.pageName); + await $(ConfirmDateOfBirthPage.noINeedToChangeTheirDateOfBirth()).click(); + await click(ConfirmDateOfBirthPage.submit()); + await verifyUrlContains(DateOfBirthPage.pageName); }); }); describe("Given I have answered a confirmation question", () => { - it("When I view the summary, Then the confirmation question should not be displayed", () => { - $(DateOfBirthPage.day()).setValue("01"); - $(DateOfBirthPage.month()).setValue("01"); - $(DateOfBirthPage.year()).setValue("2007"); - $(DateOfBirthPage.submit()).click(); + it("When I view the summary, Then the confirmation question should not be displayed", async () => { + await $(DateOfBirthPage.day()).setValue("01"); + await $(DateOfBirthPage.month()).setValue("01"); + await $(DateOfBirthPage.year()).setValue("2015"); + await click(DateOfBirthPage.submit()); - $(ConfirmDateOfBirthPage.yesPersonNameIsAgeOld()).click(); - $(ConfirmDateOfBirthPage.submit()).click(); + await $(ConfirmDateOfBirthPage.yesPersonNameIsAgeOld()).click(); + await click(ConfirmDateOfBirthPage.submit()); - expect(browser.getUrl()).to.contain("sections/default-section/"); - expect($(DefaultSectionSummary.confirmDateOfBirth()).isExisting()).to.be.false; + await verifyUrlContains("sections/default-section/"); + await expect(await $(DefaultSectionSummary.confirmDateOfBirth()).isExisting()).toBe(false); }); }); describe("Given a confirmation question with a skip condition", () => { - it("When I submit an a date of birth where the age is at least '16', Then I should be skipped past the confirmation question and directed to the carer question", () => { - $(DateOfBirthPage.day()).setValue("01"); - $(DateOfBirthPage.month()).setValue("01"); - $(DateOfBirthPage.year()).setValue("2000"); - $(DateOfBirthPage.submit()).click(); + it("When I submit an a date of birth where the age is at least '16', Then I should be skipped past the confirmation question and directed to the carer question", async () => { + await $(DateOfBirthPage.day()).setValue("01"); + await $(DateOfBirthPage.month()).setValue("01"); + await $(DateOfBirthPage.year()).setValue("2000"); + await click(DateOfBirthPage.submit()); - expect(browser.getUrl()).to.contain(CarerPage.pageName); - expect($(CarerPage.questionText()).getText()).to.contain("Does John Doe look"); + await verifyUrlContains(CarerPage.pageName); + await expect(await $(CarerPage.questionText()).getText()).toContain("Does John Doe look"); }); }); }); diff --git a/tests/functional/spec/features/default_value/default_value.spec.js b/tests/functional/spec/features/default_value/default_value.spec.js index 62c826f818..8b5497ecde 100644 --- a/tests/functional/spec/features/default_value/default_value.spec.js +++ b/tests/functional/spec/features/default_value/default_value.spec.js @@ -3,36 +3,36 @@ import QuestionPageTwo from "../../../generated_pages/default/number-question-tw import SubmitPage from "../../../generated_pages/default/submit.page.js"; import QuestionPageOneSkip from "../../../generated_pages/default_with_skip/number-question-one.page.js"; import QuestionPageThreeSkip from "../../../generated_pages/default_with_skip/number-question-three.page.js"; - +import { click, verifyUrlContains } from "../../../helpers"; describe("Feature: Default Value", () => { - it('Given I start default schema, When I do not answer a question, Then "no answer provided" is displayed on the Summary page', () => { - browser.openQuestionnaire("test_default.json"); - $(QuestionPageOne.submit()).click(); - expect(browser.getUrl()).to.contain(QuestionPageTwo.pageName); - $(QuestionPageTwo.two()).setValue(123); - $(QuestionPageTwo.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); - expect($(SubmitPage.answerOne()).getText()).to.contain("0"); + it('Given I start default schema, When I do not answer a question, Then "no answer provided" is displayed on the Summary page', async () => { + await browser.openQuestionnaire("test_default.json"); + await click(QuestionPageOne.submit()); + await verifyUrlContains(QuestionPageTwo.pageName); + await $(QuestionPageTwo.two()).setValue(123); + await click(QuestionPageTwo.submit()); + await verifyUrlContains(SubmitPage.pageName); + await expect(await $(SubmitPage.answerOne()).getText()).toBe("0"); }); - it("Given I have not answered a question containing a default value, When I return to the question, Then no value should be displayed", () => { - browser.openQuestionnaire("test_default.json"); - $(QuestionPageOne.submit()).click(); - expect(browser.getUrl()).to.contain(QuestionPageTwo.pageName); - $(QuestionPageTwo.two()).setValue(123); - $(QuestionPageTwo.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); - $(SubmitPage.previous()).click(); - expect(browser.getUrl()).to.contain(QuestionPageTwo.pageName); - $(QuestionPageTwo.previous()).click(); - expect(browser.getUrl()).to.contain(QuestionPageOne.pageName); - expect($(QuestionPageOne.one()).getValue()).to.equal(""); + it("Given I have not answered a question containing a default value, When I return to the question, Then no value should be displayed", async () => { + await browser.openQuestionnaire("test_default.json"); + await click(QuestionPageOne.submit()); + await verifyUrlContains(QuestionPageTwo.pageName); + await $(QuestionPageTwo.two()).setValue(123); + await click(QuestionPageTwo.submit()); + await verifyUrlContains(SubmitPage.pageName); + await $(SubmitPage.previous()).click(); + await verifyUrlContains(QuestionPageTwo.pageName); + await $(QuestionPageTwo.previous()).click(); + await verifyUrlContains(QuestionPageOne.pageName); + await expect(await $(QuestionPageOne.one()).getValue()).toBe(""); }); - it("Given I have not answered a question containing a default value, When a skip condition checks for the default value, Then I should skip the next question", () => { - browser.openQuestionnaire("test_default_with_skip.json"); - $(QuestionPageOneSkip.submit()).click(); - expect(browser.getUrl()).to.contain(QuestionPageThreeSkip.pageName); - expect($(QuestionPageThreeSkip.questionText()).getText()).to.contain("Question Three"); + it("Given I have not answered a question containing a default value, When a skip condition checks for the default value, Then I should skip the next question", async () => { + await browser.openQuestionnaire("test_default_with_skip.json"); + await click(QuestionPageOneSkip.submit()); + await verifyUrlContains(QuestionPageThreeSkip.pageName); + await expect(await $(QuestionPageThreeSkip.questionText()).getText()).toBe("Question Three"); }); }); diff --git a/tests/functional/spec/features/dynamic_answer_options/function_driven.js b/tests/functional/spec/features/dynamic_answer_options/function_driven.js index 5d07111266..12ce602565 100644 --- a/tests/functional/spec/features/dynamic_answer_options/function_driven.js +++ b/tests/functional/spec/features/dynamic_answer_options/function_driven.js @@ -4,7 +4,7 @@ import DynamicRadioPage from "../../../generated_pages/dynamic_answer_options_fu import DynamicDropdownPage from "../../../generated_pages/dynamic_answer_options_function_driven_with_static_options/dynamic-dropdown.page"; import DynamicMutuallyExclusivePage from "../../../generated_pages/dynamic_answer_options_function_driven_with_static_options/dynamic-mutually-exclusive.page"; import SubmitPage from "../../../generated_pages/dynamic_answer_options_function_driven_with_static_options/submit.page"; - +import { click, verifyUrlContains } from "../../../helpers"; const dropdownOptionValues = ["2020-12-28", "2020-12-29", "2020-12-30", "2020-12-31", "2021-01-01", "2021-01-02", "2021-01-03"]; const dropdownOptionValuesWithStaticOption = [...dropdownOptionValues, "I did not work"]; @@ -17,147 +17,147 @@ const testCases = [ { schemaName: "test_dynamic_answer_options_function_driven.json", answerOptionCount: 7, - dropdownOptionValues: dropdownOptionValues, + dropdownOptionValues, }, ]; -const openQuestionnaireAndSetUp = (schema) => { - browser.openQuestionnaire(schema); +const openQuestionnaireAndSetUp = async (schema) => { + await browser.openQuestionnaire(schema); // Set reference date - $(ReferenceDatePage.day()).setValue("1"); - $(ReferenceDatePage.month()).setValue("1"); - $(ReferenceDatePage.year()).setValue("2021"); - $(ReferenceDatePage.submit()).click(); + await $(ReferenceDatePage.day()).setValue("1"); + await $(ReferenceDatePage.month()).setValue("1"); + await $(ReferenceDatePage.year()).setValue("2021"); + await click(ReferenceDatePage.submit()); }; testCases.forEach((testCase) => { describe(`Feature: Dynamically generated answer options driven by a function (${testCase.schemaName})`, () => { describe("Selecting/Deselecting", () => { - before("Open questionnaire", () => { - openQuestionnaireAndSetUp(testCase.schemaName); + before("Open questionnaire", async () => { + await openQuestionnaireAndSetUp(testCase.schemaName); }); describe("Given a dynamic answer options questionnaire and I am on a dynamic checkbox answer page", () => { - it("When I click a checkbox option, then the checkbox should be selected", () => { + it("When I click a checkbox option, then the checkbox should be selected", async () => { for (let i = 0; i < testCase.answerOptionCount; i++) { - $(DynamicCheckboxPage.answerByIndex(i)).click(); - expect($(DynamicCheckboxPage.answerByIndex(i)).isSelected()).to.be.true; + await $(DynamicCheckboxPage.answerByIndex(i)).click(); + await expect(await $(DynamicCheckboxPage.answerByIndex(i)).isSelected()).toBe(true); } }); - it("When I click a selected option, then it should be deselected", () => { + it("When I click a selected option, then it should be deselected", async () => { for (let i = 0; i < testCase.answerOptionCount; i++) { - $(DynamicCheckboxPage.answerByIndex(i)).click(); - expect($(DynamicCheckboxPage.answerByIndex(i)).isSelected()).to.be.false; + await $(DynamicCheckboxPage.answerByIndex(i)).click(); + await expect(await $(DynamicCheckboxPage.answerByIndex(i)).isSelected()).toBe(false); } }); - it("When I submit the page, then I should be taken to the next page", () => { - $(DynamicCheckboxPage.submit()).click(); - expect(browser.getUrl()).to.contain(DynamicRadioPage.pageName); + it("When I submit the page, then I should be taken to the next page", async () => { + await click(DynamicCheckboxPage.submit()); + await verifyUrlContains(DynamicRadioPage.pageName); }); }); describe("Given a dynamic answer options questionnaire and I am on the radio answer page", () => { - it("When I click a radio option, then the radio should be selected", () => { + it("When I click a radio option, then the radio should be selected", async () => { for (let i = 0; i < testCase.answerOptionCount; i++) { - $(DynamicRadioPage.answerByIndex(i)).click(); - expect($(DynamicRadioPage.answerByIndex(i)).isSelected()).to.be.true; + await $(DynamicRadioPage.answerByIndex(i)).click(); + await expect(await $(DynamicRadioPage.answerByIndex(i)).isSelected()).toBe(true); } }); - it("When I submit the page, then I should be taken to the next page", () => { - $(DynamicRadioPage.submit()).click(); - expect(browser.getUrl()).to.contain(DynamicDropdownPage.pageName); + it("When I submit the page, then I should be taken to the next page", async () => { + await click(DynamicRadioPage.submit()); + await verifyUrlContains(DynamicDropdownPage.pageName); }); }); describe("Given a dynamic answer options questionnaire and I am on the dropdown page", () => { - it("When I select a dropdown option, then the option should be selected", () => { + it("When I select a dropdown option, then the option should be selected", async () => { for (const value of testCase.dropdownOptionValues) { - $(DynamicDropdownPage.answer()).selectByAttribute("value", value); - expect($(DynamicDropdownPage.answer()).getValue()).to.equal(value); + await $(DynamicDropdownPage.answer()).selectByAttribute("value", value); + await expect(await $(DynamicDropdownPage.answer()).getValue()).toBe(value); } }); - it("When I submit the page, then I should be taken to the next page", () => { - $(DynamicDropdownPage.submit()).click(); - expect(browser.getUrl()).to.contain(DynamicMutuallyExclusivePage.pageName); + it("When I submit the page, then I should be taken to the next page", async () => { + await click(DynamicDropdownPage.submit()); + await verifyUrlContains(DynamicMutuallyExclusivePage.pageName); }); }); describe("Given a dynamic answer options questionnaire and I am on the mutually exclusive page", () => { - it("When I click a dynamic checkbox option, then the checkbox should be selected", () => { + it("When I click a dynamic checkbox option, then the checkbox should be selected", async () => { for (let i = 0; i < testCase.answerOptionCount; i++) { - $(DynamicMutuallyExclusivePage.answerByIndex(i)).click(); - expect($(DynamicMutuallyExclusivePage.answerByIndex(i)).isSelected()).to.be.true; + await $(DynamicMutuallyExclusivePage.answerByIndex(i)).click(); + await expect(await $(DynamicMutuallyExclusivePage.answerByIndex(i)).isSelected()).toBe(true); } }); - it("When I click a selected option, then it should be deselected", () => { + it("When I click a selected option, then it should be deselected", async () => { for (let i = 0; i < testCase.answerOptionCount; i++) { - $(DynamicMutuallyExclusivePage.answerByIndex(i)).click(); - expect($(DynamicMutuallyExclusivePage.answerByIndex(i)).isSelected()).to.be.false; + await $(DynamicMutuallyExclusivePage.answerByIndex(i)).click(); + await expect(await $(DynamicMutuallyExclusivePage.answerByIndex(i)).isSelected()).toBe(false); } }); - it("When I click the static checkbox option, then the static checkbox should be selected", () => { + it("When I click the static checkbox option, then the static checkbox should be selected", async () => { // Test exclusive option (Static option) - $(DynamicMutuallyExclusivePage.staticIDidNotWork()).click(); - expect($(DynamicMutuallyExclusivePage.staticIDidNotWork()).isSelected()).to.be.true; + await $(DynamicMutuallyExclusivePage.staticIDidNotWork()).click(); + await expect(await $(DynamicMutuallyExclusivePage.staticIDidNotWork()).isSelected()).toBe(true); }); - it("When I click the selected static checkbox option, then that checkbox should be deselected", () => { + it("When I click the selected static checkbox option, then that checkbox should be deselected", async () => { // Test exclusive option (Static option) - $(DynamicMutuallyExclusivePage.staticIDidNotWork()).click(); - expect($(DynamicMutuallyExclusivePage.staticIDidNotWork()).isSelected()).to.be.false; + await $(DynamicMutuallyExclusivePage.staticIDidNotWork()).click(); + await expect(await $(DynamicMutuallyExclusivePage.staticIDidNotWork()).isSelected()).toBe(false); }); }); }); describe("Summary page", () => { - beforeEach("Open questionnaire", () => { - openQuestionnaireAndSetUp(testCase.schemaName); + beforeEach("Open questionnaire", async () => { + await openQuestionnaireAndSetUp(testCase.schemaName); }); describe("Given a dynamic answer options questionnaire", () => { - it("When I submit my questions without answering, then the summary should display `No answer provided` for each question", () => { - $(DynamicCheckboxPage.submit()).click(); - $(DynamicRadioPage.submit()).click(); - $(DynamicRadioPage.submit()).click(); - $(DynamicMutuallyExclusivePage.submit()).click(); - - expect(browser.getUrl()).to.contain(SubmitPage.pageName); - expect($(SubmitPage.dynamicCheckboxAnswer()).getText()).to.equal("No answer provided"); - expect($(SubmitPage.dynamicRadioAnswer()).getText()).to.equal("No answer provided"); - expect($(SubmitPage.dynamicDropdownAnswer()).getText()).to.equal("No answer provided"); - expect($(SubmitPage.dynamicMutuallyExclusiveDynamicAnswer()).getText()).to.equal("No answer provided"); + it("When I submit my questions without answering, then the summary should display `No answer provided` for each question", async () => { + await click(DynamicCheckboxPage.submit()); + await click(DynamicRadioPage.submit()); + await click(DynamicRadioPage.submit()); + await click(DynamicMutuallyExclusivePage.submit()); + + await verifyUrlContains(SubmitPage.pageName); + await expect(await $(SubmitPage.dynamicCheckboxAnswer()).getText()).toBe("No answer provided"); + await expect(await $(SubmitPage.dynamicRadioAnswer()).getText()).toBe("No answer provided"); + await expect(await $(SubmitPage.dynamicDropdownAnswer()).getText()).toBe("No answer provided"); + await expect(await $(SubmitPage.dynamicMutuallyExclusiveDynamicAnswer()).getText()).toBe("No answer provided"); }); - it("When I select a dynamically generated answer option for each question, then my selected answers should be displayed on the summary", () => { + it("When I select a dynamically generated answer option for each question, then my selected answers should be displayed on the summary", async () => { // Answer Checkbox - $(DynamicCheckboxPage.answerByIndex(2)).click(); // Wednesday 30 December 2020 - $(DynamicCheckboxPage.answerByIndex(3)).click(); // Thursday 30 December 2020 - $(DynamicCheckboxPage.submit()).click(); + await $(DynamicCheckboxPage.answerByIndex(2)).click(); // Wednesday 30 December 2020 + await $(DynamicCheckboxPage.answerByIndex(3)).click(); // Thursday 30 December 2020 + await click(DynamicCheckboxPage.submit()); // Answer Radio - $(DynamicRadioPage.answerByIndex(1)).click(); // Tuesday 29 December 2020 - $(DynamicRadioPage.submit()).click(); + await $(DynamicRadioPage.answerByIndex(1)).click(); // Tuesday 29 December 2020 + await click(DynamicRadioPage.submit()); // Answer Dropdown - $(DynamicDropdownPage.answer()).selectByAttribute("value", "2021-01-02"); // Saturday 2 January 2021 - $(DynamicDropdownPage.submit()).click(); + await $(DynamicDropdownPage.answer()).selectByAttribute("value", "2021-01-02"); // Saturday 2 January 2021 + await click(DynamicDropdownPage.submit()); // Answer Mutually exclusive - $(DynamicMutuallyExclusivePage.answerByIndex(0)).click(); // Monday 28 December 2020 - $(DynamicMutuallyExclusivePage.answerByIndex(6)).click(); // Sunday 3 January 2021 - $(DynamicMutuallyExclusivePage.submit()).click(); - - expect(browser.getUrl()).to.contain(SubmitPage.pageName); - expect($(SubmitPage.dynamicCheckboxAnswer()).getText()).to.equal("Wednesday 30 December 2020\nThursday 31 December 2020"); - expect($(SubmitPage.dynamicRadioAnswer()).getText()).to.equal("Tuesday 29 December 2020"); - expect($(SubmitPage.dynamicDropdownAnswer()).getText()).to.equal("Saturday 2 January 2021"); - expect($(SubmitPage.dynamicMutuallyExclusiveDynamicAnswer()).getText()).to.equal("Monday 28 December 2020\nSunday 3 January 2021"); + await $(DynamicMutuallyExclusivePage.answerByIndex(0)).click(); // Monday 28 December 2020 + await $(DynamicMutuallyExclusivePage.answerByIndex(6)).click(); // Sunday 3 January 2021 + await click(DynamicMutuallyExclusivePage.submit()); + + await verifyUrlContains(SubmitPage.pageName); + await expect(await $(SubmitPage.dynamicCheckboxAnswer()).getText()).toBe("Wednesday 30 December 2020\nThursday 31 December 2020"); + await expect(await $(SubmitPage.dynamicRadioAnswer()).getText()).toBe("Tuesday 29 December 2020"); + await expect(await $(SubmitPage.dynamicDropdownAnswer()).getText()).toBe("Saturday 2 January 2021"); + await expect(await $(SubmitPage.dynamicMutuallyExclusiveDynamicAnswer()).getText()).toBe("Monday 28 December 2020\nSunday 3 January 2021"); }); }); }); @@ -166,67 +166,67 @@ testCases.forEach((testCase) => { describe(`Feature: Dynamically generated answer options driven by a function with static options`, () => { describe("Given a dynamic answer options questionnaire with static options", () => { - before("Open questionnaire", () => { - openQuestionnaireAndSetUp("test_dynamic_answer_options_function_driven_with_static_options.json"); + before("Open questionnaire", async () => { + await openQuestionnaireAndSetUp("test_dynamic_answer_options_function_driven_with_static_options.json"); }); - it("When I select a static answer option for each question, then my selected answer(s) should be displayed on the summary", () => { + it("When I select a static answer option for each question, then my selected answer(s) should be displayed on the summary", async () => { // Answer Checkbox - $(DynamicCheckboxPage.answerByIndex(7)).click(); - $(DynamicCheckboxPage.submit()).click(); + await $(DynamicCheckboxPage.answerByIndex(7)).click(); + await click(DynamicCheckboxPage.submit()); // Answer Radio - $(DynamicRadioPage.answerByIndex(7)).click(); - $(DynamicRadioPage.submit()).click(); + await $(DynamicRadioPage.answerByIndex(7)).click(); + await click(DynamicRadioPage.submit()); // Answer Dropdown - $(DynamicDropdownPage.answer()).selectByAttribute("value", "I did not work"); - $(DynamicDropdownPage.submit()).click(); + await $(DynamicDropdownPage.answer()).selectByAttribute("value", "I did not work"); + await click(DynamicDropdownPage.submit()); // Answer Mutually exclusive // Test static option for mutually exclusive from non exclusive choices - $(DynamicMutuallyExclusivePage.answerByIndex(7)).click(); - $(DynamicMutuallyExclusivePage.submit()).click(); - expect($(SubmitPage.dynamicMutuallyExclusiveDynamicAnswer()).getText()).to.equal("None of the above"); + await $(DynamicMutuallyExclusivePage.answerByIndex(7)).click(); + await click(DynamicMutuallyExclusivePage.submit()); + await expect(await $(SubmitPage.dynamicMutuallyExclusiveDynamicAnswer()).getText()).toBe("None of the above"); // Test exclusive static choice - $(SubmitPage.previous()).click(); - $(DynamicMutuallyExclusivePage.staticIDidNotWork()).click(); - $(DynamicMutuallyExclusivePage.submit()).click(); - - expect(browser.getUrl()).to.contain(SubmitPage.pageName); - expect($(SubmitPage.dynamicCheckboxAnswer()).getText()).to.equal("I did not work"); - expect($(SubmitPage.dynamicRadioAnswer()).getText()).to.equal("I did not work"); - expect($(SubmitPage.dynamicDropdownAnswer()).getText()).to.equal("I did not work"); - expect($(SubmitPage.dynamicMutuallyExclusiveStaticAnswer()).getText()).to.equal("I did not work"); + await $(SubmitPage.previous()).click(); + await $(DynamicMutuallyExclusivePage.staticIDidNotWork()).click(); + await click(DynamicMutuallyExclusivePage.submit()); + + await verifyUrlContains(SubmitPage.pageName); + await expect(await $(SubmitPage.dynamicCheckboxAnswer()).getText()).toBe("I did not work"); + await expect(await $(SubmitPage.dynamicRadioAnswer()).getText()).toBe("I did not work"); + await expect(await $(SubmitPage.dynamicDropdownAnswer()).getText()).toBe("I did not work"); + await expect(await $(SubmitPage.dynamicMutuallyExclusiveStaticAnswer()).getText()).toBe("I did not work"); }); - it("When I edit and change the reference date which the other questions are dependent on, then all dependent answers are removed", () => { - $(SubmitPage.referenceDateAnswerEdit()).click(); - $(ReferenceDatePage.day()).setValue("2"); - $(ReferenceDatePage.submit()).click(); + it("When I edit and change the reference date which the other questions are dependent on, then all dependent answers are removed", async () => { + await $(SubmitPage.referenceDateAnswerEdit()).click(); + await $(ReferenceDatePage.day()).setValue("2"); + await click(ReferenceDatePage.submit()); - expect($(DynamicCheckboxPage.answerByIndex(7)).isSelected()).to.be.false; + await expect(await $(DynamicCheckboxPage.answerByIndex(7)).isSelected()).toBe(false); - $(DynamicCheckboxPage.answerByIndex(7)).click(); - $(DynamicCheckboxPage.submit()).click(); + await $(DynamicCheckboxPage.answerByIndex(7)).click(); + await click(DynamicCheckboxPage.submit()); - expect($(DynamicRadioPage.answerByIndex(7)).isSelected()).to.be.false; + await expect(await $(DynamicRadioPage.answerByIndex(7)).isSelected()).toBe(false); - $(DynamicRadioPage.answerByIndex(7)).click(); - $(DynamicRadioPage.submit()).click(); + await $(DynamicRadioPage.answerByIndex(7)).click(); + await click(DynamicRadioPage.submit()); - expect($(DynamicDropdownPage.answer()).getText()).to.contain("Select an answer"); + await expect(await $(DynamicDropdownPage.answer()).getText()).toContain("Select an answer"); - $(DynamicDropdownPage.answer()).selectByAttribute("value", "I did not work"); - $(DynamicDropdownPage.submit()).click(); + await $(DynamicDropdownPage.answer()).selectByAttribute("value", "I did not work"); + await click(DynamicDropdownPage.submit()); // The Mutually exclusive answer is not removed as it is a different answer_id to the dependent, however the block must be re-submitted - expect($(DynamicMutuallyExclusivePage.staticIDidNotWork()).isSelected()).to.be.true; - $(DynamicMutuallyExclusivePage.submit()).click(); + await expect(await $(DynamicMutuallyExclusivePage.staticIDidNotWork()).isSelected()).toBe(true); + await click(DynamicMutuallyExclusivePage.submit()); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); + await verifyUrlContains(SubmitPage.pageName); }); }); }); diff --git a/tests/functional/spec/features/dynamic_answer_options/function_driven_mandatory.js b/tests/functional/spec/features/dynamic_answer_options/function_driven_mandatory.js index 6aa6260a88..d88ac70d7e 100644 --- a/tests/functional/spec/features/dynamic_answer_options/function_driven_mandatory.js +++ b/tests/functional/spec/features/dynamic_answer_options/function_driven_mandatory.js @@ -3,56 +3,56 @@ import DynamicCheckboxPage from "../../../generated_pages/dynamic_answer_options import DynamicRadioPage from "../../../generated_pages/dynamic_answer_options_function_driven_with_static_options_mandatory/dynamic-radio.page"; import DynamicDropdownPage from "../../../generated_pages/dynamic_answer_options_function_driven_with_static_options_mandatory/dynamic-dropdown.page"; import DynamicMutuallyExclusivePage from "../../../generated_pages/dynamic_answer_options_function_driven_with_static_options_mandatory/dynamic-mutually-exclusive.page"; - +import { click } from "../../../helpers"; describe(`Feature: Dynamically generated mandatory answer options driven by a function with static options`, () => { describe("Given a mandatory dynamic answer options questionnaire with static options", () => { - before("Open questionnaire", () => { - browser.openQuestionnaire("test_dynamic_answer_options_function_driven_with_static_options_mandatory.json"); + before("Open questionnaire", async () => { + await browser.openQuestionnaire("test_dynamic_answer_options_function_driven_with_static_options_mandatory.json"); // Set reference date - $(ReferenceDatePage.day()).setValue("1"); - $(ReferenceDatePage.month()).setValue("1"); - $(ReferenceDatePage.year()).setValue("2021"); - $(ReferenceDatePage.submit()).click(); + await $(ReferenceDatePage.day()).setValue("1"); + await $(ReferenceDatePage.month()).setValue("1"); + await $(ReferenceDatePage.year()).setValue("2021"); + await click(ReferenceDatePage.submit()); }); - it("When I do not answer the Checkbox question and submit, then an error message and the question error panel should be displayed.", () => { - $(DynamicCheckboxPage.submit()).click(); - expect($(DynamicCheckboxPage.errorHeader()).getText()).to.contain("There is a problem with your answer"); - expect($(DynamicCheckboxPage.answerErrorItem()).getText()).to.contain("Select at least one answer"); - expect($(DynamicCheckboxPage.questionErrorPanel()).isExisting()).to.be.true; + it("When I do not answer the Checkbox question and submit, then an error message and the question error panel should be displayed.", async () => { + await click(DynamicCheckboxPage.submit()); + await expect(await $(DynamicCheckboxPage.errorHeader()).getText()).toBe("There is a problem with your answer"); + await expect(await $(DynamicCheckboxPage.answerErrorItem()).getText()).toContain("Select at least one answer"); + await expect(await $(DynamicCheckboxPage.questionErrorPanel()).isExisting()).toBe(true); }); - it("When I do not answer the Radio question and submit, then an error message and the question error panel should be displayed.", () => { + it("When I do not answer the Radio question and submit, then an error message and the question error panel should be displayed.", async () => { // Get to Radio question - $(DynamicCheckboxPage.answerByIndex(0)).click(); - $(DynamicCheckboxPage.submit()).click(); + await $(DynamicCheckboxPage.answerByIndex(0)).click(); + await click(DynamicCheckboxPage.submit()); - $(DynamicRadioPage.submit()).click(); - expect($(DynamicRadioPage.errorHeader()).getText()).to.contain("There is a problem with your answer"); - expect($(DynamicRadioPage.answerErrorItem()).getText()).to.contain("Select an answer"); - expect($(DynamicRadioPage.questionErrorPanel()).isExisting()).to.be.true; + await click(DynamicRadioPage.submit()); + await expect(await $(DynamicRadioPage.errorHeader()).getText()).toBe("There is a problem with your answer"); + await expect(await $(DynamicRadioPage.answerErrorItem()).getText()).toContain("Select an answer"); + await expect(await $(DynamicRadioPage.questionErrorPanel()).isExisting()).toBe(true); }); - it("When I do not answer the Dropdown question and submit, then an error message and the question error panel should be displayed.", () => { + it("When I do not answer the Dropdown question and submit, then an error message and the question error panel should be displayed.", async () => { // Get to Dropdown question - $(DynamicRadioPage.answerByIndex(0)).click(); - $(DynamicRadioPage.submit()).click(); + await $(DynamicRadioPage.answerByIndex(0)).click(); + await click(DynamicRadioPage.submit()); - $(DynamicDropdownPage.submit()).click(); - expect($(DynamicDropdownPage.errorHeader()).getText()).to.contain("There is a problem with your answer"); - expect($(DynamicDropdownPage.answerErrorItem()).getText()).to.contain("Select an answer"); - expect($(DynamicDropdownPage.questionErrorPanel()).isExisting()).to.be.true; + await click(DynamicDropdownPage.submit()); + await expect(await $(DynamicDropdownPage.errorHeader()).getText()).toBe("There is a problem with your answer"); + await expect(await $(DynamicDropdownPage.answerErrorItem()).getText()).toBe("Select an answer"); + await expect(await $(DynamicDropdownPage.questionErrorPanel()).isExisting()).toBe(true); }); - it("When I do not answer the Mutually Exclusive Checkbox question and submit, then an error message and the question error panel should be displayed.", () => { + it("When I do not answer the Mutually Exclusive Checkbox question and submit, then an error message and the question error panel should be displayed.", async () => { // Get to Mutually Exclusive question - $(DynamicDropdownPage.answer()).selectByAttribute("value", "2021-01-02"); - $(DynamicDropdownPage.submit()).click(); + await $(DynamicDropdownPage.answer()).selectByAttribute("value", "2021-01-02"); + await click(DynamicDropdownPage.submit()); - $(DynamicMutuallyExclusivePage.submit()).click(); - expect($(DynamicMutuallyExclusivePage.errorHeader()).getText()).to.contain("There is a problem with your answer"); - expect($(DynamicMutuallyExclusivePage.errorNumber(1)).getText()).to.contain("Select at least one answer"); - expect($(DynamicMutuallyExclusivePage.questionErrorPanel()).isExisting()).to.be.true; + await click(DynamicMutuallyExclusivePage.submit()); + await expect(await $(DynamicMutuallyExclusivePage.errorHeader()).getText()).toBe("There is a problem with your answer"); + await expect(await $(DynamicMutuallyExclusivePage.errorNumber(1)).getText()).toContain("Select at least one answer"); + await expect(await $(DynamicMutuallyExclusivePage.questionErrorPanel()).isExisting()).toBe(true); }); }); }); diff --git a/tests/functional/spec/features/dynamic_answer_options/radio_options_from_checkbox_answers.js b/tests/functional/spec/features/dynamic_answer_options/radio_options_from_checkbox_answers.js index bf58c93895..f5992d173f 100644 --- a/tests/functional/spec/features/dynamic_answer_options/radio_options_from_checkbox_answers.js +++ b/tests/functional/spec/features/dynamic_answer_options/radio_options_from_checkbox_answers.js @@ -2,49 +2,49 @@ import HealedTheQuickestPage from "../../../generated_pages/dynamic_radio_option import InjurySustainedPage from "../../../generated_pages/dynamic_radio_options_from_checkbox/injury-sustained.page"; import MostSeriousInjuryPage from "../../../generated_pages/dynamic_radio_options_from_checkbox/most-serious-injury.page"; import SubmitPage from "../../../generated_pages/dynamic_radio_options_from_checkbox/submit.page"; - +import { click, verifyUrlContains } from "../../../helpers"; describe("Dynamic radio options from checkbox answers", () => { describe("Given the dynamic radio options from checkbox questionnaire and I am on the checkbox answer page", () => { - it("When the respondent answers the checkbox question and submits, Then the radio question should show the answers from that checkbox as options, as well as a static option", () => { - browser.openQuestionnaire("test_dynamic_radio_options_from_checkbox.json"); - $(InjurySustainedPage.head()).click(); - $(InjurySustainedPage.body()).click(); - $(InjurySustainedPage.submit()).click(); - - expect(browser.getUrl()).to.contain(MostSeriousInjuryPage.pageName); - expect($(MostSeriousInjuryPage.answerLabelByIndex(0)).getText()).to.contain("Head"); - expect($(MostSeriousInjuryPage.answerLabelByIndex(1)).getText()).to.contain("Body"); - expect($(MostSeriousInjuryPage.answerLabelByIndex(2)).getText()).to.contain("They were of equal severity (static option)"); - expect($(MostSeriousInjuryPage.answerLabelByIndex(3)).isExisting()).to.be.false; + it("When the respondent answers the checkbox question and submits, Then the radio question should show the answers from that checkbox as options, as well as a static option", async () => { + await browser.openQuestionnaire("test_dynamic_radio_options_from_checkbox.json"); + await $(InjurySustainedPage.head()).click(); + await $(InjurySustainedPage.body()).click(); + await click(InjurySustainedPage.submit()); + + await verifyUrlContains(MostSeriousInjuryPage.pageName); + await expect(await $(MostSeriousInjuryPage.answerLabelByIndex(0)).getText()).toBe("Head"); + await expect(await $(MostSeriousInjuryPage.answerLabelByIndex(1)).getText()).toBe("Body"); + await expect(await $(MostSeriousInjuryPage.answerLabelByIndex(2)).getText()).toBe("They were of equal severity (static option)"); + await expect(await $(MostSeriousInjuryPage.answerLabelByIndex(3)).isExisting()).toBe(false); }); - it("When the respondent answers the radio question and submits, Then the next radio question should show only the answers from the first checkbox as options", () => { - $(MostSeriousInjuryPage.answerLabelByIndex(0)).click(); - $(MostSeriousInjuryPage.submit()).click(); + it("When the respondent answers the radio question and submits, Then the next radio question should show only the answers from the first checkbox as options", async () => { + await $(MostSeriousInjuryPage.answerLabelByIndex(0)).click(); + await click(MostSeriousInjuryPage.submit()); - expect(browser.getUrl()).to.contain(HealedTheQuickestPage.pageName); - expect($(HealedTheQuickestPage.answerLabelByIndex(0)).getText()).to.contain("Head"); - expect($(HealedTheQuickestPage.answerLabelByIndex(1)).getText()).to.contain("Body"); - expect($(HealedTheQuickestPage.answerLabelByIndex(2)).isExisting()).to.be.false; + await verifyUrlContains(HealedTheQuickestPage.pageName); + await expect(await $(HealedTheQuickestPage.answerLabelByIndex(0)).getText()).toBe("Head"); + await expect(await $(HealedTheQuickestPage.answerLabelByIndex(1)).getText()).toBe("Body"); + await expect(await $(HealedTheQuickestPage.answerLabelByIndex(2)).isExisting()).toBe(false); }); - it("When the respondent answers the radio question and submits, then the summary should display all the answers correctly", () => { - $(HealedTheQuickestPage.answerLabelByIndex(1)).click(); - $(HealedTheQuickestPage.submit()).click(); + it("When the respondent answers the radio question and submits, then the summary should display all the answers correctly", async () => { + await $(HealedTheQuickestPage.answerLabelByIndex(1)).click(); + await click(HealedTheQuickestPage.submit()); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); - expect($(SubmitPage.injurySustainedAnswer()).getText()).to.contain("Head\nBody"); - expect($(SubmitPage.mostSeriousInjuryAnswer()).getText()).to.contain("Head"); - expect($(SubmitPage.healedTheQuickestAnswer()).getText()).to.contain("Body"); + await verifyUrlContains(SubmitPage.pageName); + await expect(await $(SubmitPage.injurySustainedAnswer()).getText()).toBe("Head\nBody"); + await expect(await $(SubmitPage.mostSeriousInjuryAnswer()).getText()).toBe("Head"); + await expect(await $(SubmitPage.healedTheQuickestAnswer()).getText()).toBe("Body"); }); - it("When I edit and change the answer which the dynamic options is dependent on, then my selected answers are removed", () => { - $(SubmitPage.injurySustainedAnswerEdit()).click(); - $(InjurySustainedPage.arms()).click(); - $(InjurySustainedPage.submit()).click(); + it("When I edit and change the answer which the dynamic options is dependent on, then my selected answers are removed", async () => { + await $(SubmitPage.injurySustainedAnswerEdit()).click(); + await $(InjurySustainedPage.arms()).click(); + await click(InjurySustainedPage.submit()); - expect($(MostSeriousInjuryPage.answerByIndex(0)).isSelected()).to.be.false; - expect($(MostSeriousInjuryPage.answerByIndex(1)).isSelected()).to.be.false; + await expect(await $(MostSeriousInjuryPage.answerByIndex(0)).isSelected()).toBe(false); + await expect(await $(MostSeriousInjuryPage.answerByIndex(1)).isSelected()).toBe(false); }); }); }); diff --git a/tests/functional/spec/features/dynamic_answers_list_value_source.spec.js b/tests/functional/spec/features/dynamic_answers_list_value_source.spec.js new file mode 100644 index 0000000000..41f64feb97 --- /dev/null +++ b/tests/functional/spec/features/dynamic_answers_list_value_source.spec.js @@ -0,0 +1,209 @@ +import DriverPage from "../../generated_pages/dynamic_answers_list_source/any-supermarket.page"; +import DynamicAnswerPage from "../../generated_pages/dynamic_answers_list_source/dynamic-answer.page"; +import DynamicAnswerOnlyPage from "../../generated_pages/dynamic_answers_list_source/dynamic-answer-only.page"; +import ListCollectorPage from "../../generated_pages/dynamic_answers_list_source/list-collector.page"; +import ListCollectorAddPage from "../../generated_pages/dynamic_answers_list_source/list-collector-add.page"; +import ListCollectorRemovePage from "../../generated_pages/dynamic_answers_list_source/list-collector-remove.page"; +import SetMinimumPage from "../../generated_pages/dynamic_answers_list_source/minimum-spending.page"; +import SectionSummaryPage from "../../generated_pages/dynamic_answers_list_source/list-collector-section-summary.page"; +import HubPage from "../../base_pages/hub.page"; +import OnlineShoppingPage from "../../generated_pages/dynamic_answers_list_source/dynamic-answer-separate-section.page"; +import { click, verifyUrlContains } from "../../helpers"; +import { expect } from "@wdio/globals"; + +describe("Dynamic answers list value source", () => { + const summaryTitles = ".ons-summary__item-title"; + const summaryValues = ".ons-summary__values"; + const summaryActions = ".ons-summary__actions"; + beforeEach("Load the survey", async () => { + await browser.openQuestionnaire("test_dynamic_answers_list_source.json"); + }); + + it("Given list items have been added, When the dynamic answers are displayed, Then the correct answers should be visible", async () => { + await addTwoSupermarkets(); + await expect(await $$(DynamicAnswerPage.labels())[0].getText()).toBe("Percentage of shopping at Tesco"); + await expect(await $$(DynamicAnswerPage.labels())[1].getText()).toBe("Percentage of shopping at Aldi"); + await expect(await $$(DynamicAnswerPage.labels()).length).toBe(4); + }); + it("Given list items have been added, When additional items are added using add link, Then the correct dynamic answers are displayed", async () => { + await $(DriverPage.yes()).click(); + await click(DriverPage.submit()); + await $(ListCollectorAddPage.supermarketName()).setValue("Tesco"); + await $(ListCollectorAddPage.setMaximum()).setValue(10000); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await expect(await $$(DynamicAnswerPage.labels())[0].getText()).toBe("Percentage of shopping at Tesco"); + await expect(await $$(DynamicAnswerPage.labels()).length).toBe(2); + await setMinimumAndGetSectionSummary(); + await $(SectionSummaryPage.supermarketsListAddLink()).click(); + await $(ListCollectorAddPage.supermarketName()).setValue("Aldi"); + await $(ListCollectorAddPage.setMaximum()).setValue(10000); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await expect(await $$(DynamicAnswerPage.labels())[0].getText()).toBe("Percentage of shopping at Tesco"); + await expect(await $$(DynamicAnswerPage.labels())[1].getText()).toBe("Percentage of shopping at Aldi"); + await expect(await $$(DynamicAnswerPage.labels()).length).toBe(4); + }); + it("Given list items have been added and the dynamic answers are submitted, When the summary is displayed, Then the correct answers should be visible and have correct values", async () => { + await addTwoSupermarkets(); + await $$(DynamicAnswerPage.inputs())[0].setValue(12); + await $$(DynamicAnswerPage.inputs())[1].setValue(21); + await $$(DynamicAnswerPage.inputs())[2].setValue(3); + await $$(DynamicAnswerPage.inputs())[3].setValue(7); + await setMinimumAndGetSectionSummary(); + await expect(await $(SectionSummaryPage.listCollectorGroupContent(2)).$$(summaryTitles)[0].getText()).toBe("Percentage of shopping at Tesco"); + await expect(await $(SectionSummaryPage.listCollectorGroupContent(2)).$$(summaryValues)[0].getText()).toBe("12%"); + await expect(await $(SectionSummaryPage.listCollectorGroupContent(2)).$$(summaryTitles)[1].getText()).toBe("Percentage of shopping at Aldi"); + await expect(await $(SectionSummaryPage.listCollectorGroupContent(2)).$$(summaryValues)[1].getText()).toBe("21%"); + await expect(await $(SectionSummaryPage.listCollectorGroupContent(2)).$$(summaryValues)[2].getText()).toBe("3"); + await expect(await $(SectionSummaryPage.listCollectorGroupContent(2)).$$(summaryValues)[3].getText()).toBe("7"); + await expect(await $(SectionSummaryPage.listCollectorGroupContent(2)).$$(summaryTitles).length).toBe(8); + await expect(await $(SectionSummaryPage.listCollectorGroupContent(2)).$$(summaryValues).length).toBe(8); + }); + it("Given list items have been added and the dynamic answers are submitted, When the dynamic answers are revisited, Then they should be visible and have correct values", async () => { + await addTwoSupermarkets(); + await $$(DynamicAnswerPage.inputs())[0].setValue(12); + await $$(DynamicAnswerPage.inputs())[1].setValue(21); + await setMinimumAndGetSectionSummary(); + await $(SectionSummaryPage.previous()).click(); + await $(DynamicAnswerOnlyPage.previous()).click(); + await $(SetMinimumPage.previous()).click(); + await verifyUrlContains(DynamicAnswerPage.pageName); + await expect(await $$(DynamicAnswerPage.inputs())[0].getValue()).toBe("12"); + await expect(await $$(DynamicAnswerPage.inputs())[1].getValue()).toBe("21"); + await expect(await $$(DynamicAnswerPage.labels())[0].getText()).toBe("Percentage of shopping at Tesco"); + await expect(await $$(DynamicAnswerPage.labels())[1].getText()).toBe("Percentage of shopping at Aldi"); + }); + it("Given list items have been added and the dynamic answers are submitted, When the dynamic answers are resubmitted with different values, Then they should be displayed correctly on summary", async () => { + await addTwoSupermarkets(); + await $$(DynamicAnswerPage.inputs())[0].setValue(12); + await $$(DynamicAnswerPage.inputs())[1].setValue(21); + await setMinimumAndGetSectionSummary(); + await $(SectionSummaryPage.previous()).click(); + await $(DynamicAnswerOnlyPage.previous()).click(); + await $(SetMinimumPage.previous()).click(); + await $$(DynamicAnswerPage.inputs())[0].setValue(21); + await $$(DynamicAnswerPage.inputs())[1].setValue(12); + await click(DynamicAnswerPage.submit()); + await expect(await $(SectionSummaryPage.listCollectorGroupContent(2)).$$(summaryValues)[0].getText()).toBe("21%"); + await expect(await $(SectionSummaryPage.listCollectorGroupContent(2)).$$(summaryValues)[1].getText()).toBe("12%"); + }); + it("Given list items have been added and the dynamic answers are submitted, When the summary edit answer link is used for dynamic answer, Then the focus is on correct answer option", async () => { + await addTwoSupermarkets(); + await $$(DynamicAnswerPage.inputs())[0].setValue(12); + await $$(DynamicAnswerPage.inputs())[1].setValue(21); + await setMinimumAndGetSectionSummary(); + await $(SectionSummaryPage.listCollectorGroupContent(2)).$$(summaryActions)[0].$("a").click(); + await verifyUrlContains(DynamicAnswerPage.pageName); + await expect(await $$(DynamicAnswerPage.inputs())[0].isFocused()).toBe(true); + await click(DynamicAnswerPage.submit()); + await $(SectionSummaryPage.listCollectorGroupContent(2)).$$(summaryActions)[1].$("a").click(); + await verifyUrlContains(DynamicAnswerPage.pageName); + await expect(await $$(DynamicAnswerPage.inputs())[1].isFocused()).toBe(true); + }); + it("Given list items have been added and the dynamic answers are submitted, When the dynamic answers are resubmitted with answers updated, Then they should be displayed correctly on summary", async () => { + await addTwoSupermarkets(); + await $$(DynamicAnswerPage.inputs())[0].setValue(12); + await $$(DynamicAnswerPage.inputs())[1].setValue(21); + await setMinimumAndGetSectionSummary(); + await $(SectionSummaryPage.listCollectorGroupContent(2)).$$(summaryActions)[0].$("a").click(); + await $$(DynamicAnswerPage.inputs())[0].setValue(21); + await click(DynamicAnswerPage.submit()); + await expect(await $(SectionSummaryPage.listCollectorGroupContent(2)).$$(summaryValues)[0].getText()).toBe("21%"); + await expect(await $(SectionSummaryPage.listCollectorGroupContent(2)).$$(summaryValues)[1].getText()).toBe("21%"); + }); + it("Given list items have been added and the dynamic answers are submitted, When the list items are removed and answers updated, Then they should be displayed correctly on summary", async () => { + await addTwoSupermarkets(); + await $$(DynamicAnswerPage.inputs())[0].setValue(12); + await $$(DynamicAnswerPage.inputs())[1].setValue(21); + await setMinimumAndGetSectionSummary(); + await $(SectionSummaryPage.supermarketsListRemoveLink(1)).click(); + await $(ListCollectorRemovePage.yes()).click(); + await click(ListCollectorRemovePage.submit()); + await click(DynamicAnswerPage.submit()); + await click(DynamicAnswerOnlyPage.submit()); + await verifyUrlContains(SectionSummaryPage.pageName); + await expect(await $(SectionSummaryPage.listCollectorGroupContent(2)).$$(summaryTitles)[0].getText()).toBe("Percentage of shopping at Aldi"); + await expect(await $(SectionSummaryPage.listCollectorGroupContent(2)).$$(summaryValues)[0].getText()).toBe("21%"); + await expect(await $(SectionSummaryPage.listCollectorGroupContent(2)).$$(summaryTitles).length).toBe(5); + await expect(await $(SectionSummaryPage.listCollectorGroupContent(2)).$$(summaryValues).length).toBe(5); + }); + it("Given list items have been added and the dynamic answers are submitted, When the driving question is changed to 'No' and subsequently changed back to 'Yes', Then all answers should re-appear on summary", async () => { + await addTwoSupermarkets(); + await $$(DynamicAnswerPage.inputs())[0].setValue(12); + await $$(DynamicAnswerPage.inputs())[1].setValue(21); + await $$(DynamicAnswerPage.inputs())[2].setValue(3); + await $$(DynamicAnswerPage.inputs())[3].setValue(7); + await setMinimumAndGetSectionSummary(); + await $(SectionSummaryPage.anySupermarketAnswerEdit()).click(); + await $(DriverPage.no()).click(); + await click(DriverPage.submit()); + await expect(await $("body").getText()).not.toBe("Percentage of shopping at Tesco"); + await expect(await $("body").getText()).not.toBe("Percentage of shopping at Aldi"); + await $(SectionSummaryPage.anySupermarketAnswerEdit()).click(); + await $(DriverPage.yes()).click(); + await click(DriverPage.submit()); + + await expect(await $(SectionSummaryPage.listCollectorGroupContent(2)).$$(summaryTitles)[0].getText()).toBe("Percentage of shopping at Tesco"); + await expect(await $(SectionSummaryPage.listCollectorGroupContent(2)).$$(summaryValues)[0].getText()).toBe("12%"); + await expect(await $(SectionSummaryPage.listCollectorGroupContent(2)).$$(summaryTitles)[1].getText()).toBe("Percentage of shopping at Aldi"); + await expect(await $(SectionSummaryPage.listCollectorGroupContent(2)).$$(summaryValues)[1].getText()).toBe("21%"); + await expect(await $(SectionSummaryPage.listCollectorGroupContent(2)).$$(summaryValues)[2].getText()).toBe("3"); + await expect(await $(SectionSummaryPage.listCollectorGroupContent(2)).$$(summaryValues)[3].getText()).toBe("7"); + await expect(await $(SectionSummaryPage.listCollectorGroupContent(2)).$$(summaryTitles).length).toBe(8); + await expect(await $(SectionSummaryPage.listCollectorGroupContent(2)).$$(summaryValues).length).toBe(8); + }); + + it("Given list items have been added, When the dynamic answers are displayed in a separate section, Then the correct answers should be visible", async () => { + await addTwoSupermarketsAndGetToNextSection(); + await expect(await $$(OnlineShoppingPage.labels())[0].getText()).toBe("Percentage of online shopping at Tesco"); + await expect(await $$(OnlineShoppingPage.labels())[1].getText()).toBe("Percentage of online shopping at Aldi"); + await expect(await $$(OnlineShoppingPage.labels()).length).toBe(4); + }); +}); + +async function addTwoSupermarkets() { + await $(DriverPage.yes()).click(); + await click(DriverPage.submit()); + await $(ListCollectorAddPage.supermarketName()).setValue("Tesco"); + await $(ListCollectorAddPage.setMaximum()).setValue(10000); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.supermarketName()).setValue("Aldi"); + await $(ListCollectorAddPage.setMaximum()).setValue(10000); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); +} + +async function addTwoSupermarketsAndGetToNextSection() { + await $(DriverPage.yes()).click(); + await click(DriverPage.submit()); + await $(ListCollectorAddPage.supermarketName()).setValue("Tesco"); + await $(ListCollectorAddPage.setMaximum()).setValue(10000); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.supermarketName()).setValue("Aldi"); + await $(ListCollectorAddPage.setMaximum()).setValue(10000); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await $$(DynamicAnswerPage.inputs())[0].setValue(12); + await $$(DynamicAnswerPage.inputs())[1].setValue(21); + await $$(DynamicAnswerPage.inputs())[2].setValue(3); + await $$(DynamicAnswerPage.inputs())[3].setValue(7); + await setMinimumAndGetSectionSummary(); + await click(SectionSummaryPage.submit()); + await click(HubPage.submit()); +} + +async function setMinimumAndGetSectionSummary() { + await click(DynamicAnswerPage.submit()); + await $(SetMinimumPage.setMinimum()).setValue(2); + await click(SetMinimumPage.submit()); + await click(DynamicAnswerOnlyPage.submit()); +} diff --git a/tests/functional/spec/features/enabled-sections/enabled_section_checkbox.spec.js b/tests/functional/spec/features/enabled-sections/enabled_section_checkbox.spec.js deleted file mode 100644 index 07e952ad97..0000000000 --- a/tests/functional/spec/features/enabled-sections/enabled_section_checkbox.spec.js +++ /dev/null @@ -1,42 +0,0 @@ -import sectionOne from "../../../generated_pages/section_enabled_checkbox/section-1-block.page"; -import sectionTwo from "../../../generated_pages/section_enabled_checkbox/section-2-block.page"; -import SubmitPage from "../../../generated_pages/section_enabled_checkbox/submit.page"; - -describe("Feature: Section Enabled Based On Checkbox Answers", () => { - beforeEach("Open survey", () => { - browser.openQuestionnaire("test_new_section_enabled_checkbox.json"); - }); - - it("When the user selects `Section 2` and submits, Then section 2 should be displayed", () => { - $(sectionOne.section1Section2()).click(); - $(sectionOne.submit()).click(); - - expect(browser.getUrl()).to.contain("section-2-block"); - }); - - it("When the user selects `Section 3` and submits, Then section 2 should not be displayed and section 3 should be displayed", () => { - $(sectionOne.section1Section3()).click(); - $(sectionOne.submit()).click(); - - expect(browser.getUrl()).to.contain("section-3-block"); - }); - - it("When the user selects `Section 2` and `Section 3` and submits, Then section 2 and section 3 should be displayed", () => { - $(sectionOne.section1Section2()).click(); - $(sectionOne.section1Section3()).click(); - $(sectionOne.submit()).click(); - - expect(browser.getUrl()).to.contain("section-2-block"); - $(sectionTwo.submit()).click(); - expect(browser.getUrl()).to.contain("section-3-block"); - }); - - it("When the user selects `Neither` and submits, Then they should be taken straight to the summary", () => { - $(sectionOne.section1ExclusiveNeither()).click(); - $(sectionOne.submit()).click(); - - expect(browser.getUrl()).to.contain(SubmitPage.url()); - expect($(SubmitPage.section2Question()).isExisting()).to.be.false; - expect($(SubmitPage.section3Question()).isExisting()).to.be.false; - }); -}); diff --git a/tests/functional/spec/features/enabled-sections/enabled_section_hub.spec.js b/tests/functional/spec/features/enabled-sections/enabled_section_hub.spec.js deleted file mode 100644 index 12e47f5a70..0000000000 --- a/tests/functional/spec/features/enabled-sections/enabled_section_hub.spec.js +++ /dev/null @@ -1,51 +0,0 @@ -import sectionOne from "../../../generated_pages/section_enabled_hub/section-1-block.page"; -import hubPage from "../../../base_pages/hub.page"; - -describe("Feature: Section Enabled With Hub", () => { - beforeEach("Open survey", () => { - browser.openQuestionnaire("test_new_section_enabled_hub.json"); - }); - - it("When the user selects `Section 2` and submits, Then only section 2 should be displayed on the hub", () => { - $(sectionOne.section1Section2()).click(); - $(sectionOne.submit()).click(); - - expect($(hubPage.summaryRowState("section-2")).isDisplayed()).to.be.true; - expect($(hubPage.summaryRowTitle("section-2")).getText()).to.equal("Section 2"); - - expect($(hubPage.summaryRowState("section-3")).isDisplayed()).to.be.false; - }); - - it("When the user selects `Section 3` and submits, Then section 2 should not be displayed and section 3 should be displayed", () => { - $(sectionOne.section1Section3()).click(); - $(sectionOne.submit()).click(); - - expect($(hubPage.summaryRowState("section-3")).isDisplayed()).to.be.true; - expect($(hubPage.summaryRowTitle("section-3")).getText()).to.equal("Section 3"); - - expect($(hubPage.summaryRowState("section-2")).isDisplayed()).to.be.false; - }); - - it("When the user selects `Section 2` and `Section 3` and submits, Then section 2 and section 3 should be displayed", () => { - $(sectionOne.section1Section2()).click(); - $(sectionOne.section1Section3()).click(); - $(sectionOne.submit()).click(); - - expect($(hubPage.summaryRowState("section-2")).isDisplayed()).to.be.true; - expect($(hubPage.summaryRowTitle("section-2")).getText()).to.equal("Section 2"); - - expect($(hubPage.summaryRowState("section-3")).isDisplayed()).to.be.true; - expect($(hubPage.summaryRowTitle("section-3")).getText()).to.equal("Section 3"); - }); - - it("When the user selects `Neither` and submits, Then hub should not display any other section and should be in the `Completed` state.", () => { - $(sectionOne.section1ExclusiveNeither()).click(); - $(sectionOne.submit()).click(); - - expect($(hubPage.summaryRowState("section-2")).isDisplayed()).to.be.false; - expect($(hubPage.summaryRowState("section-3")).isDisplayed()).to.be.false; - - expect($(hubPage.submit()).getText()).to.equal("Submit survey"); - expect($(hubPage.heading()).getText()).to.equal("Submit survey"); - }); -}); diff --git a/tests/functional/spec/features/enabled-sections/enabled_section_radio.spec.js b/tests/functional/spec/features/enabled-sections/enabled_section_radio.spec.js deleted file mode 100644 index b4f31ac356..0000000000 --- a/tests/functional/spec/features/enabled-sections/enabled_section_radio.spec.js +++ /dev/null @@ -1,41 +0,0 @@ -import sectionOne from "../../../generated_pages/section_enabled_radio/section-1-block.page"; -import SubmitPage from "../../../generated_pages/section_enabled_radio/submit.page"; - -describe("Feature: Section Enabled Based On Radio Answers", () => { - beforeEach("Open survey", () => { - browser.openQuestionnaire("test_new_section_enabled_radio.json"); - }); - - it("When the user answers `Yes, enable section 2` and submits, Then section 2 should be displayed", () => { - $(sectionOne.yesEnableSection2()).click(); - $(sectionOne.submit()).click(); - - expect(browser.getUrl()).to.contain("section-2-block"); - }); - - it("When the user answers `No, disable section 2` and submits, Then they should be taking straight to the summary", () => { - $(sectionOne.noDisableSection2()).click(); - $(sectionOne.submit()).click(); - - expect(browser.getUrl()).to.contain(SubmitPage.url()); - expect($(SubmitPage.section2Question()).isExisting()).to.be.false; - }); - - describe("Given that section 2 is enabled", () => { - beforeEach("Enable section 2", () => { - $(sectionOne.yesEnableSection2()).click(); - $(sectionOne.submit()).click(); - - expect(browser.getUrl()).to.contain("section-2-block"); - }); - - it("When the user changes the answers and disables section 2, Then they should be taken straight to the summary", () => { - browser.back(); - expect(browser.getUrl()).to.contain("section-1-block"); - - $(sectionOne.noDisableSection2()).click(); - $(sectionOne.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.url()); - }); - }); -}); diff --git a/tests/functional/spec/features/hub_and_spoke/choose_another_section.spec.js b/tests/functional/spec/features/hub_and_spoke/choose_another_section.spec.js deleted file mode 100644 index 80c8aa6605..0000000000 --- a/tests/functional/spec/features/hub_and_spoke/choose_another_section.spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import EmploymentStatusBlockPage from "../../../generated_pages/hub_and_spoke/employment-status.page.js"; -import ProxyPage from "../../../generated_pages/hub_and_spoke/proxy.page.js"; -import HubPage from "../../../base_pages/hub.page.js"; - -describe("Choose another section link", () => { - it("When a user first views the Hub, then the link should not be displayed", () => { - browser.openQuestionnaire("test_hub_and_spoke.json"); - expect($("body").getText()).to.not.have.string("Choose another section and return to this later"); - }); - - it("When a user views the first question and the hub is not available, then the link should not be displayed", () => { - browser.openQuestionnaire("test_hub_complete_sections.json"); - expect($("body").getText()).to.not.have.string("Choose another section and return to this later"); - }); - - it("When a user starts a new section and the hub is available, then the link should be displayed", () => { - browser.openQuestionnaire("test_hub_complete_sections.json"); - $(EmploymentStatusBlockPage.workingAsAnEmployee()).click(); - $(EmploymentStatusBlockPage.submit()).click(); - $(HubPage.summaryRowLink("accommodation-section")).click(); - expect($("body").getText()).to.contain("Choose another section and return to this later"); - }); - - it("When a user gets to a section summary and the hub is available, then the link should not be displayed", () => { - browser.openQuestionnaire("test_hub_complete_sections.json"); - $(EmploymentStatusBlockPage.workingAsAnEmployee()).click(); - $(EmploymentStatusBlockPage.submit()).click(); - $(HubPage.summaryRowLink("accommodation-section")).click(); - $(ProxyPage.noIMAnsweringForMyself()).click(); - $(ProxyPage.submit()).click(); - expect($("body").getText()).to.not.have.string("Choose another section and return to this later"); - }); -}); diff --git a/tests/functional/spec/features/hub_and_spoke/hub_and_spoke.spec.js b/tests/functional/spec/features/hub_and_spoke/hub_and_spoke.spec.js deleted file mode 100644 index 8210466263..0000000000 --- a/tests/functional/spec/features/hub_and_spoke/hub_and_spoke.spec.js +++ /dev/null @@ -1,257 +0,0 @@ -import AccomodationDetailsSummaryBlockPage from "../../../generated_pages/hub_and_spoke/accommodation-section-summary.page.js"; -import AnyoneRelated from "../../../generated_pages/hub_and_spoke/anyone-related.page.js"; -import DoesAnyoneLiveHere from "../../../generated_pages/hub_and_spoke/does-anyone-live-here.page.js"; -import EmploymentStatusBlockPage from "../../../generated_pages/hub_and_spoke/employment-status.page.js"; -import EmploymentTypeBlockPage from "../../../generated_pages/hub_and_spoke/employment-type.page.js"; -import HouseholdSummary from "../../../generated_pages/hub_and_spoke/household-section-summary.page.js"; -import HowManyPeopleLiveHere from "../../../generated_pages/hub_and_spoke/how-many-people-live-here.page.js"; -import HubPage from "../../../base_pages/hub.page.js"; -import ProxyPage from "../../../generated_pages/hub_and_spoke/proxy.page.js"; -import RelationshipsSummary from "../../../generated_pages/hub_and_spoke/relationships-section-summary.page.js"; - -describe("Feature: Hub and Spoke", () => { - const hubAndSpokeSchema = "test_hub_and_spoke.json"; - - describe("Given I am completing the test_hub_context schema,", () => { - beforeEach("load the survey", () => { - browser.openQuestionnaire(hubAndSpokeSchema); - }); - - it("When a user first views the Hub, The Hub should be in a continue state", () => { - expect($(HubPage.submit()).getText()).to.contain("Continue"); - expect($(HubPage.heading()).getText()).to.contain("Choose another section to complete"); - expect($(HubPage.summaryRowState("employment-section")).getText()).to.contain("Not started"); - expect($(HubPage.summaryRowState("accommodation-section")).getText()).to.contain("Not started"); - expect($(HubPage.summaryRowState("household-section")).getText()).to.contain("Not started"); - }); - - it("When a user views the Hub, any section with show_on_hub set to true should appear", () => { - expect($(HubPage.summaryItems()).getText()).to.contain("Employment"); - expect($(HubPage.summaryItems()).getText()).to.contain("Accommodation"); - expect($(HubPage.summaryItems()).getText()).to.contain("Household residents"); - }); - - it("When a user views the Hub, any section with show_on_hub set to false should not appear", () => { - expect($(HubPage.summaryItems()).getText()).not.to.contain("Relationships"); - }); - - it("When the user click the 'Save and sign out' button then they should be redirected to the correct log out url", () => { - $(HubPage.saveSignOut()).click(); - - const expectedUrl = browser.getUrl(); - - expect(expectedUrl).to.contain("/surveys/todo"); - }); - - it("When a user views the Hub, Then the page title should be Choose another section to complete", () => { - const pageTitle = browser.getTitle(); - expect(pageTitle).to.equal("Choose another section to complete - Hub & Spoke"); - }); - }); - - describe("Given a user has not started a section", () => { - beforeEach("Open survey", () => { - browser.openQuestionnaire(hubAndSpokeSchema); - expect($(HubPage.summaryRowState("employment-section")).getText()).to.contain("Not started"); - expect($(HubPage.summaryRowState("accommodation-section")).getText()).to.contain("Not started"); - expect($(HubPage.summaryRowState("household-section")).getText()).to.contain("Not started"); - }); - - it("When the user starts a section, Then the first question in the section should be displayed", () => { - $(HubPage.submit()).click(); - const expectedUrl = browser.getUrl(); - expect(expectedUrl).to.contain(EmploymentStatusBlockPage.url()); - }); - - it("When the user starts a section and clicks the Previous link on the first question, Then they should be taken back to the Hub", () => { - $(HubPage.submit()).click(); - $(EmploymentStatusBlockPage.previous()).click(); - const expectedUrl = browser.getUrl(); - expect(expectedUrl).to.contain(HubPage.url()); - }); - }); - - describe("Given a user has started a section", () => { - before("Start section", () => { - browser.openQuestionnaire(hubAndSpokeSchema); - $(HubPage.summaryRowLink("employment-section")).click(); - $(EmploymentStatusBlockPage.exclusiveNoneOfTheseApply()).click(); - $(EmploymentStatusBlockPage.submit()).click(); - }); - - it("When the user returns to the Hub, Then the Hub should be in a continue state", () => { - browser.url(HubPage.url()); - expect($(HubPage.submit()).getText()).to.contain("Continue"); - expect($(HubPage.heading()).getText()).to.contain("Choose another section to complete"); - }); - - it("When the user returns to the Hub, Then the section should be marked as 'Partially completed'", () => { - browser.url(HubPage.url()); - expect($(HubPage.summaryRowState("employment-section")).getText()).to.contain("Partially completed"); - }); - - it("When the user returns to the Hub and restarts the same section, Then they should be redirected to the first incomplete block", () => { - browser.url(HubPage.url()); - $(HubPage.summaryRowLink("employment-section")).click(); - const expectedUrl = browser.getUrl(); - expect(expectedUrl).to.contain(EmploymentTypeBlockPage.url()); - }); - }); - - describe("Given a user has completed a section", () => { - beforeEach("Complete section", () => { - browser.openQuestionnaire(hubAndSpokeSchema); - $(HubPage.summaryRowLink("employment-section")).click(); - $(EmploymentStatusBlockPage.exclusiveNoneOfTheseApply()).click(); - $(EmploymentStatusBlockPage.submit()).click(); - $(EmploymentTypeBlockPage.studying()).click(); - }); - - it("When the user clicks the 'Continue' button, it should return them to the hub", () => { - $(EmploymentTypeBlockPage.submit()).click(); - const expectedUrl = browser.getUrl(); - expect(expectedUrl).to.contain(HubPage.url()); - }); - - it("When the user returns to the Hub, Then the Hub should be in a continue state", () => { - $(EmploymentTypeBlockPage.submit()).click(); - expect($(HubPage.submit()).getText()).to.contain("Continue"); - expect($(HubPage.heading()).getText()).to.contain("Choose another section to complete"); - }); - - it("When the user returns to the Hub, Then the section should be marked as 'Completed'", () => { - $(EmploymentTypeBlockPage.submit()).click(); - expect($(HubPage.summaryRowState("employment-section")).getText()).to.contain("Completed"); - }); - - it("When the user returns to the Hub and clicks the 'View answers' link on the Hub, if this no summary they are returned to the first block", () => { - $(EmploymentTypeBlockPage.submit()).click(); - $(HubPage.summaryRowLink("employment-section")).click(); - const expectedUrl = browser.getUrl(); - expect(expectedUrl).to.contain(EmploymentStatusBlockPage.url()); - }); - - it("When the user returns to the Hub and continues, Then they should progress to the next section", () => { - $(EmploymentTypeBlockPage.submit()).click(); - expect(browser.getUrl()).to.contain(HubPage.url()); - $(HubPage.submit()).click(); - const expectedUrl = browser.getUrl(); - expect(expectedUrl).to.contain(ProxyPage.url()); - }); - }); - - describe("Given a user has completed a section and is on the Hub page", () => { - beforeEach("Complete section", () => { - browser.openQuestionnaire(hubAndSpokeSchema); - $(HubPage.summaryRowLink("employment-section")).click(); - $(EmploymentStatusBlockPage.workingAsAnEmployee()).click(); - $(EmploymentStatusBlockPage.submit()).click(); - - expect($(HubPage.summaryRowState("employment-section")).getText()).to.contain("Completed"); - }); - - it("When the user clicks the 'View answers' link and incompletes the section, Then they the should be taken to the next incomplete question on 'Continue", () => { - $(HubPage.summaryRowLink("employment-section")).click(); - expect(browser.getUrl()).to.contain(EmploymentStatusBlockPage.url()); - $(EmploymentStatusBlockPage.exclusiveNoneOfTheseApply()).click(); - $(EmploymentStatusBlockPage.submit()).click(); - const expectedUrl = browser.getUrl(); - expect(expectedUrl).to.contain(EmploymentTypeBlockPage.url()); - }); - - it("When the user clicks the 'View answers' link and incompletes the section and returns to the hub, Then the section should be marked as 'Partially completed'", () => { - $(HubPage.summaryRowLink("employment-section")).click(); - expect(browser.getUrl()).to.contain(EmploymentStatusBlockPage.url()); - $(EmploymentStatusBlockPage.exclusiveNoneOfTheseApply()).click(); - $(EmploymentStatusBlockPage.submit()).click(); - browser.url(HubPage.url()); - const expectedUrl = browser.getUrl(); - expect(expectedUrl).to.contain(HubPage.url()); - expect($(HubPage.summaryRowState("employment-section")).getText()).to.contain("Partially completed"); - }); - }); - - describe("Given a user has completed all sections", () => { - beforeEach("Complete all sections", () => { - browser.openQuestionnaire(hubAndSpokeSchema); - $(HubPage.summaryRowLink("employment-section")).click(); - $(EmploymentStatusBlockPage.exclusiveNoneOfTheseApply()).click(); - $(EmploymentStatusBlockPage.submit()).click(); - $(EmploymentTypeBlockPage.studying()).click(); - $(EmploymentTypeBlockPage.submit()).click(); - $(HubPage.submit()).click(); - $(ProxyPage.yes()).click(); - $(ProxyPage.submit()).click(); - $(AccomodationDetailsSummaryBlockPage.submit()).click(); - $(HubPage.submit()).click(); - $(DoesAnyoneLiveHere.no()).click(); - $(DoesAnyoneLiveHere.submit()).click(); - $(HouseholdSummary.submit()).click(); - $(HubPage.submit()).click(); - $(AnyoneRelated.yes()).click(); - $(AnyoneRelated.submit()).click(); - $(RelationshipsSummary.submit()).click(); - }); - - it("It should return them to the hub", () => { - const expectedUrl = browser.getUrl(); - expect(expectedUrl).to.contain(HubPage.url()); - }); - - it("When the user returns to the Hub, Then the Hub should be in a completed state", () => { - expect($(HubPage.submit()).getText()).to.contain("Submit survey"); - expect($(HubPage.heading()).getText()).to.contain("Submit survey"); - }); - - it("When the user submits, it should show the thankyou page", () => { - $(HubPage.submit()).click(); - const expectedUrl = browser.getUrl(); - expect(expectedUrl).to.contain("thank-you"); - }); - }); - - describe("Given a user opens a schema with required sections", () => { - beforeEach("Load survey", () => { - browser.openQuestionnaire("test_hub_complete_sections.json"); - }); - - it("The hub should not show first of all", () => { - expect(browser.getUrl()).to.contain(EmploymentStatusBlockPage.url()); - }); - - it("The hub should only display when required sections are complete", () => { - $(EmploymentStatusBlockPage.exclusiveNoneOfTheseApply()).click(); - $(EmploymentStatusBlockPage.submit()).click(); - $(EmploymentTypeBlockPage.studying()).click(); - $(EmploymentTypeBlockPage.submit()).click(); - expect(browser.getUrl()).to.contain(HubPage.url()); - }); - }); - - describe("Given a section is complete and the user has been returned to a section summary by clicking the 'View answers' link ", () => { - beforeEach("Complete section", () => { - browser.openQuestionnaire(hubAndSpokeSchema); - $(HubPage.summaryRowLink("household-section")).click(); - $(DoesAnyoneLiveHere.no()).click(); - $(DoesAnyoneLiveHere.submit()).click(); - $(HouseholdSummary.submit()).click(); - }); - - it("When there are no changes, continue returns directly to the hub", () => { - $(HubPage.summaryRowLink("household-section")).click(); - $(HouseholdSummary.submit()).click(); - const expectedUrl = browser.getUrl(); - expect(expectedUrl).to.contain(HubPage.url()); - }); - - it("When there are changes, which would set the section to in_progress it routes accordingly", () => { - $(HubPage.summaryRowLink("household-section")).click(); - $(HouseholdSummary.doesAnyoneLiveHereAnswerEdit()).click(); - $(DoesAnyoneLiveHere.yes()).click(); - $(DoesAnyoneLiveHere.submit()).click(); - $(HouseholdSummary.submit()).click(); - const expectedUrl = browser.getUrl(); - expect(expectedUrl).to.contain(HowManyPeopleLiveHere.url()); - }); - }); -}); diff --git a/tests/functional/spec/features/hub_and_spoke/hub_and_spoke_custom_content.spec.js b/tests/functional/spec/features/hub_and_spoke/hub_and_spoke_custom_content.spec.js deleted file mode 100644 index 9cf3031c4f..0000000000 --- a/tests/functional/spec/features/hub_and_spoke/hub_and_spoke_custom_content.spec.js +++ /dev/null @@ -1,30 +0,0 @@ -import HouseholdSummary from "../../../generated_pages/hub_and_spoke_custom_content/household-section-summary.page.js"; -import HowManyPeopleLiveHere from "../../../generated_pages/hub_and_spoke_custom_content/how-many-people-live-here.page.js"; -import DoesAnyoneLiveHere from "../../../generated_pages/hub_and_spoke_custom_content/does-anyone-live-here.page.js"; -import HubPage from "../../../base_pages/hub.page.js"; - -describe("Feature: Hub and Spoke with custom content", () => { - const hubAndSpokeSchema = "test_hub_and_spoke_custom_content.json"; - - it("When the questionnaire is incomplete, then custom content should be displayed correctly", () => { - browser.openQuestionnaire(hubAndSpokeSchema); - expect($(HubPage.heading()).getText()).to.contain("Choose another section to complete"); - expect($(HubPage.guidance()).isExisting()).to.be.false; - expect($(HubPage.submit()).getText()).to.contain("Continue"); - expect($(HubPage.warning()).isExisting()).to.be.false; - }); - - it("When the questionnaire is complete, then custom content should be displayed correctly", () => { - browser.openQuestionnaire(hubAndSpokeSchema); - $(HubPage.summaryRowLink("household-section")).click(); - $(DoesAnyoneLiveHere.yes()).click(); - $(DoesAnyoneLiveHere.submit()).click(); - $(HowManyPeopleLiveHere.answer1()).click(); - $(HowManyPeopleLiveHere.submit()).click(); - $(HouseholdSummary.submit()).click(); - expect($(HubPage.heading()).getText()).to.contain("Submission title"); - expect($(HubPage.guidance()).getText()).to.contain("Submission guidance"); - expect($(HubPage.submit()).getText()).to.contain("Submission button"); - expect($(HubPage.warning()).getText()).to.contain("Submission warning"); - }); -}); diff --git a/tests/functional/spec/features/hub_and_spoke/hub_and_spoke_required_enable.spec.js b/tests/functional/spec/features/hub_and_spoke/hub_and_spoke_required_enable.spec.js deleted file mode 100644 index 461f9e5536..0000000000 --- a/tests/functional/spec/features/hub_and_spoke/hub_and_spoke_required_enable.spec.js +++ /dev/null @@ -1,21 +0,0 @@ -import HouseholdRelationshipsBlockPage from "../../../generated_pages/hub_section_required_and_enabled/household-relationships-block.page"; -import RelationshipsCountPage from "../../../generated_pages/hub_section_required_and_enabled/relationships-count.page"; -import { SubmitPage } from "../../../base_pages/submit.page"; - -describe("Hub and spoke section required and enabled", () => { - beforeEach("Load the survey", () => { - browser.openQuestionnaire("test_hub_section_required_and_enabled.json"); - }); - it("Given a relationship question in household, When I answer 'Yes', meaning the second section is enabled, Then I am routed to second section", () => { - $(HouseholdRelationshipsBlockPage.yes()).click(); - $(HouseholdRelationshipsBlockPage.submit()).click(); - expect($(RelationshipsCountPage.legend()).getText()).to.contain("How many people are related"); - }); - it("Given a relationship question in household, When I answer 'No', Then I am redirected to the hub and can submit my answers without completing the other section", () => { - $(HouseholdRelationshipsBlockPage.no()).click(); - $(HouseholdRelationshipsBlockPage.submit()).click(); - expect($("body").getText()).to.contain("Submit survey"); - $(SubmitPage.submit()).click(); - expect(browser.getUrl()).to.contain("thank-you"); - }); -}); diff --git a/tests/functional/spec/features/hub_and_spoke/previous.spec.js b/tests/functional/spec/features/hub_and_spoke/previous.spec.js deleted file mode 100644 index b71a743025..0000000000 --- a/tests/functional/spec/features/hub_and_spoke/previous.spec.js +++ /dev/null @@ -1,34 +0,0 @@ -import EmploymentStatusBlockPage from "../../../generated_pages/hub_and_spoke/employment-status.page.js"; -import EmploymentTypePage from "../../../generated_pages/hub_and_spoke/employment-type.page.js"; -import HubPage from "../../../base_pages/hub.page.js"; -import ProxyPage from "../../../generated_pages/hub_and_spoke/proxy.page.js"; -const schema = "test_hub_complete_sections.json"; - -describe("Choose another section link", () => { - beforeEach(() => { - browser.openQuestionnaire(schema); - }); - - it("When a user gets to initial question, then the previous location link should not be displayed", () => { - expect($(EmploymentStatusBlockPage.previous()).isExisting()).to.be.false; - }); - - it("When a user gets to the hub, then the previous location link should not be displayed", () => { - $(EmploymentStatusBlockPage.workingAsAnEmployee()).click(); - $(EmploymentStatusBlockPage.submit()).click(); - expect($(HubPage.previous()).isExisting()).to.be.false; - }); - - it("When a user gets to subsequent question, then the previous location link should be displayed", () => { - $(EmploymentStatusBlockPage.exclusiveNoneOfTheseApply()).click(); - $(EmploymentStatusBlockPage.submit()).click(); - expect($(EmploymentTypePage.previous()).isExisting()).to.be.true; - }); - - it("When a user gets to subsequent questions past the hub, then the previous location link should be displayed", () => { - $(EmploymentStatusBlockPage.workingAsAnEmployee()).click(); - $(EmploymentStatusBlockPage.submit()).click(); - $(HubPage.summaryRowLink("accommodation-section")).click(); - expect($(ProxyPage.previous()).isExisting()).to.be.true; - }); -}); diff --git a/tests/functional/spec/features/last_viewed_guidance/last_viewed_guidance.spec.js b/tests/functional/spec/features/last_viewed_guidance/last_viewed_guidance.spec.js index faf02232f3..a8d2f523f7 100644 --- a/tests/functional/spec/features/last_viewed_guidance/last_viewed_guidance.spec.js +++ b/tests/functional/spec/features/last_viewed_guidance/last_viewed_guidance.spec.js @@ -2,7 +2,7 @@ import { getRandomString } from "../../../jwt_helper"; import AddressConfirmationPage from "../../../generated_pages/last_viewed_question_guidance/address-confirmation.page"; import HouseholdInterstitialPage from "../../../generated_pages/last_viewed_question_guidance/household-interstitial.page.js"; import PrimaryPersonListCollectorPage from "../../../generated_pages/last_viewed_question_guidance/primary-person-list-collector.page.js"; - +import { click, verifyUrlContains } from "../../../helpers"; describe("Last viewed question guidance", () => { const resumableLaunchParams = { responseId: getRandomString(16), @@ -10,42 +10,44 @@ describe("Last viewed question guidance", () => { }; describe("Given the last viewed question guidance questionnaire", () => { - before("Open survey", () => { - browser.openQuestionnaire("test_last_viewed_question_guidance.json", resumableLaunchParams); + before("Open survey", async () => { + await browser.openQuestionnaire("test_last_viewed_question_guidance.json", resumableLaunchParams); }); - it("When the respondent first launches the survey, then last question guidance is not shown", () => { - expect(browser.getUrl()).to.contain(HouseholdInterstitialPage.url()); - expect($(HouseholdInterstitialPage.lastViewedQuestionGuidance()).isExisting()).to.be.false; + it("When the respondent first launches the survey, then last question guidance is not shown", async () => { + await verifyUrlContains(HouseholdInterstitialPage.url()); + await expect(await $(HouseholdInterstitialPage.lastViewedQuestionGuidance()).isExisting()).toBe(false); }); - it("When the respondent resumes on the first block of a section, then last question guidance is not shown", () => { - $(HouseholdInterstitialPage.saveSignOut()).click(); - browser.openQuestionnaire("test_last_viewed_question_guidance.json", resumableLaunchParams); - expect(browser.getUrl()).to.contain(HouseholdInterstitialPage.url()); - expect($(HouseholdInterstitialPage.lastViewedQuestionGuidance()).isExisting()).to.be.false; + it("When the respondent resumes on the first block of a section, then last question guidance is not shown", async () => { + await $(HouseholdInterstitialPage.saveSignOut()).click(); + await browser.openQuestionnaire("test_last_viewed_question_guidance.json", resumableLaunchParams); + await browser.pause(100); + await verifyUrlContains(HouseholdInterstitialPage.url()); + await expect(await $(HouseholdInterstitialPage.lastViewedQuestionGuidance()).isExisting()).toBe(false); }); - it("When the respondent saves and resumes from a section which is in progress, then last question guidance is shown", () => { - $(HouseholdInterstitialPage.submit()).click(); - $(AddressConfirmationPage.saveSignOut()).click(); - browser.openQuestionnaire("test_last_viewed_question_guidance.json", resumableLaunchParams); - expect(browser.getUrl()).to.contain(AddressConfirmationPage.url()); - expect($(AddressConfirmationPage.lastViewedQuestionGuidanceLink()).getAttribute("href")).to.contain(HouseholdInterstitialPage.url()); - expect($(AddressConfirmationPage.lastViewedQuestionGuidance()).isExisting()).to.be.true; + it("When the respondent saves and resumes from a section which is in progress, then last question guidance is shown", async () => { + await click(HouseholdInterstitialPage.submit()); + await $(AddressConfirmationPage.saveSignOut()).click(); + await browser.openQuestionnaire("test_last_viewed_question_guidance.json", resumableLaunchParams); + await browser.pause(100); + await verifyUrlContains(AddressConfirmationPage.url()); + await expect(await $(AddressConfirmationPage.lastViewedQuestionGuidanceLink()).getAttribute("href")).toContain(HouseholdInterstitialPage.url()); + await expect(await $(AddressConfirmationPage.lastViewedQuestionGuidance()).isExisting()).toBe(true); }); - it("When the respondent answers the question and saves and continues, then last question guidance is not shown on the next question", () => { - $(AddressConfirmationPage.yes()).click(); - $(AddressConfirmationPage.submit()).click(); - expect(browser.getUrl()).to.contain(PrimaryPersonListCollectorPage.url()); - expect($(HouseholdInterstitialPage.lastViewedQuestionGuidance()).isExisting()).to.be.false; + it("When the respondent answers the question and saves and continues, then last question guidance is not shown on the next question", async () => { + await $(AddressConfirmationPage.yes()).click(); + await click(AddressConfirmationPage.submit()); + await verifyUrlContains(PrimaryPersonListCollectorPage.url()); + await expect(await $(HouseholdInterstitialPage.lastViewedQuestionGuidance()).isExisting()).toBe(false); }); - it("When the respondent uses the previous link from the next question, then last question guidance is not shown", () => { - $(AddressConfirmationPage.submit()).click(); - $(PrimaryPersonListCollectorPage.previous()).click(); - expect($(HouseholdInterstitialPage.lastViewedQuestionGuidance()).isExisting()).to.be.false; + it("When the respondent uses the previous link from the next question, then last question guidance is not shown", async () => { + await click(AddressConfirmationPage.submit()); + await $(PrimaryPersonListCollectorPage.previous()).click(); + await expect(await $(HouseholdInterstitialPage.lastViewedQuestionGuidance()).isExisting()).toBe(false); }); }); }); diff --git a/tests/functional/spec/features/last_viewed_guidance/last_viewed_guidance_hub.spec.js b/tests/functional/spec/features/last_viewed_guidance/last_viewed_guidance_hub.spec.js index 0e39c58551..1d53bf1aa1 100644 --- a/tests/functional/spec/features/last_viewed_guidance/last_viewed_guidance_hub.spec.js +++ b/tests/functional/spec/features/last_viewed_guidance/last_viewed_guidance_hub.spec.js @@ -8,7 +8,7 @@ import SportsPage from "../../../generated_pages/last_viewed_question_guidance_h import UnPaidWorkPage from "../../../generated_pages/last_viewed_question_guidance_hub/unpaid-work.page.js"; import WorkInterstitialPage from "../../../generated_pages/last_viewed_question_guidance_hub/work-interstitial.page.js"; import HubPage from "../../../base_pages/hub.page.js"; - +import { click, verifyUrlContains } from "../../../helpers"; describe("Last viewed question guidance", () => { const resumableLaunchParams = { responseId: getRandomString(16), @@ -16,94 +16,95 @@ describe("Last viewed question guidance", () => { }; describe("Given the hub has a required section, which has not been completed", () => { - before("Open survey", () => { - browser.openQuestionnaire("test_last_viewed_question_guidance_hub.json", resumableLaunchParams); + before("Open survey", async () => { + await browser.openQuestionnaire("test_last_viewed_question_guidance_hub.json", resumableLaunchParams); }); - it("When the respondent launches the survey, then last question guidance is not shown", () => { - expect(browser.getUrl()).to.contain(WorkInterstitialPage.url()); - expect($(WorkInterstitialPage.lastViewedQuestionGuidance()).isExisting()).to.be.false; + it("When the respondent launches the survey, then last question guidance is not shown", async () => { + await verifyUrlContains(WorkInterstitialPage.url()); + await expect(await $(WorkInterstitialPage.lastViewedQuestionGuidance()).isExisting()).toBe(false); }); - it("When the respondent saves and resumes from a section which is not started, then last question guidance is not shown", () => { - $(WorkInterstitialPage.saveSignOut()).click(); - browser.openQuestionnaire("test_last_viewed_question_guidance_hub.json", resumableLaunchParams); - expect(browser.getUrl()).to.contain(WorkInterstitialPage.url()); - expect($(WorkInterstitialPage.lastViewedQuestionGuidance()).isExisting()).to.be.false; + it("When the respondent saves and resumes from a section which is not started, then last question guidance is not shown", async () => { + await $(WorkInterstitialPage.saveSignOut()).click(); + await browser.openQuestionnaire("test_last_viewed_question_guidance_hub.json", resumableLaunchParams); + await browser.pause(100); + await verifyUrlContains(WorkInterstitialPage.url()); + await expect(await $(WorkInterstitialPage.lastViewedQuestionGuidance()).isExisting()).toBe(false); }); - it("When the respondent saves and resumes from a section which is in progress, then last question guidance is shown", () => { - $(WorkInterstitialPage.submit()).click(); - $(PaidWorkPage.saveSignOut()).click(); - browser.openQuestionnaire("test_last_viewed_question_guidance_hub.json", resumableLaunchParams); - expect($(PaidWorkPage.lastViewedQuestionGuidanceLink()).getAttribute("href")).to.contain(WorkInterstitialPage.url()); - expect($(PaidWorkPage.lastViewedQuestionGuidance()).isExisting()).to.be.true; + it("When the respondent saves and resumes from a section which is in progress, then last question guidance is shown", async () => { + await click(WorkInterstitialPage.submit()); + await $(PaidWorkPage.saveSignOut()).click(); + await browser.openQuestionnaire("test_last_viewed_question_guidance_hub.json", resumableLaunchParams); + await expect(await $(PaidWorkPage.lastViewedQuestionGuidanceLink()).getAttribute("href")).toContain(WorkInterstitialPage.url()); + await expect(await $(PaidWorkPage.lastViewedQuestionGuidance()).isExisting()).toBe(true); }); }); describe("Given the respondent has completed the required section and is on the hub", () => { - before("Open survey and complete first section", () => { - browser.openQuestionnaire("test_last_viewed_question_guidance_hub.json"); - $(WorkInterstitialPage.submit()).click(); - $(PaidWorkPage.yes()).click(); - $(PaidWorkPage.submit()).click(); - $(UnPaidWorkPage.yes()).click(); - $(UnPaidWorkPage.submit()).click(); + before("Open survey and complete first section", async () => { + await browser.openQuestionnaire("test_last_viewed_question_guidance_hub.json"); + await click(WorkInterstitialPage.submit()); + await $(PaidWorkPage.yes()).click(); + await click(PaidWorkPage.submit()); + await $(UnPaidWorkPage.yes()).click(); + await click(UnPaidWorkPage.submit()); }); - it("When the respondent selects a section which is not started, then last question guidance is not shown", () => { - $(HubPage.summaryRowLink("education-section")).click(); - expect(browser.getUrl()).to.contain(GcsesPage.url()); - expect($(GcsesPage.lastViewedQuestionGuidance()).isExisting()).to.be.false; + it("When the respondent selects a section which is not started, then last question guidance is not shown", async () => { + await $(HubPage.summaryRowLink("education-section")).click(); + await verifyUrlContains(GcsesPage.url()); + await expect(await $(GcsesPage.lastViewedQuestionGuidance()).isExisting()).toBe(false); }); - it("When the respondent selects a section which is in progress, then last question guidance is shown", () => { - $(HubPage.submit()).click(); - $(GcsesPage.yes()).click(); - $(GcsesPage.submit()).click(); - browser.url(HubPage.url()); - $(HubPage.summaryRowLink("education-section")).click(); - expect(browser.getUrl()).to.contain(ALevelsPage.url()); - expect($(ALevelsPage.lastViewedQuestionGuidanceLink()).getAttribute("href")).to.contain(GcsesPage.url()); - expect($(ALevelsPage.lastViewedQuestionGuidance()).isExisting()).to.be.true; + it("When the respondent selects a section which is in progress, then last question guidance is shown", async () => { + await click(HubPage.submit()); + await $(GcsesPage.yes()).click(); + await click(GcsesPage.submit()); + await browser.url(HubPage.url()); + await $(HubPage.summaryRowLink("education-section")).click(); + await verifyUrlContains(ALevelsPage.url()); + await expect(await $(ALevelsPage.lastViewedQuestionGuidanceLink()).getAttribute("href")).toContain(GcsesPage.url()); + await expect(await $(ALevelsPage.lastViewedQuestionGuidance()).isExisting()).toBe(true); }); - it("When the respondent selects a section which is complete , then last question guidance is not shown on the summary or any link clicked from the summary", () => { - $(ALevelsPage.yes()).click(); - $(ALevelsPage.submit()).click(); - expect(browser.getUrl()).to.contain(EducationSectionSummaryPage.url()); - expect($(ALevelsPage.lastViewedQuestionGuidance()).isExisting()).to.be.false; - $(EducationSectionSummaryPage.submit()).click(); - $(HubPage.summaryRowLink("education-section")).click(); - expect(browser.getUrl()).to.contain(EducationSectionSummaryPage.url()); - $(EducationSectionSummaryPage.alevelsAnswerEdit()).click(); - expect($(ALevelsPage.lastViewedQuestionGuidance()).isExisting()).to.be.false; + it("When the respondent selects a section which is complete , then last question guidance is not shown on the summary or any link clicked from the summary", async () => { + await $(ALevelsPage.yes()).click(); + await click(ALevelsPage.submit()); + await verifyUrlContains(EducationSectionSummaryPage.url()); + await expect(await $(ALevelsPage.lastViewedQuestionGuidance()).isExisting()).toBe(false); + await click(EducationSectionSummaryPage.submit()); + await $(HubPage.summaryRowLink("education-section")).click(); + await verifyUrlContains(EducationSectionSummaryPage.url()); + await $(EducationSectionSummaryPage.alevelsAnswerEdit()).click(); + await expect(await $(ALevelsPage.lastViewedQuestionGuidance()).isExisting()).toBe(false); }); - it("When the user clicks continue on the hub and it takes you to a section which is not started, then last question guidance is not shown", () => { - browser.url(HubPage.url()); - $(HubPage.submit()).click(); - expect(browser.getUrl()).to.contain(SportsPage.url()); - expect($(SportsPage.lastViewedQuestionGuidance()).isExisting()).to.be.false; + it("When the user clicks continue on the hub and it takes you to a section which is not started, then last question guidance is not shown", async () => { + await browser.url(HubPage.url()); + await click(HubPage.submit()); + await verifyUrlContains(SportsPage.url()); + await expect(await $(SportsPage.lastViewedQuestionGuidance()).isExisting()).toBe(false); }); - it("When the user clicks continue on the hub and it takes you to a section which is in progress, then last question guidance is shown", () => { - $(HubPage.submit()).click(); - $(SportsPage.yes()).click(); - $(SportsPage.submit()).click(); - browser.url(HubPage.url()); - $(HubPage.submit()).click(); - expect(browser.getUrl()).to.contain(HobbiesPage.url()); - expect($(HobbiesPage.lastViewedQuestionGuidanceLink()).getAttribute("href")).to.contain(SportsPage.url()); - expect($(HobbiesPage.lastViewedQuestionGuidance()).isExisting()).to.be.true; + it("When the user clicks continue on the hub and it takes you to a section which is in progress, then last question guidance is shown", async () => { + await click(HubPage.submit()); + await $(SportsPage.yes()).click(); + await click(SportsPage.submit()); + await browser.url(HubPage.url()); + await click(HubPage.submit()); + await verifyUrlContains(HobbiesPage.url()); + await expect(await $(HobbiesPage.lastViewedQuestionGuidanceLink()).getAttribute("href")).toContain(SportsPage.url()); + await expect(await $(HobbiesPage.lastViewedQuestionGuidance()).isExisting()).toBe(true); }); - it("When the user clicks continue on the hub and it takes you to a section which is complete but doesnt have a summary, then last question guidance is not shown", () => { - $(HobbiesPage.yes()).click(); - $(HobbiesPage.submit()).click(); - $(HubPage.summaryRowLink("interests-section")).click(); - expect(browser.getUrl()).to.contain(SportsPage.url()); - expect($(SportsPage.lastViewedQuestionGuidance()).isExisting()).to.be.false; + it("When the user clicks continue on the hub and it takes you to a section which is complete but doesnt have a summary, then last question guidance is not shown", async () => { + await $(HobbiesPage.yes()).click(); + await click(HobbiesPage.submit()); + await $(HubPage.summaryRowLink("interests-section")).click(); + await verifyUrlContains(SportsPage.url()); + await expect(await $(SportsPage.lastViewedQuestionGuidance()).isExisting()).toBe(false); }); }); }); diff --git a/tests/functional/spec/features/placeholder/answer_option_based_on_first_item_in_list.spec.js b/tests/functional/spec/features/placeholder/answer_option_based_on_first_item_in_list.spec.js index e82ea04b30..85d4c1c8eb 100644 --- a/tests/functional/spec/features/placeholder/answer_option_based_on_first_item_in_list.spec.js +++ b/tests/functional/spec/features/placeholder/answer_option_based_on_first_item_in_list.spec.js @@ -5,65 +5,65 @@ import FavouriteDrinkQuestion from "../../../generated_pages/placeholder_based_o import ListStatusQuestion from "../../../generated_pages/placeholder_based_on_first_item_in_list/list-status-2.page.js"; import SummaryPage from "../../../generated_pages/placeholder_based_on_first_item_in_list/personal-details-section-summary.page.js"; import HubPage from "../../../base_pages/hub.page.js"; - +import { click } from "../../../helpers"; describe("Component: Definition", () => { describe("Load the Survey", () => { - beforeEach(() => { - browser.openQuestionnaire("test_placeholder_based_on_first_item_in_list.json"); + beforeEach(async () => { + await browser.openQuestionnaire("test_placeholder_based_on_first_item_in_list.json"); }); - it("Given I am the first person in the list, When I get to the question page, Then I should see the default answer option", () => { + it("Given I am the first person in the list, When I get to the question page, Then I should see the default answer option", async () => { // Given - $(HubPage.submit()).click(); - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("Marcus"); - $(ListCollectorAddPage.lastName()).setValue("Twin"); - $(ListCollectorAddPage.submit()).click(); - $(ListCollectorPage.no()).click(); - $(ListCollectorPage.submit()).click(); - $(HubPage.submit()).click(); + await click(HubPage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Marcus"); + await $(ListCollectorAddPage.lastName()).setValue("Twin"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await click(HubPage.submit()); // When - $(ListStatusInterstitial.submit()).click(); - $(FavouriteDrinkQuestion.answer()).setValue("Orange Juice"); - $(FavouriteDrinkQuestion.submit()).click(); + await click(ListStatusInterstitial.submit()); + await $(FavouriteDrinkQuestion.answer()).setValue("Orange Juice"); + await click(FavouriteDrinkQuestion.submit()); // Then - expect($(ListStatusQuestion.listStatus2TeaLabel()).getText()).to.contain("Tea"); + await expect(await $(ListStatusQuestion.listStatus2TeaLabel()).getText()).toBe("Tea"); }); - it("Given I am not the first person in the list, When I get to the question page, Then I should see the correct answer option", () => { + it("Given I am not the first person in the list, When I get to the question page, Then I should see the correct answer option", async () => { // Given - $(HubPage.submit()).click(); - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("Marcus"); - $(ListCollectorAddPage.lastName()).setValue("Twin"); - $(ListCollectorAddPage.submit()).click(); - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("John"); - $(ListCollectorAddPage.lastName()).setValue("Doe"); - $(ListCollectorAddPage.submit()).click(); - $(ListCollectorPage.no()).click(); - $(ListCollectorPage.submit()).click(); - $(HubPage.submit()).click(); + await click(HubPage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Marcus"); + await $(ListCollectorAddPage.lastName()).setValue("Twin"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("John"); + await $(ListCollectorAddPage.lastName()).setValue("Doe"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await click(HubPage.submit()); // When - $(ListStatusInterstitial.submit()).click(); - $(FavouriteDrinkQuestion.answer()).setValue("Orange Juice"); - $(FavouriteDrinkQuestion.submit()).click(); - $(ListStatusQuestion.listStatus2Tea()).click(); - $(ListStatusQuestion.submit()).click(); - $(SummaryPage.submit()).click(); - $(HubPage.submit()).click(); - $(ListStatusInterstitial.submit()).click(); - $(FavouriteDrinkQuestion.answer()).setValue("Lemonade"); - $(FavouriteDrinkQuestion.submit()).click(); + await click(ListStatusInterstitial.submit()); + await $(FavouriteDrinkQuestion.answer()).setValue("Orange Juice"); + await click(FavouriteDrinkQuestion.submit()); + await $(ListStatusQuestion.listStatus2Tea()).click(); + await click(ListStatusQuestion.submit()); + await click(SummaryPage.submit()); + await click(HubPage.submit()); + await click(ListStatusInterstitial.submit()); + await $(FavouriteDrinkQuestion.answer()).setValue("Lemonade"); + await click(FavouriteDrinkQuestion.submit()); // Then - expect($(ListStatusQuestion.listStatus2TeaLabel()).getText()).to.contain("Orange Juice"); + await expect(await $(ListStatusQuestion.listStatus2TeaLabel()).getText()).toBe("Orange Juice"); }); }); }); diff --git a/tests/functional/spec/features/placeholder/placeholder_date_difference.spec.js b/tests/functional/spec/features/placeholder/placeholder_date_difference.spec.js index bfefb4225f..11e466e624 100644 --- a/tests/functional/spec/features/placeholder/placeholder_date_difference.spec.js +++ b/tests/functional/spec/features/placeholder/placeholder_date_difference.spec.js @@ -6,72 +6,72 @@ import AgeBlockDayMonthYearRangePage from "../../../generated_pages/placeholder_ import AgeTestDayMonthYearRangePage from "../../../generated_pages/placeholder_difference_in_years_range/age-test.page"; import AgeBlockMonthYearRangePage from "../../../generated_pages/placeholder_difference_in_years_month_year_range/date-block.page"; import AgeTestMonthYearRangePage from "../../../generated_pages/placeholder_difference_in_years_month_year_range/age-test.page"; - +import { click } from "../../../helpers"; describe("Difference check (years)", () => { - before("Load the survey", () => { - browser.openQuestionnaire("test_placeholder_difference_in_years.json"); + before("Load the survey", async () => { + await browser.openQuestionnaire("test_placeholder_difference_in_years.json"); }); - it("Given a day, month and year answer is provided for a date question then the age in years should be calculated and displayed on the page ", () => { - $(AgeBlockYearPage.day()).setValue(1); - $(AgeBlockYearPage.month()).setValue(1); - $(AgeBlockYearPage.year()).setValue(1990); - $(AgeBlockYearPage.submit()).click(); - expect($(AgeTestYearPage.heading()).getText()).to.equal(`You are ${getYears("1990/01/01")} years old. Is this correct?`); + it("Given a day, month and year answer is provided for a date question then the age in years should be calculated and displayed on the page ", async () => { + await $(AgeBlockYearPage.day()).setValue(1); + await $(AgeBlockYearPage.month()).setValue(1); + await $(AgeBlockYearPage.year()).setValue(1990); + await click(AgeBlockYearPage.submit()); + await expect(await $(AgeTestYearPage.heading()).getText()).toBe(`You are ${getYears("1990/01/01")} years old. Is this correct?`); }); }); describe("Difference check (months and years)", () => { - before("Load the survey", () => { - browser.openQuestionnaire("test_placeholder_difference_in_years_month_year.json"); + before("Load the survey", async () => { + await browser.openQuestionnaire("test_placeholder_difference_in_years_month_year.json"); }); - it("Given a month and year answer is provided for a date question then the difference in years should be calculated and displayed on the page ", () => { - $(AgeBlockMonthYearPage.Month()).setValue(1); - $(AgeBlockMonthYearPage.Year()).setValue(1990); + it("Given a month and year answer is provided for a date question then the difference in years should be calculated and displayed on the page ", async () => { + await $(AgeBlockMonthYearPage.Month()).setValue(1); + await $(AgeBlockMonthYearPage.Year()).setValue(1990); - $(AgeBlockMonthYearPage.submit()).click(); + await click(AgeBlockMonthYearPage.submit()); - expect($(AgeTestMonthYearPage.heading()).getText()).to.equal( - `It has been ${getYears("1990/01/01")} years since you last went on holiday. Is this correct?` + await expect(await $(AgeTestMonthYearPage.heading()).getText()).toBe( + `It has been ${getYears("1990/01/01")} years since you last went on holiday. Is this correct?`, ); }); }); describe("Difference check (months and years range)", () => { - before("Load the survey", () => { - browser.openQuestionnaire("test_placeholder_difference_in_years_month_year_range.json"); + before("Load the survey", async () => { + await browser.openQuestionnaire("test_placeholder_difference_in_years_month_year_range.json"); }); - it("Given a month and year answers 'from' and 'to' are provided for a date question then the difference in years should be calculated and displayed on the page ", () => { - $(AgeBlockMonthYearRangePage.periodFromMonth()).setValue(1); - $(AgeBlockMonthYearRangePage.periodFromYear()).setValue(1990); - $(AgeBlockMonthYearRangePage.periodToMonth()).setValue(1); - $(AgeBlockMonthYearRangePage.periodToYear()).setValue(1991); + it("Given a month and year answers 'from' and 'to' are provided for a date question then the difference in years should be calculated and displayed on the page ", async () => { + await $(AgeBlockMonthYearRangePage.periodFromMonth()).setValue(1); + await $(AgeBlockMonthYearRangePage.periodFromYear()).setValue(1990); + await $(AgeBlockMonthYearRangePage.periodToMonth()).setValue(1); + await $(AgeBlockMonthYearRangePage.periodToYear()).setValue(1991); - $(AgeBlockMonthYearRangePage.submit()).click(); + await click(AgeBlockMonthYearRangePage.submit()); - expect($(AgeTestMonthYearRangePage.heading()).getText()).to.have.string("You were out of the UK for 1 year. Is this correct?"); + await expect(await $(AgeTestMonthYearRangePage.heading()).getText()).toBe("You were out of the UK for 1 year. Is this correct?"); }); }); describe("Difference check (years range)", () => { - before("Load the survey", () => { - browser.openQuestionnaire("test_placeholder_difference_in_years_range.json"); + before("Load the survey", async () => { + await browser.openQuestionnaire("test_placeholder_difference_in_years_range.json"); }); - it("Given a day, month and year answers 'from' and 'to' are provided for a date question then the difference in years should be calculated and displayed on the page ", () => { - $(AgeBlockDayMonthYearRangePage.periodFromday()).setValue(1); - $(AgeBlockDayMonthYearRangePage.periodFrommonth()).setValue(1); - $(AgeBlockDayMonthYearRangePage.periodFromyear()).setValue(1990); + it("Given a day, month and year answers 'from' and 'to' are provided for a date question then the difference in years should be calculated and displayed on the page ", async () => { + await $(AgeBlockDayMonthYearRangePage.periodFromday()).setValue(1); + await $(AgeBlockDayMonthYearRangePage.periodFrommonth()).setValue(1); + await $(AgeBlockDayMonthYearRangePage.periodFromyear()).setValue(1990); - $(AgeBlockDayMonthYearRangePage.periodToday()).setValue(1); - $(AgeBlockDayMonthYearRangePage.periodTomonth()).setValue(1); - $(AgeBlockDayMonthYearRangePage.periodToyear()).setValue(1991); + await $(AgeBlockDayMonthYearRangePage.periodToday()).setValue(1); + await $(AgeBlockDayMonthYearRangePage.periodTomonth()).setValue(1); + await $(AgeBlockDayMonthYearRangePage.periodToyear()).setValue(1991); - $(AgeBlockDayMonthYearRangePage.submit()).click(); + await click(AgeBlockDayMonthYearRangePage.submit()); - expect($(AgeTestDayMonthYearRangePage.heading()).getText()).to.have.string("You were out of the UK for 1 year. Is this correct?"); + await expect(await $(AgeTestDayMonthYearRangePage.heading()).getText()).toBe("You were out of the UK for 1 year. Is this correct?"); }); }); diff --git a/tests/functional/spec/features/placeholder/placeholder_date_ranges.spec.js b/tests/functional/spec/features/placeholder/placeholder_date_ranges.spec.js index 5b4615ed44..ae50636dde 100644 --- a/tests/functional/spec/features/placeholder/placeholder_date_ranges.spec.js +++ b/tests/functional/spec/features/placeholder/placeholder_date_ranges.spec.js @@ -2,41 +2,41 @@ import DateQuestionPage from "../../../generated_pages/placeholder_transform_dat import DaysQuestionBlockPage from "../../../generated_pages/placeholder_transform_date_range_bounds/days-question-block.page"; import Block0Page from "../../../generated_pages/placeholder_transform_date_range_bounds/block0.page"; import RangeQuestionBlockPage from "../../../generated_pages/placeholder_transform_date_range_bounds/range-question-block.page"; - +import { click } from "../../../helpers"; describe("Date checks", () => { - beforeEach("Load the survey", () => { - browser.openQuestionnaire("test_placeholder_transform_date_range_bounds.json"); + beforeEach("Load the survey", async () => { + await browser.openQuestionnaire("test_placeholder_transform_date_range_bounds.json"); }); - it("Given a reference date is provided, when I get to the next page, then the placeholder contains a formatted date range based on the reference date", () => { - $(DateQuestionPage.day()).setValue(8); - $(DateQuestionPage.month()).setValue(9); - $(DateQuestionPage.year()).setValue(2021); + it("Given a reference date is provided, when I get to the next page, then the placeholder contains a formatted date range based on the reference date", async () => { + await $(DateQuestionPage.day()).setValue(8); + await $(DateQuestionPage.month()).setValue(9); + await $(DateQuestionPage.year()).setValue(2021); - $(DateQuestionPage.submit()).click(); + await click(DateQuestionPage.submit()); - expect($(DaysQuestionBlockPage.questionText()).getText()).to.contain("Monday 30 August to Monday 13 September 2021"); - $(DaysQuestionBlockPage.submit()).click(); + await expect(await $(DaysQuestionBlockPage.questionText()).getText()).toContain("Monday 30 August to Monday 13 September 2021"); + await click(DaysQuestionBlockPage.submit()); }); - it("Given a reference date is provided, when I get to the next page, then the placeholder contains a formatted date range", () => { - $(DateQuestionPage.day()).setValue(15); - $(DateQuestionPage.month()).setValue(9); - $(DateQuestionPage.year()).setValue(2021); + it("Given a reference date is provided, when I get to the next page, then the placeholder contains a formatted date range", async () => { + await $(DateQuestionPage.day()).setValue(15); + await $(DateQuestionPage.month()).setValue(9); + await $(DateQuestionPage.year()).setValue(2021); - $(DateQuestionPage.submit()).click(); - $(DaysQuestionBlockPage.submit()).click(); + await click(DateQuestionPage.submit()); + await click(DaysQuestionBlockPage.submit()); - $(Block0Page.ref0day()).setValue(1); - $(Block0Page.ref0month()).setValue(5); - $(Block0Page.ref0year()).setValue(2019); + await $(Block0Page.ref0day()).setValue(1); + await $(Block0Page.ref0month()).setValue(5); + await $(Block0Page.ref0year()).setValue(2019); - $(Block0Page.ref1day()).setValue(19); - $(Block0Page.ref1month()).setValue(5); - $(Block0Page.ref1year()).setValue(2019); + await $(Block0Page.ref1day()).setValue(19); + await $(Block0Page.ref1month()).setValue(5); + await $(Block0Page.ref1year()).setValue(2019); - $(Block0Page.submit()).click(); + await click(Block0Page.submit()); - expect($(RangeQuestionBlockPage.questionText()).getText()).to.contain("Wednesday 1 to Sunday 19 May 2019"); + await expect(await $(RangeQuestionBlockPage.questionText()).getText()).toContain("Wednesday 1 to Sunday 19 May 2019"); }); }); diff --git a/tests/functional/spec/features/placeholder/placeholder_default_value.spec.js b/tests/functional/spec/features/placeholder/placeholder_default_value.spec.js index 218a0f2b33..ed490ff9dc 100644 --- a/tests/functional/spec/features/placeholder/placeholder_default_value.spec.js +++ b/tests/functional/spec/features/placeholder/placeholder_default_value.spec.js @@ -1,33 +1,33 @@ import EmployeesNumberBlockPage from "../../../generated_pages/placeholder_default_value/employees-number-block.page"; import EmployeesTrainingBlockPage from "../../../generated_pages/placeholder_default_value/employees-training-block.page"; import EmployeesNumberInterstitialPage from "../../../generated_pages/placeholder_default_value/employees-number-interstitial.page"; - +import { click } from "../../../helpers"; describe("Placeholder default value check", () => { - beforeEach("Load the survey", () => { - browser.openQuestionnaire("test_placeholder_default_value.json"); + beforeEach("Load the survey", async () => { + await browser.openQuestionnaire("test_placeholder_default_value.json"); }); - it("Given a question with default answer, When I do not enter any number and click submit, Then the interstitial page shows default employees number as 0", () => { - $(EmployeesNumberBlockPage.submit()).click(); - expect($("#main-content > p").getText()).to.contain("The total number of employees confirmed are 0"); + it("Given a question with default answer, When I do not enter any number and click submit, Then the interstitial page shows default employees number as 0", async () => { + await click(EmployeesNumberBlockPage.submit()); + await expect(await $("#main-content > p").getText()).toContain("The total number of employees confirmed are 0"); }); - it("Given a question with default answer, When I enter a number of employee and click submit, Then the interstitial page shows me the employees number entered", () => { - $(EmployeesNumberBlockPage.employeesNo()).setValue("54"); - $(EmployeesNumberBlockPage.submit()).click(); - expect($("#main-content > p").getText()).to.contain("The total number of employees confirmed are 54"); + it("Given a question with default answer, When I enter a number of employee and click submit, Then the interstitial page shows me the employees number entered", async () => { + await $(EmployeesNumberBlockPage.employeesNo()).setValue("54"); + await click(EmployeesNumberBlockPage.submit()); + await expect(await $("#main-content > p").getText()).toContain("The total number of employees confirmed are 54"); }); - it("Given a training budget question with default answer, When I do not enter any amount and click submit, Then the interstitial page shows default amount as 250.00", () => { - $(EmployeesNumberBlockPage.submit()).click(); - $(EmployeesNumberInterstitialPage.submit()).click(); - $(EmployeesTrainingBlockPage.submit()).click(); - expect($("#main-content > p").getText()).to.contain("The average training budget per employee is ÂŖ250.00"); + it("Given a training budget question with default answer, When I do not enter any amount and click submit, Then the interstitial page shows default amount as 250.00", async () => { + await click(EmployeesNumberBlockPage.submit()); + await click(EmployeesNumberInterstitialPage.submit()); + await click(EmployeesTrainingBlockPage.submit()); + await expect(await $("#main-content > p").getText()).toBe("The average training budget per employee is ÂŖ250.00"); }); - it("Given a training budget question with default answer, When I enter an amount and click submit, Then the interstitial page shows amount entered", () => { - $(EmployeesNumberBlockPage.submit()).click(); - $(EmployeesNumberInterstitialPage.submit()).click(); - $(EmployeesTrainingBlockPage.employeesAvgTraining()).setValue("100"); - $(EmployeesTrainingBlockPage.submit()).click(); - expect($("#main-content > p").getText()).to.contain("The average training budget per employee is ÂŖ100.00"); + it("Given a training budget question with default answer, When I enter an amount and click submit, Then the interstitial page shows amount entered", async () => { + await click(EmployeesNumberBlockPage.submit()); + await click(EmployeesNumberInterstitialPage.submit()); + await $(EmployeesTrainingBlockPage.employeesAvgTraining()).setValue("100"); + await click(EmployeesTrainingBlockPage.submit()); + await expect(await $("#main-content > p").getText()).toBe("The average training budget per employee is ÂŖ100.00"); }); }); diff --git a/tests/functional/spec/features/placeholder/placeholder_metadata.spec.js b/tests/functional/spec/features/placeholder/placeholder_metadata.spec.js index 7492bd1e77..a875f725d2 100644 --- a/tests/functional/spec/features/placeholder/placeholder_metadata.spec.js +++ b/tests/functional/spec/features/placeholder/placeholder_metadata.spec.js @@ -1,20 +1,20 @@ import MandatoryRadioPage from "../../../generated_pages/placeholder_metadata/mandatory-radio.page"; import SubmitPage from "../../../generated_pages/placeholder_metadata/submit.page"; - +import { click } from "../../../helpers"; describe("Placeholder metadata check", () => { describe("Given I launch placeholder metadata question", () => { - before("Load the survey", () => { - browser.openQuestionnaire("test_placeholder_metadata.json"); + before("Load the survey", async () => { + await browser.openQuestionnaire("test_placeholder_metadata.json"); }); - it("When I see responding unit question, Then I see radio options with first option as metadata placeholder (ru_name)", () => { - expect($(MandatoryRadioPage.answerRuNameLabel()).getText()).to.equal("Apple"); + it("When I see responding unit question, Then I see radio options with first option as metadata placeholder (ru_name)", async () => { + await expect(await $(MandatoryRadioPage.answerRuNameLabel()).getText()).toBe("Apple"); }); - it("When I answer responding unit question, Then I see confirmation page with my selected placeholder metadata option (ru_name)", () => { - $(MandatoryRadioPage.answerRuName()).click(); - $(MandatoryRadioPage.submit()).click(); + it("When I answer responding unit question, Then I see confirmation page with my selected placeholder metadata option (ru_name)", async () => { + await $(MandatoryRadioPage.answerRuName()).click(); + await click(MandatoryRadioPage.submit()); - expect($(SubmitPage.mandatoryRadioAnswer()).getText()).to.equal("Apple"); - expect($(SubmitPage.guidance()).getText()).to.contain("Please submit this survey to complete it"); + await expect(await $(SubmitPage.mandatoryRadioAnswer()).getText()).toBe("Apple"); + await expect(await $(SubmitPage.guidance()).getText()).toBe("Please submit this survey to complete it"); }); }); }); diff --git a/tests/functional/spec/features/placeholder/placeholder_option_label_from_value.spec.js b/tests/functional/spec/features/placeholder/placeholder_option_label_from_value.spec.js index 28444cce99..fc792c4b5c 100644 --- a/tests/functional/spec/features/placeholder/placeholder_option_label_from_value.spec.js +++ b/tests/functional/spec/features/placeholder/placeholder_option_label_from_value.spec.js @@ -1,21 +1,23 @@ import MandatoryRadioPage from "../../../generated_pages/placeholder_option_label_from_value/mandatory-radio.page"; import ConfirmationQuestionRadioBlockPage from "../../../generated_pages/placeholder_option_label_from_value/confirmation-question-radio-block.page"; - +import { click } from "../../../helpers"; describe("Option label value check", () => { - beforeEach("Load the survey", () => { - browser.openQuestionnaire("test_placeholder_option_label_from_value.json"); + beforeEach("Load the survey", async () => { + await browser.openQuestionnaire("test_placeholder_option_label_from_value.json"); }); - it("Given radio options are provided, when I select first answer (piped from metadata) and go to the next page, then the question title contains the label text of the answer I selected", () => { - expect($(MandatoryRadioPage.answerBusinessNamePipedLabel()).getText()).to.contain("Apple (piped)"); - $(MandatoryRadioPage.answerBusinessNamePiped()).click(); - $(MandatoryRadioPage.submit()).click(); - expect($(ConfirmationQuestionRadioBlockPage.questionText()).getText()).to.contain("Apple (piped)"); + it("Given radio options are provided, when I select first answer (piped from metadata) and go to the next page, then the question title contains the label text of the answer I selected", async () => { + await expect(await $(MandatoryRadioPage.answerBusinessNamePipedLabel()).getText()).toContain("Apple (piped)"); + await $(MandatoryRadioPage.answerBusinessNamePiped()).click(); + await $(MandatoryRadioPage.submit()).scrollIntoView(); + await click(MandatoryRadioPage.submit()); + await expect(await $(ConfirmationQuestionRadioBlockPage.questionText()).getText()).toContain("Apple (piped)"); }); - it("Given radio options are provided, when I select an answer (static) and go to the next page, then the question title contains the label text of the answer I selected", () => { - $(MandatoryRadioPage.googleLtd()).click(); - $(MandatoryRadioPage.submit()).click(); - expect($(ConfirmationQuestionRadioBlockPage.questionText()).getText()).to.contain("Google LTD"); + it("Given radio options are provided, when I select an answer (static) and go to the next page, then the question title contains the label text of the answer I selected", async () => { + await $(MandatoryRadioPage.googleLtd()).click(); + await $(MandatoryRadioPage.submit()).scrollIntoView(); + await click(MandatoryRadioPage.submit()); + await expect(await $(ConfirmationQuestionRadioBlockPage.questionText()).getText()).toContain("Google LTD"); }); }); diff --git a/tests/functional/spec/features/placeholder/playback_confirmation.spec.js b/tests/functional/spec/features/placeholder/playback_confirmation.spec.js index 881b57289f..fe5017b2df 100644 --- a/tests/functional/spec/features/placeholder/playback_confirmation.spec.js +++ b/tests/functional/spec/features/placeholder/playback_confirmation.spec.js @@ -1,15 +1,16 @@ import MandatoryCheckboxPage from "../../../generated_pages/placeholder_playback_list/mandatory-checkbox.page"; - +import { click } from "../../../helpers"; describe("Feature: Playback Confirmation", () => { - beforeEach("Open the schema", () => { - browser.openQuestionnaire("test_placeholder_playback_list.json"); + beforeEach("Open the schema", async () => { + await browser.openQuestionnaire("test_placeholder_playback_list.json"); }); - it("When the user submits an answer, their answers should be shown on the confirmation screen", () => { - $(MandatoryCheckboxPage.cheese()).click(); - $(MandatoryCheckboxPage.ham()).click(); - $(MandatoryCheckboxPage.submit()).click(); + it("When the user submits an answer, their answers should be shown on the confirmation screen", async () => { + await $(MandatoryCheckboxPage.cheese()).click(); + await $(MandatoryCheckboxPage.ham()).click(); + await click(MandatoryCheckboxPage.submit()); - expect($("#confirm-answers-question ul").getHTML()).to.contain("Cheese").to.contain("Ham"); + await expect(await $("#confirm-answers-question ul").getHTML()).toContain("Ham"); + await expect(await $("#confirm-answers-question ul").getHTML()).toContain("Cheese"); }); }); diff --git a/tests/functional/spec/features/question_summary/custom_question_summary.spec.js b/tests/functional/spec/features/question_summary/custom_question_summary.spec.js deleted file mode 100644 index e47b523f94..0000000000 --- a/tests/functional/spec/features/question_summary/custom_question_summary.spec.js +++ /dev/null @@ -1,38 +0,0 @@ -import AddressBlockPage from "../../../generated_pages/custom_question_summary/address.page.js"; -import AgeBlock from "../../../generated_pages/custom_question_summary/age.page.js"; -import NameBlockPage from "../../../generated_pages/custom_question_summary/name.page.js"; -import SubmitPage from "../../../generated_pages/custom_question_summary/submit.page.js"; - -describe("Summary Screen", () => { - beforeEach("Load the survey", () => { - browser.openQuestionnaire("test_custom_question_summary.json"); - }); - - it("Given a survey has question summary concatenations and has been completed when on the summary page then the correct response should be displayed formatted correctly", () => { - completeAllQuestions(); - expect($(SubmitPage.summaryRowState("name-question-concatenated-answer")).getText()).to.contain("John Smith"); - expect($(SubmitPage.summaryRowState("address-question-concatenated-answer")).getText()).to.contain("Cardiff Road\nNewport\nNP10 8XG"); - expect($(SubmitPage.summaryRowState("age-question-concatenated-answer")).getText()).to.contain("7\nThis age is an estimate"); - }); - - it("Given no values are entered in a question with multiple answers and concatenation set, when on the summary screen then the correct response should be displayed", () => { - $(NameBlockPage.submit()).click(); - $(AddressBlockPage.submit()).click(); - $(AgeBlock.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); - expect($(SubmitPage.summaryRowState("name-question-concatenated-answer")).getText()).to.contain("No answer provided"); - }); - - function completeAllQuestions() { - $(NameBlockPage.first()).setValue("John"); - $(NameBlockPage.last()).setValue("Smith"); - $(NameBlockPage.submit()).click(); - $(AddressBlockPage.line1()).setValue("Cardiff Road"); - $(AddressBlockPage.townCity()).setValue("Newport"); - $(AddressBlockPage.postcode()).setValue("NP10 8XG"); - $(AddressBlockPage.submit()).click(); - $(AgeBlock.number()).setValue(7); - $(AgeBlock.singleCheckboxThisAgeIsAnEstimate()).click(); - $(AgeBlock.submit()).click(); - } -}); diff --git a/tests/functional/spec/features/repeating_sections/repeating_sections_with_hub_and_spoke.spec.js b/tests/functional/spec/features/repeating_sections/repeating_sections_with_hub_and_spoke.spec.js deleted file mode 100644 index 438f4b9f65..0000000000 --- a/tests/functional/spec/features/repeating_sections/repeating_sections_with_hub_and_spoke.spec.js +++ /dev/null @@ -1,295 +0,0 @@ -import ConfirmDateOfBirthPage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/confirm-dob.page"; -import DateOfBirthPage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/date-of-birth.page"; -import FirstListCollectorAddPage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/list-collector-add.page"; -import FirstListCollectorPage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/list-collector.page"; -import HubPage from "../../../base_pages/hub.page.js"; -import PersonalDetailsSummaryPage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/personal-details-section-summary.page"; -import PrimaryPersonAddPage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/primary-person-list-collector-add.page"; -import PrimaryPersonPage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/primary-person-list-collector.page"; -import ProxyPage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/proxy.page"; -import SecondListCollectorAddPage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/another-list-collector-block-add.page"; -import SecondListCollectorInterstitialPage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/next-interstitial.page"; -import SecondListCollectorPage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/another-list-collector-block.page"; -import SectionSummaryPage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/section-summary.page.js"; -import SexPage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/sex.page"; -import VisitorsDateOfBirthPage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/visitors-date-of-birth.page"; -import VisitorsListCollectorAddPage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/visitors-block-add.page"; -import VisitorsListCollectorPage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/visitors-block.page"; -import VisitorsListCollectorRemovePage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/visitors-block-remove.page"; - -describe("Feature: Repeating Sections with Hub and Spoke", () => { - describe("Given the user has added some members to the household and is on the Hub", () => { - before("Open survey and add household members", () => { - browser.openQuestionnaire("test_repeating_sections_with_hub_and_spoke.json"); - // Accept cookies, this is done due to headless window size where cookie banner - // is pushing the submit button outside window - $(HubPage.acceptCookies()).click(); - // Ensure we are on the Hub - expect(browser.getUrl()).to.contain(HubPage.url()); - // Ensure the first section is not started - expect($(HubPage.summaryRowState("section")).getText()).to.equal("Not started"); - // Start first section to add household members - $(HubPage.summaryRowLink("section")).click(); - - // Add a primary person - $(PrimaryPersonPage.yes()).click(); - $(PrimaryPersonPage.submit()).click(); - $(PrimaryPersonAddPage.firstName()).setValue("Marcus"); - $(PrimaryPersonAddPage.lastName()).setValue("Twin"); - $(PrimaryPersonPage.submit()).click(); - - // Add other household members (First list collector) - $(FirstListCollectorPage.yes()).click(); - $(FirstListCollectorPage.submit()).click(); - $(FirstListCollectorAddPage.firstName()).setValue("Jean"); - $(FirstListCollectorAddPage.lastName()).setValue("Clemens"); - $(FirstListCollectorAddPage.submit()).click(); - - $(FirstListCollectorPage.yes()).click(); - $(FirstListCollectorPage.submit()).click(); - $(FirstListCollectorAddPage.firstName()).setValue("Samuel"); - $(FirstListCollectorAddPage.lastName()).setValue("Clemens"); - $(FirstListCollectorAddPage.submit()).click(); - - // Go to second list collector - $(FirstListCollectorPage.no()).click(); - $(FirstListCollectorPage.submit()).click(); - $(SecondListCollectorInterstitialPage.submit()).click(); - - // Add other household members (Second list collector) - $(SecondListCollectorPage.yes()).click(); - $(SecondListCollectorPage.submit()).click(); - $(SecondListCollectorAddPage.firstName()).setValue("John"); - $(SecondListCollectorAddPage.lastName()).setValue("Doe"); - $(SecondListCollectorAddPage.submit()).click(); - - // Go back to the Hub - $(SecondListCollectorPage.no()).click(); - $(SecondListCollectorPage.submit()).click(); - $(VisitorsListCollectorPage.no()).click(); - $(VisitorsListCollectorPage.submit()).click(); - }); - - beforeEach("Navigate to the Hub", () => browser.url(HubPage.url())); - - it("Then a section for each household member should be displayed", () => { - expect(browser.getUrl()).to.contain(HubPage.url()); - - expect($(HubPage.summaryRowState("section")).getText()).to.equal("Completed"); - expect($(HubPage.summaryRowTitle("personal-details-section-1")).getText()).to.equal("Marcus Twin"); - expect($(HubPage.summaryRowState("personal-details-section-1")).getText()).to.equal("Not started"); - expect($(HubPage.summaryRowState("personal-details-section-2")).getText()).to.equal("Not started"); - expect($(HubPage.summaryRowTitle("personal-details-section-2")).getText()).to.equal("Jean Clemens"); - expect($(HubPage.summaryRowState("personal-details-section-3")).getText()).to.equal("Not started"); - expect($(HubPage.summaryRowTitle("personal-details-section-3")).getText()).to.equal("Samuel Clemens"); - expect($(HubPage.summaryRowState("personal-details-section-4")).getText()).to.equal("Not started"); - expect($(HubPage.summaryRowTitle("personal-details-section-4")).getText()).to.equal("John Doe"); - - expect($(HubPage.summaryRowState("section-5")).isExisting()).to.be.false; - }); - - it("When the user starts a repeating section and clicks the Previous link on the first question, Then they should be taken back to the Hub", () => { - $(HubPage.summaryRowLink("personal-details-section-2")).click(); - $(ProxyPage.previous()).click(); - - expect(browser.getUrl()).to.contain(HubPage.url()); - }); - - it("When the user partially completes a repeating section, Then that section should be marked as 'Partially completed' on the Hub", () => { - $(HubPage.summaryRowLink("personal-details-section-1")).click(); - $(ProxyPage.yes()).click(); - $(ProxyPage.submit()).click(); - - $(DateOfBirthPage.day()).setValue("01"); - $(DateOfBirthPage.month()).setValue("03"); - $(DateOfBirthPage.year()).setValue("2000"); - $(DateOfBirthPage.submit()).click(); - - $(ConfirmDateOfBirthPage.confirmDateOfBirthYesPersonNameIsAgeOld()).click(); - $(ConfirmDateOfBirthPage.submit()).click(); - - browser.url(HubPage.url()); - - expect(browser.getUrl()).to.contain(HubPage.url()); - expect($(HubPage.summaryRowState("personal-details-section-1")).getText()).to.equal("Partially completed"); - }); - - it("When the user continues with a partially completed repeating section, Then they are taken to the first incomplete block", () => { - $(HubPage.summaryRowLink("personal-details-section-1")).click(); - - expect($(SexPage.questionText()).getText()).to.equal("What is Marcus Twin’s sex?"); - }); - - it("When the user completes a repeating section, Then that section should be marked as 'Completed' on the Hub", () => { - $(HubPage.summaryRowLink("personal-details-section-2")).click(); - $(ProxyPage.yes()).click(); - $(ProxyPage.submit()).click(); - - $(DateOfBirthPage.day()).setValue("09"); - $(DateOfBirthPage.month()).setValue("09"); - $(DateOfBirthPage.year()).setValue("1995"); - $(DateOfBirthPage.submit()).click(); - - $(ConfirmDateOfBirthPage.confirmDateOfBirthYesPersonNameIsAgeOld()).click(); - $(ConfirmDateOfBirthPage.submit()).click(); - - $(SexPage.female()).click(); - $(SexPage.submit()).click(); - - $(PersonalDetailsSummaryPage.submit()).click(); - - expect(browser.getUrl()).to.contain(HubPage.url()); - expect($(HubPage.summaryRowState("personal-details-section-2")).getText()).to.equal("Completed"); - }); - - it("When the user clicks 'View answers' for a completed repeating section, Then they are taken to the summary", () => { - $(HubPage.summaryRowLink("personal-details-section-2")).click(); - expect(browser.getUrl()).to.contain("/sections/personal-details-section"); - }); - - it("When the user views the summary for a repeating section, Then the page title is shown", () => { - $(HubPage.summaryRowLink("personal-details-section-2")).click(); - expect(browser.getTitle()).to.equal("â€Ļ - Hub & Spoke"); - }); - - it("When the user adds 2 visitors to the household then a section for each visitor should be display on the hub", () => { - // Ensure no other sections exist - expect($(HubPage.summaryRowState("personal-details-section-5")).isExisting()).to.be.false; - expect($(HubPage.summaryRowState("visitors-section-1")).isExisting()).to.be.false; - - // Start section for first visitor - $(HubPage.summaryRowLink("section")).click(); - - // Add first visitor - $(SectionSummaryPage.visitorListAddLink()).click(); - $(VisitorsListCollectorAddPage.firstName()).setValue("Joe"); - $(VisitorsListCollectorAddPage.lastName()).setValue("Public"); - $(VisitorsListCollectorAddPage.submit()).click(); - - // Add second visitor - $(SectionSummaryPage.visitorListAddLink()).click(); - $(VisitorsListCollectorAddPage.firstName()).setValue("Yvonne"); - $(VisitorsListCollectorAddPage.lastName()).setValue("Yoe"); - $(VisitorsListCollectorAddPage.submit()).click(); - $(SectionSummaryPage.submit()).click(); - - expect($(HubPage.summaryRowState("visitors-section-1")).getText()).to.equal("Not started"); - expect($(HubPage.summaryRowTitle("visitors-section-1")).getText()).to.equal("Joe Public"); - expect($(HubPage.summaryRowState("visitors-section-2")).getText()).to.equal("Not started"); - expect($(HubPage.summaryRowTitle("visitors-section-2")).getText()).to.equal("Yvonne Yoe"); - - expect($(HubPage.summaryRowState("visitors-section-3")).isExisting()).to.be.false; - }); - - it("When the user clicks 'Continue' from the Hub, Then they should progress to the first incomplete section", () => { - $(HubPage.submit()).click(); - expect($(ConfirmDateOfBirthPage.questionText()).getText()).to.equal("What is Marcus Twin’s sex?"); - }); - - it("When the user answers on their behalf, Then they are shown the non proxy question variant", () => { - $(HubPage.summaryRowLink("personal-details-section-4")).click(); - $(ProxyPage.noIMAnsweringForMyself()).click(); - $(ProxyPage.submit()).click(); - - $(DateOfBirthPage.day()).setValue("07"); - $(DateOfBirthPage.month()).setValue("07"); - $(DateOfBirthPage.year()).setValue("1970"); - $(DateOfBirthPage.submit()).click(); - - $(ConfirmDateOfBirthPage.confirmDateOfBirthYesIAmAgeOld()).click(); - $(ConfirmDateOfBirthPage.submit()).click(); - - expect($(SexPage.questionText()).getText()).to.equal("What is your sex?"); - }); - - it("When the user answers on on behalf of someone else, Then they are shown the proxy question variant for the relevant repeating section", () => { - $(HubPage.summaryRowLink("personal-details-section-3")).click(); - $(ProxyPage.yes()).click(); - $(ProxyPage.submit()).click(); - - $(DateOfBirthPage.day()).setValue("11"); - $(DateOfBirthPage.month()).setValue("11"); - $(DateOfBirthPage.year()).setValue("1990"); - $(DateOfBirthPage.submit()).click(); - - $(ConfirmDateOfBirthPage.confirmDateOfBirthYesPersonNameIsAgeOld()).click(); - $(ConfirmDateOfBirthPage.submit()).click(); - expect($(SexPage.questionText()).getText()).to.equal("What is Samuel Clemens’ sex?"); - }); - - it("When the user completes all sections, Then the Hub should be in the completed state", () => { - // Complete remaining sections - $(HubPage.submit()).click(); - $(SexPage.male()).click(); - $(SexPage.submit()).click(); - $(PersonalDetailsSummaryPage.submit()).click(); - - $(HubPage.submit()).click(); - $(SexPage.submit()).click(); - $(PersonalDetailsSummaryPage.submit()).click(); - - $(HubPage.submit()).click(); - $(SexPage.female()).click(); - $(SexPage.submit()).click(); - $(PersonalDetailsSummaryPage.submit()).click(); - - $(HubPage.submit()).click(); - $(VisitorsDateOfBirthPage.day()).setValue("03"); - $(VisitorsDateOfBirthPage.month()).setValue("09"); - $(VisitorsDateOfBirthPage.year()).setValue("1975"); - $(VisitorsDateOfBirthPage.submit()).click(); - - $(HubPage.submit()).click(); - $(VisitorsDateOfBirthPage.day()).setValue("31"); - $(VisitorsDateOfBirthPage.month()).setValue("07"); - $(VisitorsDateOfBirthPage.year()).setValue("1999"); - $(VisitorsDateOfBirthPage.submit()).click(); - - expect($(HubPage.submit()).getText()).to.equal("Submit survey"); - expect($(HubPage.heading()).getText()).to.equal("Submit survey"); - }); - - it("When the user adds a new visitor, Then the Hub should not be in the completed state", () => { - $(HubPage.summaryRowLink("section")).click(); - - // Add another visitor - $(SectionSummaryPage.visitorListAddLink()).click(); - $(VisitorsListCollectorAddPage.firstName()).setValue("Anna"); - $(VisitorsListCollectorAddPage.lastName()).setValue("Doe"); - $(VisitorsListCollectorAddPage.submit()).click(); - $(SectionSummaryPage.submit()).click(); - - // New visitor added to hub - expect($(HubPage.summaryRowState("visitors-section-3")).getText()).to.equal("Not started"); - expect($(HubPage.summaryRowState("visitors-section-3")).isExisting()).to.be.true; - - expect($(HubPage.submit()).getText()).to.not.equal("Submit survey"); - expect($(HubPage.submit()).getText()).to.equal("Continue"); - - expect($(HubPage.heading()).getText()).to.not.equal("Submit survey"); - expect($(HubPage.heading()).getText()).to.equal("Choose another section to complete"); - }); - - it("When the user removes a visitor, Then their section is not longer displayed on he Hub", () => { - // Ensure final householder exists - expect($(HubPage.summaryRowState("visitors-section-3")).isExisting()).to.be.true; - - $(HubPage.summaryRowLink("section")).click(); - - // Remove final visitor - $(SectionSummaryPage.visitorListRemoveLink(3)).click(); - - $(VisitorsListCollectorRemovePage.yes()).click(); - $(VisitorsListCollectorPage.submit()).click(); - $(SectionSummaryPage.submit()).click(); - - // Ensure final householder no longer exists - expect($(HubPage.summaryRowState("visitors-section-3")).isExisting()).to.be.false; - }); - - it("When the user submits, it should show the thank you page", () => { - $(HubPage.submit()).click(); - expect(browser.getUrl()).to.contain("thank-you"); - }); - }); -}); diff --git a/tests/functional/spec/features/routing/all_in.spec.js b/tests/functional/spec/features/routing/all_in.spec.js deleted file mode 100644 index b4b45f96fa..0000000000 --- a/tests/functional/spec/features/routing/all_in.spec.js +++ /dev/null @@ -1,31 +0,0 @@ -import CountryCheckboxPage from "../../../generated_pages/new_routing_checkbox_contains_all/country-checkbox.page"; -import CountryInterstitialPage from "../../../generated_pages/new_routing_checkbox_contains_all/country-interstitial-india-and-malta.page"; -import CountryInterstitialOtherPage from "../../../generated_pages/new_routing_checkbox_contains_all/country-interstitial-not-india-and-malta.page"; - -describe("Feature: Routing - ALL-IN Operator", () => { - describe("Equals", () => { - describe("Given I start the ALL-IN operator routing survey", () => { - beforeEach(() => { - browser.openQuestionnaire("test_new_routing_checkbox_contains_all.json"); - }); - - it("When I do select India and Malta, Then I should be routed to the correct answer interstitial page", () => { - $(CountryCheckboxPage.india()).click(); - $(CountryCheckboxPage.malta()).click(); - $(CountryCheckboxPage.submit()).click(); - expect(browser.getUrl()).to.contain(CountryInterstitialPage.pageName); - }); - it("When I do select India only, Then I should be routed to the correct answer interstitial page", () => { - $(CountryCheckboxPage.india()).click(); - $(CountryCheckboxPage.submit()).click(); - expect(browser.getUrl()).to.contain(CountryInterstitialOtherPage.pageName); - }); - - it("When I do not select India or Malta, Then I should be routed to the incorrect answer interstitial page", () => { - $(CountryCheckboxPage.liechtenstein()).click(); - $(CountryCheckboxPage.submit()).click(); - expect(browser.getUrl()).to.contain(CountryInterstitialOtherPage.pageName); - }); - }); - }); -}); diff --git a/tests/functional/spec/features/routing/and.spec.js b/tests/functional/spec/features/routing/and.spec.js deleted file mode 100644 index 37dace104c..0000000000 --- a/tests/functional/spec/features/routing/and.spec.js +++ /dev/null @@ -1,46 +0,0 @@ -import FirstNumberQuestionPage from "../../../generated_pages/new_routing_and/number-question-1.page"; -import SecondNumberQuestionPage from "../../../generated_pages/new_routing_and/number-question-2.page"; -import CorrectAnswerPage from "../../../generated_pages/new_routing_and/correct-answer.page"; -import IncorrectAnswerPage from "../../../generated_pages/new_routing_and/incorrect-answer.page"; - -describe("Feature: Routing - And Operator", () => { - describe("Equals", () => { - describe("Given I start the and operator routing survey", () => { - beforeEach(() => { - browser.openQuestionnaire("test_new_routing_and.json"); - }); - - it("When I enter both answers correctly with 123 and 321, Then I should be routed to the correct page", () => { - $(FirstNumberQuestionPage.answer1()).setValue(123); - $(FirstNumberQuestionPage.submit()).click(); - $(SecondNumberQuestionPage.answer2()).setValue(321); - $(SecondNumberQuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(CorrectAnswerPage.pageName); - }); - - it("When I only enter the second answer correctly with 555 and 321, Then I should be routed to the incorrect page", () => { - $(FirstNumberQuestionPage.answer1()).setValue(555); - $(FirstNumberQuestionPage.submit()).click(); - $(SecondNumberQuestionPage.answer2()).setValue(321); - $(SecondNumberQuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(IncorrectAnswerPage.pageName); - }); - - it("When I only enter the first answer correctly with 123 and 555, Then I should be routed to the incorrect page", () => { - $(FirstNumberQuestionPage.answer1()).setValue(123); - $(FirstNumberQuestionPage.submit()).click(); - $(SecondNumberQuestionPage.answer2()).setValue(555); - $(SecondNumberQuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(IncorrectAnswerPage.pageName); - }); - - it("When I answer both questions incorrectly with 555 and 444, Then I should be routed to the incorrect page", () => { - $(FirstNumberQuestionPage.answer1()).setValue(555); - $(FirstNumberQuestionPage.submit()).click(); - $(SecondNumberQuestionPage.answer2()).setValue(444); - $(SecondNumberQuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(IncorrectAnswerPage.pageName); - }); - }); - }); -}); diff --git a/tests/functional/spec/features/routing/answer_comparison_routing.spec.js b/tests/functional/spec/features/routing/answer_comparison_routing.spec.js deleted file mode 100644 index bce424e98a..0000000000 --- a/tests/functional/spec/features/routing/answer_comparison_routing.spec.js +++ /dev/null @@ -1,32 +0,0 @@ -import RouteComparison1Page from "../../../generated_pages/new_routing_answer_comparison/route-comparison-1.page.js"; -import RouteComparison2Page from "../../../generated_pages/new_routing_answer_comparison/route-comparison-2.page.js"; - -describe("Test routing skip", () => { - beforeEach(() => { - browser.openQuestionnaire("test_new_routing_answer_comparison.json"); - }); - - it("Given we start the routing test survey, When we enter a low number then a high number, Then, we should be routed to the fourth page", () => { - $(RouteComparison1Page.answer()).setValue(1); - $(RouteComparison1Page.submit()).click(); - $(RouteComparison2Page.answer()).setValue(2); - $(RouteComparison2Page.submit()).click(); - expect($("#main-content > p").getText()).to.contain("This page should never be skipped"); - }); - - it("Given we start the routing test survey, When we enter a high number then a low number, Then, we should be routed to the third page", () => { - $(RouteComparison1Page.answer()).setValue(1); - $(RouteComparison1Page.submit()).click(); - $(RouteComparison2Page.answer()).setValue(0); - $(RouteComparison2Page.submit()).click(); - expect($("#main-content > p").getText()).to.contain("This page should be skipped if your second answer was higher than your first"); - }); - - it("Given we start the routing test survey, When we enter an equal number on both questions, Then, we should be routed to the third page", () => { - $(RouteComparison1Page.answer()).setValue(1); - $(RouteComparison1Page.submit()).click(); - $(RouteComparison2Page.answer()).setValue(1); - $(RouteComparison2Page.submit()).click(); - expect($("#main-content > p").getText()).to.contain("This page should be skipped if your second answer was higher than your first"); - }); -}); diff --git a/tests/functional/spec/features/routing/answer_not_on_path.spec.js b/tests/functional/spec/features/routing/answer_not_on_path.spec.js deleted file mode 100644 index 20e1fc15b9..0000000000 --- a/tests/functional/spec/features/routing/answer_not_on_path.spec.js +++ /dev/null @@ -1,37 +0,0 @@ -import InitialChoicePage from "../../../generated_pages/new_routing_not_affected_by_answers_not_on_path/initial-choice.page.js"; -import InvalidPathPage from "../../../generated_pages/new_routing_not_affected_by_answers_not_on_path/invalid-path.page.js"; -import InvalidPathInterstitialPage from "../../../generated_pages/new_routing_not_affected_by_answers_not_on_path/invalid-path-interstitial.page.js"; -import ValidPathPage from "../../../generated_pages/new_routing_not_affected_by_answers_not_on_path/valid-path.page.js"; -import ValidFinalInterstitialPage from "../../../generated_pages/new_routing_not_affected_by_answers_not_on_path/valid-final-interstitial.page.js"; - -describe("Answers not on path are not considered when routing", () => { - beforeEach(() => { - browser.openQuestionnaire("test_new_routing_not_affected_by_answers_not_on_path.json"); - }); - - it("Given the user enters an answer on the first path, when they return to the second path, they should be routed to the valid path interstitial", () => { - $(InitialChoicePage.goHereFirst()).click(); - $(InitialChoicePage.submit()).click(); - - expect(browser.getUrl()).to.contain(InvalidPathPage.pageName); - $(InvalidPathPage.answer()).setValue(123); - $(InvalidPathPage.submit()).click(); - - // We now have an answer in the store on the 'invalid' path - - expect(browser.getUrl()).to.contain(InvalidPathInterstitialPage.pageName); - $(InvalidPathInterstitialPage.previous()).click(); - $(InvalidPathPage.previous()).click(); - - // Take the second route - - $(InitialChoicePage.goHereSecond()).click(); - $(InitialChoicePage.submit()).click(); - - $(ValidPathPage.answer()).setValue(321); - $(ValidPathPage.submit()).click(); - - // We should be routed to the valid interstitial page since the invalid path answer should not be considered whilst routing. - expect(browser.getUrl()).to.contain(ValidFinalInterstitialPage.pageName); - }); -}); diff --git a/tests/functional/spec/features/routing/answered_unanswered.spec.js b/tests/functional/spec/features/routing/answered_unanswered.spec.js deleted file mode 100644 index 8044aace3e..0000000000 --- a/tests/functional/spec/features/routing/answered_unanswered.spec.js +++ /dev/null @@ -1,96 +0,0 @@ -import QuestionOne from "../../../generated_pages/new_routing_answered_unanswered/block-1.page"; -import QuestionOneAnswered from "../../../generated_pages/new_routing_answered_unanswered/answered-question-1.page"; -import QuestionOneUnanswered from "../../../generated_pages/new_routing_answered_unanswered/unanswered-question-1.page"; - -import QuestionTwo from "../../../generated_pages/new_routing_answered_unanswered/block-2.page"; -import QuestionTwoAnswered from "../../../generated_pages/new_routing_answered_unanswered/answered-question-2.page"; -import QuestionTwoUnanswered from "../../../generated_pages/new_routing_answered_unanswered/unanswered-question-2.page"; - -import QuestionThree from "../../../generated_pages/new_routing_answered_unanswered/block-3.page"; -import QuestionThreeAnsweredOrNotZero from "../../../generated_pages/new_routing_answered_unanswered/answered-question-3.page"; -import QuestionThreeUnansweredOrAnswerZero from "../../../generated_pages/new_routing_answered_unanswered/unanswered-or-zero-question-3.page"; - -describe("Test routing question answered/unanswered", () => { - describe("Given I am on the first question", () => { - beforeEach("Load the questionnaire", () => { - browser.openQuestionnaire("test_new_routing_answered_unanswered.json"); - }); - - it("When I select any answer and submit, Then I should see a page saying I have answered the first question", () => { - $(QuestionOne.ham()).click(); - $(QuestionOne.submit()).click(); - expect($(QuestionOneAnswered.heading()).getText()).to.contain("You answered the first question!"); - expect(browser.getUrl()).to.contain(QuestionOneAnswered.pageName); - - $(QuestionOneAnswered.previous()).click(); - $(QuestionOne.cheese()).click(); - $(QuestionOne.submit()).click(); - expect($(QuestionOneAnswered.heading()).getText()).to.contain("You answered the first question!"); - expect(browser.getUrl()).to.contain(QuestionOneAnswered.pageName); - }); - - it("When I don't select an answer and submit, Then I should see a page saying I have not answered the first question", () => { - $(QuestionOne.submit()).click(); - expect($(QuestionOneAnswered.heading()).getText()).to.contain("You did not answer the first question!"); - expect(browser.getUrl()).to.contain(QuestionOneAnswered.pageName); - }); - }); - - describe("Given I am on the second question", () => { - beforeEach("Load the questionnaire and get to the second question", () => { - browser.openQuestionnaire("test_new_routing_answered_unanswered.json"); - - $(QuestionOne.submit()).click(); - $(QuestionOneUnanswered.submit()).click(); - }); - - it("When I select any answer and submit, Then I should see a page saying I have answered the second question", () => { - $(QuestionTwo.pizzaHut()).click(); - $(QuestionTwo.submit()).click(); - expect($(QuestionTwoAnswered.heading()).getText()).to.contain("You answered the second question!"); - expect(browser.getUrl()).to.contain(QuestionTwoAnswered.pageName); - - $(QuestionOneAnswered.previous()).click(); - $(QuestionTwo.dominoS()).click(); - $(QuestionTwo.submit()).click(); - expect($(QuestionTwoAnswered.heading()).getText()).to.contain("You answered the second question!"); - expect(browser.getUrl()).to.contain(QuestionTwoAnswered.pageName); - }); - - it("When I don't select an answer and submit, Then I should see a page saying I have not answered the second question", () => { - $(QuestionTwo.submit()).click(); - expect($(QuestionTwoUnanswered.heading()).getText()).to.contain("You did not answer the second question!"); - expect(browser.getUrl()).to.contain(QuestionTwoAnswered.pageName); - }); - }); - - describe("Given I am on the third question", () => { - beforeEach("Load the questionnaire and get to the third question", () => { - browser.openQuestionnaire("test_new_routing_answered_unanswered.json"); - - $(QuestionOne.submit()).click(); - $(QuestionOneUnanswered.submit()).click(); - $(QuestionTwo.submit()).click(); - $(QuestionTwoUnanswered.submit()).click(); - }); - - it("When I do not answer the question or answer `0` and submit, Then I should see a page saying I did not answer the question or that I chose `0`", () => { - $(QuestionThree.submit()).click(); - expect($(QuestionThreeUnansweredOrAnswerZero.heading()).getText()).to.contain("You did not answer the question or chose 0 slices"); - expect(browser.getUrl()).to.contain(QuestionThreeUnansweredOrAnswerZero.pageName); - - $(QuestionThreeUnansweredOrAnswerZero.previous()).click(); - $(QuestionThree.answer3()).setValue("0"); - $(QuestionThree.submit()).click(); - expect($(QuestionThreeUnansweredOrAnswerZero.heading()).getText()).to.contain("You did not answer the question or chose 0 slices"); - expect(browser.getUrl()).to.contain(QuestionThreeUnansweredOrAnswerZero.pageName); - }); - - it("When I enter an answer greater than 0 and submit, Then I should see a page saying I chose at least one", () => { - $(QuestionThree.answer3()).setValue("2"); - $(QuestionThree.submit()).click(); - expect($(QuestionTwoAnswered.heading()).getText()).to.contain("You chose at least 1 slice"); - expect(browser.getUrl()).to.contain(QuestionThreeAnsweredOrNotZero.pageName); - }); - }); -}); diff --git a/tests/functional/spec/features/routing/any_in.spec.js b/tests/functional/spec/features/routing/any_in.spec.js deleted file mode 100644 index 44849820ec..0000000000 --- a/tests/functional/spec/features/routing/any_in.spec.js +++ /dev/null @@ -1,31 +0,0 @@ -import CountryCheckboxPage from "../../../generated_pages/new_routing_checkbox_contains_any/country-checkbox.page"; -import CountryInterstitialPage from "../../../generated_pages/new_routing_checkbox_contains_any/country-interstitial-india-or-malta-or-both.page"; -import CountryInterstitialOtherPage from "../../../generated_pages/new_routing_checkbox_contains_any/country-interstitial-not-india-or-malta-or-both.page"; - -describe("Feature: Routing - ANY-IN Operator", () => { - describe("Equals", () => { - describe("Given I start the ANY-IN operator routing survey", () => { - beforeEach(() => { - browser.openQuestionnaire("test_new_routing_checkbox_contains_any.json"); - }); - - it("When I do select India and Malta, Then I should be routed to the correct answer interstitial page", () => { - $(CountryCheckboxPage.india()).click(); - $(CountryCheckboxPage.malta()).click(); - $(CountryCheckboxPage.submit()).click(); - expect(browser.getUrl()).to.contain(CountryInterstitialPage.pageName); - }); - - it("When I do select India or Malta, Then I should be routed to the correct answer interstitial page", () => { - $(CountryCheckboxPage.india()).click(); - $(CountryCheckboxPage.submit()).click(); - expect(browser.getUrl()).to.contain(CountryInterstitialPage.pageName); - }); - it("When I do not select India or Malta, Then I should be routed to the incorrect answer interstitial page", () => { - $(CountryCheckboxPage.liechtenstein()).click(); - $(CountryCheckboxPage.submit()).click(); - expect(browser.getUrl()).to.contain(CountryInterstitialOtherPage.pageName); - }); - }); - }); -}); diff --git a/tests/functional/spec/features/routing/checkbox_count.spec.js b/tests/functional/spec/features/routing/checkbox_count.spec.js deleted file mode 100644 index 12b36841a6..0000000000 --- a/tests/functional/spec/features/routing/checkbox_count.spec.js +++ /dev/null @@ -1,43 +0,0 @@ -import ToppingCheckboxPage from "../../../generated_pages/new_routing_checkbox_count/topping-checkbox.page.js"; -import CorrectAnswerPage from "../../../generated_pages/new_routing_checkbox_count/correct-answer.page"; -import IncorrectAnswerPage from "../../../generated_pages/new_routing_checkbox_count/incorrect-answer.page"; - -describe("Test routing using count of checkboxes checked", () => { - beforeEach(() => { - browser.openQuestionnaire("test_new_routing_checkbox_count.json"); - }); - - it("Given a user selects 2 checkboxes, When they submit, Then they should be routed to the correct page", () => { - $(ToppingCheckboxPage.cheese()).click(); - $(ToppingCheckboxPage.ham()).click(); - $(ToppingCheckboxPage.submit()).click(); - - expect(browser.getUrl()).to.contain(CorrectAnswerPage.pageName); - expect($(CorrectAnswerPage.questionText()).getText()).to.have.string("You selected 2 or more toppings"); - }); - - it("Given a user selects no checkboxes, When they submit, Then they should be routed to the incorrect page", () => { - $(ToppingCheckboxPage.submit()).click(); - - expect(browser.getUrl()).to.contain(IncorrectAnswerPage.pageName); - expect($(IncorrectAnswerPage.questionText()).getText()).to.have.string("You did not select 2 or more toppings"); - }); - - it("Given a user selects 1 checkbox, When they submit, Then they should be routed to the incorrect page", () => { - $(ToppingCheckboxPage.cheese()).click(); - $(ToppingCheckboxPage.submit()).click(); - - expect(browser.getUrl()).to.contain(IncorrectAnswerPage.pageName); - expect($(IncorrectAnswerPage.questionText()).getText()).to.have.string("You did not select 2 or more toppings"); - }); - - it("Given a user selects 3 checkbox, When they submit, Then they should be routed to the correct page", () => { - $(ToppingCheckboxPage.cheese()).click(); - $(ToppingCheckboxPage.ham()).click(); - $(ToppingCheckboxPage.pineapple()).click(); - $(ToppingCheckboxPage.submit()).click(); - - expect(browser.getUrl()).to.contain(CorrectAnswerPage.pageName); - expect($(CorrectAnswerPage.questionText()).getText()).to.have.string("You selected 2 or more toppings"); - }); -}); diff --git a/tests/functional/spec/features/routing/date.spec.js b/tests/functional/spec/features/routing/date.spec.js deleted file mode 100644 index 1835c1c000..0000000000 --- a/tests/functional/spec/features/routing/date.spec.js +++ /dev/null @@ -1,292 +0,0 @@ -import IncorrectAnswerPage from "../../../generated_pages/new_routing_date_equals/incorrect-answer.page.js"; -import CorrectAnswerPage from "../../../generated_pages/new_routing_date_equals/correct-answer.page.js"; - -import DateEqualsComparisonQuestionPage from "../../../generated_pages/new_routing_date_equals/comparison-date-block.page"; -import DateEqualsQuestionPage from "../../../generated_pages/new_routing_date_equals/date-question.page"; -import DateNotEqualsQuestionPage from "../../../generated_pages/new_routing_date_not_equals/date-question.page"; -import DateGreaterThanQuestionPage from "../../../generated_pages/new_routing_date_greater_than/date-question.page"; -import DateGreaterThanOrEqualsQuestionPage from "../../../generated_pages/new_routing_date_greater_than_or_equals/date-question.page"; -import DateLessThanQuestionPage from "../../../generated_pages/new_routing_date_less_than/date-question.page"; -import DateLessThanOrEqualsQuestionPage from "../../../generated_pages/new_routing_date_less_than_or_equals/date-question.page"; - -const today = new Date(); -const dayToday = today.getDate(); -const monthToday = today.getMonth() + 1; // January is 0! -const yearToday = today.getFullYear(); - -const yesterday = new Date(); -yesterday.setDate(today.getDate() - 1); -const dayYesterday = yesterday.getDate(); -const monthYesterday = yesterday.getMonth() + 1; -const yearYesterday = yesterday.getFullYear(); - -const tomorrow = new Date(); -tomorrow.setDate(today.getDate() + 1); -const dayTomorrow = tomorrow.getDate(); -const monthTomorrow = tomorrow.getMonth() + 1; -const yearTomorrow = tomorrow.getFullYear(); - -describe("Feature: Routing on a Date", () => { - describe("Equals", () => { - describe("Given I start date routing equals survey", () => { - beforeEach(() => { - browser.openQuestionnaire("test_new_routing_date_equals.json"); - - $(DateEqualsComparisonQuestionPage.day()).setValue(31); - $(DateEqualsComparisonQuestionPage.month()).setValue(3); - $(DateEqualsComparisonQuestionPage.year()).setValue(2020); - $(DateEqualsComparisonQuestionPage.submit()).click(); - }); - - it("When I enter the same date, Then I should be routed to the correct page", () => { - $(DateEqualsQuestionPage.day()).setValue(31); - $(DateEqualsQuestionPage.month()).setValue(3); - $(DateEqualsQuestionPage.year()).setValue(2020); - $(DateEqualsQuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(CorrectAnswerPage.pageName); - }); - - it("When I enter the yesterday date, Then I should be routed to the correct page", () => { - $(DateEqualsQuestionPage.day()).setValue(30); - $(DateEqualsQuestionPage.month()).setValue(3); - $(DateEqualsQuestionPage.year()).setValue(2020); - $(DateEqualsQuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(CorrectAnswerPage.pageName); - }); - - it("When I enter the tomorrow date, Then I should be routed to the correct page", () => { - $(DateEqualsQuestionPage.day()).setValue(1); - $(DateEqualsQuestionPage.month()).setValue(4); - $(DateEqualsQuestionPage.year()).setValue(2020); - $(DateEqualsQuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(CorrectAnswerPage.pageName); - }); - - it("When I enter the last month date, Then I should be routed to the correct page", () => { - $(DateEqualsQuestionPage.day()).setValue(29); - $(DateEqualsQuestionPage.month()).setValue(2); - $(DateEqualsQuestionPage.year()).setValue(2020); - $(DateEqualsQuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(CorrectAnswerPage.pageName); - }); - - it("When I enter the next month date, Then I should be routed to the correct page", () => { - $(DateEqualsQuestionPage.day()).setValue(30); - $(DateEqualsQuestionPage.month()).setValue(4); - $(DateEqualsQuestionPage.year()).setValue(2020); - $(DateEqualsQuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(CorrectAnswerPage.pageName); - }); - - it("When I enter the last year date, Then I should be routed to the correct page", () => { - $(DateEqualsQuestionPage.day()).setValue(31); - $(DateEqualsQuestionPage.month()).setValue(3); - $(DateEqualsQuestionPage.year()).setValue(2019); - $(DateEqualsQuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(CorrectAnswerPage.pageName); - }); - - it("When I enter the next year date, Then I should be routed to the correct page", () => { - $(DateEqualsQuestionPage.day()).setValue(31); - $(DateEqualsQuestionPage.month()).setValue(3); - $(DateEqualsQuestionPage.year()).setValue(2021); - $(DateEqualsQuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(CorrectAnswerPage.pageName); - }); - - it("When I enter an incorrect date, Then I should be routed to the incorrect page", () => { - $(DateEqualsQuestionPage.day()).setValue(1); - $(DateEqualsQuestionPage.month()).setValue(3); - $(DateEqualsQuestionPage.year()).setValue(2020); - $(DateEqualsComparisonQuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(CorrectAnswerPage.pageName); - }); - }); - }); - - describe("Not Equals", () => { - describe("Given I start date routing not equals survey", () => { - beforeEach(() => { - browser.openQuestionnaire("test_new_routing_date_not_equals.json"); - }); - - it("When I enter a different date to February 2018, Then I should be routed to the correct page", () => { - $(DateNotEqualsQuestionPage.Month()).setValue(3); - $(DateNotEqualsQuestionPage.Year()).setValue(2018); - $(DateNotEqualsQuestionPage.submit()).click(); - - const expectedUrl = browser.getUrl(); - - expect(expectedUrl).to.contain(CorrectAnswerPage.pageName); - }); - - it("When I enter February 2018, Then I should be routed to the incorrect page", () => { - $(DateNotEqualsQuestionPage.Month()).setValue(2); - $(DateNotEqualsQuestionPage.Year()).setValue(2018); - $(DateNotEqualsQuestionPage.submit()).click(); - - const expectedUrl = browser.getUrl(); - - expect(expectedUrl).to.contain(IncorrectAnswerPage.pageName); - }); - }); - }); - - describe("Greater Than", () => { - describe("Given I start date routing greater than survey", () => { - beforeEach(() => { - browser.openQuestionnaire("test_new_routing_date_greater_than.json"); - }); - - it("When I enter a date greater than the 1st March 2017, Then I should be routed to the correct page", () => { - $(DateGreaterThanQuestionPage.day()).setValue(2); - $(DateGreaterThanQuestionPage.month()).setValue(3); - $(DateGreaterThanQuestionPage.year()).setValue(2017); - $(DateGreaterThanQuestionPage.submit()).click(); - - const expectedUrl = browser.getUrl(); - - expect(expectedUrl).to.contain(CorrectAnswerPage.pageName); - }); - - it("When I enter the 1st March 2017, Then I should be routed to the incorrect page", () => { - $(DateGreaterThanQuestionPage.day()).setValue(1); - $(DateGreaterThanQuestionPage.month()).setValue(3); - $(DateGreaterThanQuestionPage.year()).setValue(2017); - $(DateGreaterThanQuestionPage.submit()).click(); - - const expectedUrl = browser.getUrl(); - - expect(expectedUrl).to.contain(CorrectAnswerPage.pageName); - }); - - it("When I enter a date less than the 1st March 2017, Then I should be routed to the incorrect page", () => { - $(DateGreaterThanQuestionPage.day()).setValue(28); - $(DateGreaterThanQuestionPage.month()).setValue(2); - $(DateGreaterThanQuestionPage.year()).setValue(2017); - $(DateGreaterThanQuestionPage.submit()).click(); - - const expectedUrl = browser.getUrl(); - - expect(expectedUrl).to.contain(IncorrectAnswerPage.pageName); - }); - }); - }); - - describe("Greater Than Or Equals", () => { - describe("Given I start date routing greater than or equals survey", () => { - beforeEach(() => { - browser.openQuestionnaire("test_new_routing_date_greater_than_or_equals.json"); - }); - - it("When I enter a date greater than 2017, Then I should be routed to the correct page", () => { - $(DateGreaterThanOrEqualsQuestionPage.Year()).setValue(2018); - $(DateGreaterThanOrEqualsQuestionPage.submit()).click(); - - const expectedUrl = browser.getUrl(); - - expect(expectedUrl).to.contain(CorrectAnswerPage.pageName); - }); - - it("When I enter 2017, Then I should be routed to the correct page", () => { - $(DateGreaterThanOrEqualsQuestionPage.Year()).setValue(2017); - $(DateGreaterThanOrEqualsQuestionPage.submit()).click(); - - const expectedUrl = browser.getUrl(); - - expect(expectedUrl).to.contain(CorrectAnswerPage.pageName); - }); - - it("When I enter a date less than March 2017, Then I should be routed to the incorrect page", () => { - $(DateGreaterThanOrEqualsQuestionPage.Year()).setValue(2016); - $(DateGreaterThanOrEqualsQuestionPage.submit()).click(); - - const expectedUrl = browser.getUrl(); - - expect(expectedUrl).to.contain(IncorrectAnswerPage.pageName); - }); - }); - }); - - describe("Less Than", () => { - describe("Given I start date routing less than survey", () => { - beforeEach(() => { - browser.openQuestionnaire("test_new_routing_date_less_than.json"); - }); - - it("When I enter a date less than today, Then I should be routed to the correct page", () => { - $(DateLessThanQuestionPage.day()).setValue(dayYesterday); - $(DateLessThanQuestionPage.month()).setValue(monthYesterday); - $(DateLessThanQuestionPage.year()).setValue(yearYesterday); - $(DateLessThanQuestionPage.submit()).click(); - - const browserUrl = browser.getUrl(); - - expect(browserUrl).to.contain(CorrectAnswerPage.pageName); - }); - - it("When I enter a date equal to today, Then I should be routed to the incorrect page", () => { - $(DateLessThanQuestionPage.day()).setValue(dayToday); - $(DateLessThanQuestionPage.month()).setValue(monthToday); - $(DateLessThanQuestionPage.year()).setValue(yearToday); - $(DateLessThanQuestionPage.submit()).click(); - - const browserUrl = browser.getUrl(); - - expect(browserUrl).to.contain(IncorrectAnswerPage.pageName); - }); - - it("When I enter a date greater than today, Then I should be routed to the incorrect page", () => { - $(DateLessThanQuestionPage.day()).setValue(dayTomorrow); - $(DateLessThanQuestionPage.month()).setValue(monthTomorrow); - $(DateLessThanQuestionPage.year()).setValue(yearTomorrow); - $(DateLessThanQuestionPage.submit()).click(); - - const browserUrl = browser.getUrl(); - - expect(browserUrl).to.contain(IncorrectAnswerPage.pageName); - }); - }); - }); - - describe("Less Than Or Equals", () => { - describe("Given I start date routing less than or equals survey", () => { - beforeEach(() => { - browser.openQuestionnaire("test_new_routing_date_less_than_or_equals.json"); - }); - - it("When I enter a date less than today, Then I should be routed to the correct page", () => { - $(DateLessThanOrEqualsQuestionPage.day()).setValue(dayYesterday); - $(DateLessThanOrEqualsQuestionPage.month()).setValue(monthYesterday); - $(DateLessThanOrEqualsQuestionPage.year()).setValue(yearYesterday); - $(DateLessThanOrEqualsQuestionPage.submit()).click(); - - const browserUrl = browser.getUrl(); - - expect(browserUrl).to.contain(CorrectAnswerPage.pageName); - }); - - it("When I enter a date equal to today, Then I should be routed to the correct page", () => { - $(DateLessThanOrEqualsQuestionPage.day()).setValue(dayToday); - $(DateLessThanOrEqualsQuestionPage.month()).setValue(monthToday); - $(DateLessThanOrEqualsQuestionPage.year()).setValue(yearToday); - $(DateLessThanOrEqualsQuestionPage.submit()).click(); - - const browserUrl = browser.getUrl(); - - expect(browserUrl).to.contain(CorrectAnswerPage.pageName); - }); - - it("When I enter a date greater than today, Then I should be routed to the incorrect page", () => { - $(DateLessThanOrEqualsQuestionPage.day()).setValue(dayTomorrow); - $(DateLessThanOrEqualsQuestionPage.month()).setValue(monthTomorrow); - $(DateLessThanOrEqualsQuestionPage.year()).setValue(yearTomorrow); - $(DateLessThanOrEqualsQuestionPage.submit()).click(); - - const browserUrl = browser.getUrl(); - - expect(browserUrl).to.contain(IncorrectAnswerPage.pageName); - }); - }); - }); -}); diff --git a/tests/functional/spec/features/routing/in.spec.js b/tests/functional/spec/features/routing/in.spec.js deleted file mode 100644 index d6abcfde89..0000000000 --- a/tests/functional/spec/features/routing/in.spec.js +++ /dev/null @@ -1,25 +0,0 @@ -import CountryCheckboxPage from "../../../generated_pages/new_routing_checkbox_contains/country-checkbox.page"; -import CountryInterstitialPage from "../../../generated_pages/new_routing_checkbox_contains/country-interstitial-india.page"; -import CountryInterstitialOtherPage from "../../../generated_pages/new_routing_checkbox_contains/country-interstitial-not-india.page"; - -describe("Feature: Routing - IN Operator", () => { - describe("Equals", () => { - describe("Given I start the IN operator routing survey", () => { - beforeEach(() => { - browser.openQuestionnaire("test_new_routing_checkbox_contains.json"); - }); - - it("When I do select India, Then I should be routed to the the correct answer interstitial page", () => { - $(CountryCheckboxPage.india()).click(); - $(CountryCheckboxPage.submit()).click(); - expect(browser.getUrl()).to.contain(CountryInterstitialPage.pageName); - }); - - it("When I do not select India, Then I should be routed to the the incorrect answer interstitial page", () => { - $(CountryCheckboxPage.liechtenstein()).click(); - $(CountryCheckboxPage.submit()).click(); - expect(browser.getUrl()).to.contain(CountryInterstitialOtherPage.pageName); - }); - }); - }); -}); diff --git a/tests/functional/spec/features/routing/not.spec.js b/tests/functional/spec/features/routing/not.spec.js deleted file mode 100644 index 24748b5a62..0000000000 --- a/tests/functional/spec/features/routing/not.spec.js +++ /dev/null @@ -1,25 +0,0 @@ -import CountryCheckboxPage from "../../../generated_pages/new_routing_not/country-checkbox.page"; -import CountryInterstitialPage from "../../../generated_pages/new_routing_not/country-interstitial-not-india.page"; -import IndiaInterstitialPage from "../../../generated_pages/new_routing_not/country-interstitial-india.page"; - -describe("Feature: Routing - Not Operator", () => { - describe("Equals", () => { - describe("Given I start the not operator routing survey", () => { - beforeEach(() => { - browser.openQuestionnaire("test_new_routing_not.json"); - }); - - it("When I do not select India, Then I should be routed to the not India interstitial page", () => { - $(CountryCheckboxPage.azerbaijan()).click(); - $(CountryCheckboxPage.submit()).click(); - expect(browser.getUrl()).to.contain(CountryInterstitialPage.pageName); - }); - - it("When I select India, Then I should be routed to the India interstitial page", () => { - $(CountryCheckboxPage.india()).click(); - $(CountryCheckboxPage.submit()).click(); - expect(browser.getUrl()).to.contain(IndiaInterstitialPage.pageName); - }); - }); - }); -}); diff --git a/tests/functional/spec/features/routing/number.spec.js b/tests/functional/spec/features/routing/number.spec.js deleted file mode 100644 index cfe000cf9a..0000000000 --- a/tests/functional/spec/features/routing/number.spec.js +++ /dev/null @@ -1,159 +0,0 @@ -import NumberQuestionPage from "../../../generated_pages/new_routing_number_equals/number-question.page"; -import CorrectAnswerPage from "../../../generated_pages/new_routing_number_equals/correct-answer.page"; -import IncorrectAnswerPage from "../../../generated_pages/new_routing_number_equals/incorrect-answer.page"; - -describe("Feature: Routing on a Number", () => { - describe("Equals", () => { - describe("Given I start number routing equals survey", () => { - before(() => { - browser.openQuestionnaire("test_new_routing_number_equals.json"); - }); - - it("When I enter 123, Then I should be routed to the correct page", () => { - $(NumberQuestionPage.answer()).setValue(123); - $(NumberQuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(CorrectAnswerPage.pageName); - }); - - it("When I enter a number that isn't 123, Then I should be routed to the incorrect page", () => { - $(CorrectAnswerPage.previous()).click(); - $(NumberQuestionPage.answer()).setValue(555); - $(NumberQuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(IncorrectAnswerPage.pageName); - }); - }); - }); - - describe("Not Equals", () => { - describe("Given I start number routing not equals survey", () => { - before(() => { - browser.openQuestionnaire("test_new_routing_number_not_equals.json"); - }); - - it("When I enter a number that isn't 123, Then I should be routed to the correct page", () => { - $(NumberQuestionPage.answer()).setValue(987); - $(NumberQuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(CorrectAnswerPage.pageName); - }); - - it("When I enter 123, Then I should be routed to the incorrect page", () => { - $(CorrectAnswerPage.previous()).click(); - $(NumberQuestionPage.answer()).setValue(123); - $(NumberQuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(IncorrectAnswerPage.pageName); - }); - }); - }); - - describe("Greater Than", () => { - describe("Given I start number routing greater than survey", () => { - before(() => { - browser.openQuestionnaire("test_new_routing_number_greater_than.json"); - }); - - it("When I enter a number greater than 123, Then I should be routed to the correct page", () => { - $(NumberQuestionPage.answer()).setValue(555); - $(NumberQuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(CorrectAnswerPage.pageName); - }); - - it("When I enter 123, Then I should be routed to the incorrect page", () => { - $(CorrectAnswerPage.previous()).click(); - $(NumberQuestionPage.answer()).setValue(123); - $(NumberQuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(IncorrectAnswerPage.pageName); - }); - - it("When I enter a number less than 123, Then I should be routed to the incorrect page", () => { - $(IncorrectAnswerPage.previous()).click(); - $(NumberQuestionPage.answer()).setValue(2); - $(NumberQuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(IncorrectAnswerPage.pageName); - }); - }); - }); - - describe("Less Than", () => { - describe("Given I start number routing less than survey", () => { - before(() => { - browser.openQuestionnaire("test_new_routing_number_less_than.json"); - }); - - it("When I enter a number less than 123, Then I should be routed to the correct page", () => { - $(NumberQuestionPage.answer()).setValue(77); - $(NumberQuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(CorrectAnswerPage.pageName); - }); - - it("When I enter 123, Then I should be routed to the incorrect page", () => { - $(CorrectAnswerPage.previous()).click(); - $(NumberQuestionPage.answer()).setValue(123); - $(NumberQuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(IncorrectAnswerPage.pageName); - }); - - it("When I enter a number greater than 123, Then I should be routed to the incorrect page", () => { - $(IncorrectAnswerPage.previous()).click(); - $(NumberQuestionPage.answer()).setValue(765); - $(NumberQuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(IncorrectAnswerPage.pageName); - }); - }); - }); - - describe("Greater Than or Equal", () => { - describe("Given I have number routing with a greater than or equal", () => { - before(() => { - browser.openQuestionnaire("test_new_routing_number_greater_than_or_equal.json"); - }); - - it("When I enter a number greater than 123, Then I should be routed to the correct page", () => { - $(NumberQuestionPage.answer()).setValue(555); - $(NumberQuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(CorrectAnswerPage.pageName); - }); - - it("When I enter 123, Then I should be routed to the correct page", () => { - $(CorrectAnswerPage.previous()).click(); - $(NumberQuestionPage.answer()).setValue(123); - $(NumberQuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(CorrectAnswerPage.pageName); - }); - - it("When I enter a number less than 123, Then I should be routed to the incorrect page", () => { - $(CorrectAnswerPage.previous()).click(); - $(NumberQuestionPage.answer()).setValue(2); - $(NumberQuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(IncorrectAnswerPage.pageName); - }); - }); - }); - - describe("Less Than or Equal", () => { - describe("Given I have number routing with a less than or equal", () => { - before(() => { - browser.openQuestionnaire("test_new_routing_number_less_than_or_equal.json"); - }); - - it("When I enter a number less than 123, Then I should be routed to the correct page", () => { - $(NumberQuestionPage.answer()).setValue(23); - $(NumberQuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(CorrectAnswerPage.pageName); - }); - - it("When I enter 123, Then I should be routed to the correct page", () => { - $(CorrectAnswerPage.previous()).click(); - $(NumberQuestionPage.answer()).setValue(123); - $(NumberQuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(CorrectAnswerPage.pageName); - }); - - it("When I enter a number larger than 123, Then I should be routed to the incorrect page", () => { - $(CorrectAnswerPage.previous()).click(); - $(NumberQuestionPage.answer()).setValue(546); - $(NumberQuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(IncorrectAnswerPage.pageName); - }); - }); - }); -}); diff --git a/tests/functional/spec/features/routing/or.spec.js b/tests/functional/spec/features/routing/or.spec.js deleted file mode 100644 index bf1a44bb79..0000000000 --- a/tests/functional/spec/features/routing/or.spec.js +++ /dev/null @@ -1,46 +0,0 @@ -import FirstNumberQuestionPage from "../../../generated_pages/new_routing_or/number-question-1.page"; -import SecondNumberQuestionPage from "../../../generated_pages/new_routing_or/number-question-2.page"; -import CorrectAnswerPage from "../../../generated_pages/new_routing_or/correct-answer.page"; -import IncorrectAnswerPage from "../../../generated_pages/new_routing_or/incorrect-answer.page"; - -describe("Feature: Routing - OR Operator", () => { - describe("Equals", () => { - describe("Given I start the or operator routing survey", () => { - beforeEach(() => { - browser.openQuestionnaire("test_new_routing_or.json"); - }); - - it("When I enter both answers correctly with 123 and 321, Then I should be routed to the correct page", () => { - $(FirstNumberQuestionPage.answer1()).setValue(123); - $(FirstNumberQuestionPage.submit()).click(); - $(SecondNumberQuestionPage.answer2()).setValue(321); - $(SecondNumberQuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(CorrectAnswerPage.pageName); - }); - - it("When I only enter the second answer correctly with 555 and 321, Then I should be routed to the correct page", () => { - $(FirstNumberQuestionPage.answer1()).setValue(555); - $(FirstNumberQuestionPage.submit()).click(); - $(SecondNumberQuestionPage.answer2()).setValue(321); - $(SecondNumberQuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(CorrectAnswerPage.pageName); - }); - - it("When I only enter the first answer correctly with 123 and 555, Then I should be routed to the correct page", () => { - $(FirstNumberQuestionPage.answer1()).setValue(123); - $(FirstNumberQuestionPage.submit()).click(); - $(SecondNumberQuestionPage.answer2()).setValue(555); - $(SecondNumberQuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(CorrectAnswerPage.pageName); - }); - - it("When I answer both questions incorrectly with 555 and 444, Then I should be routed to the incorrect page", () => { - $(FirstNumberQuestionPage.answer1()).setValue(555); - $(FirstNumberQuestionPage.submit()).click(); - $(SecondNumberQuestionPage.answer2()).setValue(444); - $(SecondNumberQuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(IncorrectAnswerPage.pageName); - }); - }); - }); -}); diff --git a/tests/functional/spec/features/section_summary/section_summary.spec.js b/tests/functional/spec/features/section_summary/section_summary.spec.js deleted file mode 100644 index b2c0700ae9..0000000000 --- a/tests/functional/spec/features/section_summary/section_summary.spec.js +++ /dev/null @@ -1,167 +0,0 @@ -import AddressDurationPage from "../../../generated_pages/section_summary/address-duration.page.js"; -import HouseholdCountSectionSummaryPage from "../../../generated_pages/section_summary/household-count-section-summary.page.js"; -import HouseholdDetailsSummaryPage from "../../../generated_pages/section_summary/house-details-section-summary.page.js"; -import HouseType from "../../../generated_pages/section_summary/house-type.page.js"; -import InsuranceAddressPage from "../../../generated_pages/section_summary/insurance-address.page.js"; -import InsuranceTypePage from "../../../generated_pages/section_summary/insurance-type.page.js"; -import ListedPage from "../../../generated_pages/section_summary/listed.page.js"; -import NumberOfPeoplePage from "../../../generated_pages/section_summary/number-of-people.page.js"; -import PropertyDetailsSummaryPage from "../../../generated_pages/section_summary/property-details-section-summary.page.js"; -import SubmitPage from "../../../generated_pages/section_summary/submit.page.js"; - -describe("Section Summary", () => { - describe("Given I start a Test Section Summary survey and complete to Section Summary", () => { - beforeEach(() => { - browser.openQuestionnaire("test_section_summary.json"); - $(InsuranceTypePage.both()).click(); - $(InsuranceTypePage.submit()).click(); - $(InsuranceAddressPage.submit()).click(); - $(ListedPage.submit()).click(); - expect($(PropertyDetailsSummaryPage.insuranceTypeAnswer()).getText()).to.contain("Both"); - }); - - it("When I get to the section summary page, Then the submit button should read 'Continue'", () => { - expect($(PropertyDetailsSummaryPage.submit()).getText()).to.contain("Continue"); - }); - - it("When I have selected an answer to edit and edit it, Then I should return to the section summary with new value displayed", () => { - $(PropertyDetailsSummaryPage.insuranceAddressAnswerEdit()).click(); - $(InsuranceAddressPage.answer()).setValue("Test Address"); - $(InsuranceAddressPage.submit()).click(); - expect($(PropertyDetailsSummaryPage.insuranceAddressAnswer()).getText()).to.contain("Test Address"); - }); - - it("When I select edit from the section summary and click previous on the question page, Then I should be taken back to the section summary", () => { - $(PropertyDetailsSummaryPage.insuranceAddressAnswerEdit()).click(); - $(InsuranceAddressPage.previous()).click(); - expect(browser.getUrl()).to.contain(PropertyDetailsSummaryPage.url()); - }); - - it("When I continue on the section summary page, Then I should be taken to the next section", () => { - $(PropertyDetailsSummaryPage.submit()).click(); - expect(browser.getUrl()).to.contain(HouseType.pageName); - }); - - it("When I select edit from Section Summary but change routing, Then I should step through the section and be returned to the Section Summary once all new questions have been answered", () => { - $(PropertyDetailsSummaryPage.insuranceTypeAnswerEdit()).click(); - $(InsuranceTypePage.contents()).click(); - $(InsuranceTypePage.submit()).click(); - expect(browser.getUrl()).to.contain(InsuranceAddressPage.pageName); - $(InsuranceAddressPage.submit()).click(); - expect(browser.getUrl()).to.contain(AddressDurationPage.pageName); - $(AddressDurationPage.submit()).click(); - expect(browser.getUrl()).to.contain(PropertyDetailsSummaryPage.pageName); - }); - - it("When I select edit from Section Summary but change routing, Then using previous should not prevent me returning to the section summary once all new questions have been answered", () => { - $(PropertyDetailsSummaryPage.insuranceTypeAnswerEdit()).click(); - $(InsuranceTypePage.contents()).click(); - $(InsuranceTypePage.submit()).click(); - expect(browser.getUrl()).to.contain(InsuranceAddressPage.pageName); - $(InsuranceAddressPage.submit()).click(); - expect(browser.getUrl()).to.contain(AddressDurationPage.pageName); - $(AddressDurationPage.previous()).click(); - expect(browser.getUrl()).to.contain(InsuranceAddressPage.pageName); - $(InsuranceAddressPage.submit()).click(); - $(AddressDurationPage.submit()).click(); - expect(browser.getUrl()).to.contain(PropertyDetailsSummaryPage.pageName); - }); - }); - - describe("Given I start a Test Section Summary survey and complete to Final Summary", () => { - beforeEach(() => { - browser.openQuestionnaire("test_section_summary.json"); - $(InsuranceTypePage.both()).click(); - $(InsuranceTypePage.submit()).click(); - $(InsuranceAddressPage.submit()).click(); - $(ListedPage.submit()).click(); - $(PropertyDetailsSummaryPage.submit()).click(); - $(HouseType.submit()).click(); - $(HouseholdDetailsSummaryPage.submit()).click(); - $(NumberOfPeoplePage.answer()).setValue(3); - $(NumberOfPeoplePage.submit()).click(); - $(HouseholdCountSectionSummaryPage.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.url()); - }); - - it("When I select edit from Final Summary and don't change an answer, Then I should be taken to the Final Summary", () => { - $(SubmitPage.summaryShowAllButton()).click(); - $(SubmitPage.insuranceAddressAnswerEdit()).click(); - $(InsuranceAddressPage.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.url()); - }); - - it("When I select edit from Final Summary and change an answer that doesn't affect completeness, Then I should be taken to the Final Summary", () => { - $(SubmitPage.summaryShowAllButton()).click(); - $(SubmitPage.insuranceAddressAnswerEdit()).click(); - $(InsuranceAddressPage.answer()).setValue("Test Address"); - $(InsuranceAddressPage.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.url()); - }); - - it("When I select edit from Final Summary but change routing, Then I should step through the section and be returned to the Final Summary once all new questions have been answered", () => { - $(SubmitPage.summaryShowAllButton()).click(); - $(SubmitPage.insuranceTypeAnswerEdit()).click(); - $(InsuranceTypePage.contents()).click(); - $(InsuranceTypePage.submit()).click(); - expect(browser.getUrl()).to.contain(InsuranceAddressPage.pageName); - $(InsuranceAddressPage.submit()).click(); - expect(browser.getUrl()).to.contain(AddressDurationPage.pageName); - $(AddressDurationPage.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); - }); - - it("When I select edit from Final Summary but change routing, Then using previous should not prevent me returning to the section summary once all new questions have been answered", () => { - $(SubmitPage.summaryShowAllButton()).click(); - $(SubmitPage.insuranceTypeAnswerEdit()).click(); - $(InsuranceTypePage.contents()).click(); - $(InsuranceTypePage.submit()).click(); - expect(browser.getUrl()).to.contain(InsuranceAddressPage.pageName); - $(InsuranceAddressPage.submit()).click(); - expect(browser.getUrl()).to.contain(AddressDurationPage.pageName); - $(AddressDurationPage.previous()).click(); - expect(browser.getUrl()).to.contain(InsuranceAddressPage.pageName); - $(InsuranceAddressPage.submit()).click(); - $(AddressDurationPage.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); - }); - - it("When I select edit from Final Summary and change an answer and then go to the next question and click previous, Then I should return to the question I originally edited", () => { - $(SubmitPage.summaryShowAllButton()).click(); - $(SubmitPage.insuranceTypeAnswerEdit()).click(); - $(InsuranceTypePage.contents()).click(); - $(InsuranceTypePage.submit()).click(); - $(InsuranceAddressPage.previous()).click(); - expect(browser.getUrl()).to.contain(InsuranceTypePage.pageName); - }); - - it("When I change an answer, Then the final summary should display the updated value", () => { - $(SubmitPage.summaryShowAllButton()).click(); - expect($(SubmitPage.insuranceAddressAnswer()).getText()).to.contain("No answer provided"); - $(SubmitPage.insuranceAddressAnswerEdit()).click(); - expect(browser.getUrl()).to.contain(InsuranceAddressPage.pageName); - $(InsuranceAddressPage.answer()).setValue("Test Address"); - $(InsuranceAddressPage.submit()).click(); - $(SubmitPage.summaryShowAllButton()).click(); - expect($(SubmitPage.insuranceAddressAnswer()).getText()).to.contain("Test Address"); - }); - }); - describe("Given I start the Test Section Summary questionnaire", () => { - before(() => { - browser.openQuestionnaire("test_section_summary.json"); - }); - it("When there is no title set in the sections summary, the section title is used for the section summary title", () => { - $(InsuranceTypePage.both()).click(); - $(InsuranceTypePage.submit()).click(); - $(InsuranceAddressPage.submit()).click(); - $(ListedPage.submit()).click(); - expect($(PropertyDetailsSummaryPage.heading()).getText()).to.contain("Property Details Section"); - }); - it("When there is a title set in the sections summary, it is used for the section summary title", () => { - $(PropertyDetailsSummaryPage.submit()).click(); - $(HouseType.semiDetached()).click(); - $(HouseType.submit()).click(); - expect($(HouseholdDetailsSummaryPage.heading()).getText()).to.contain("Household Summary - Semi-detached"); - }); - }); -}); diff --git a/tests/functional/spec/features/section_summary/section_summary_repeating_sections.spec.js b/tests/functional/spec/features/section_summary/section_summary_repeating_sections.spec.js deleted file mode 100644 index 4efb211057..0000000000 --- a/tests/functional/spec/features/section_summary/section_summary_repeating_sections.spec.js +++ /dev/null @@ -1,68 +0,0 @@ -import PrimaryPersonPage from "../../../generated_pages/repeating_section_summaries/primary-person-list-collector.page"; -import PrimaryPersonAddPage from "../../../generated_pages/repeating_section_summaries/primary-person-list-collector-add.page"; -import FirstListCollectorPage from "../../../generated_pages/repeating_section_summaries/list-collector.page"; -import FirstListCollectorAddPage from "../../../generated_pages/repeating_section_summaries/list-collector-add.page"; -import PersonalSummaryPage from "../../../generated_pages/repeating_section_summaries/personal-details-section-summary.page"; -import ProxyPage from "../../../generated_pages/repeating_section_summaries/proxy.page"; -import DateOfBirthPage from "../../../generated_pages/repeating_section_summaries/date-of-birth.page"; -import HubPage from "../../../base_pages/hub.page.js"; - -describe("Feature: Repeating Section Summaries", () => { - describe("Given the user has added some members to the household and is on the Hub", () => { - before("Open survey and add household members", () => { - browser.openQuestionnaire("test_repeating_section_summaries.json"); - // Ensure we are on the Hub - expect(browser.getUrl()).to.contain(HubPage.url()); - // Start first section to add household members - $(HubPage.summaryRowLink("section")).click(); - - // Add a primary person - $(PrimaryPersonPage.yes()).click(); - $(PrimaryPersonPage.submit()).click(); - $(PrimaryPersonAddPage.firstName()).setValue("Mark"); - $(PrimaryPersonAddPage.lastName()).setValue("Twain"); - $(PrimaryPersonPage.submit()).click(); - - // Add other household members - - $(FirstListCollectorPage.yes()).click(); - $(FirstListCollectorPage.submit()).click(); - $(FirstListCollectorAddPage.firstName()).setValue("Jean"); - $(FirstListCollectorAddPage.lastName()).setValue("Clemens"); - $(FirstListCollectorAddPage.submit()).click(); - - $(FirstListCollectorPage.no()).click(); - $(FirstListCollectorPage.submit()).click(); - }); - - describe("When the user finishes a repeating section", () => { - before("Enter information for a repeating section", () => { - $(HubPage.summaryRowLink("personal-details-section-1")).click(); - $(ProxyPage.yes()).click(); - $(ProxyPage.submit()).click(); - - $(DateOfBirthPage.day()).setValue("30"); - $(DateOfBirthPage.month()).setValue("11"); - $(DateOfBirthPage.year()).setValue("1835"); - $(DateOfBirthPage.submit()).click(); - }); - - beforeEach("Navigate to the Section Summary", () => { - browser.url(HubPage.url()); - $(HubPage.summaryRowLink("personal-details-section-1")).click(); - }); - - it("the title set in the repeating block is used for the section summary title", () => { - expect($(PersonalSummaryPage.heading()).getText()).to.contain("Mark Twain"); - }); - - it("renders their name as part of the question title on the section summary", () => { - expect($(PersonalSummaryPage.dateOfBirthQuestion()).getText()).to.contain("Mark Twain’s"); - }); - - it("renders the correct date of birth answer", () => { - expect($(PersonalSummaryPage.dateOfBirthAnswer()).getText()).to.contain("30 November 1835"); - }); - }); - }); -}); diff --git a/tests/functional/spec/features/skipping/answer_comparison_skip_conditions.spec.js b/tests/functional/spec/features/skipping/answer_comparison_skip_conditions.spec.js deleted file mode 100644 index acad09c9bc..0000000000 --- a/tests/functional/spec/features/skipping/answer_comparison_skip_conditions.spec.js +++ /dev/null @@ -1,30 +0,0 @@ -import Comparison1Page from "../../../generated_pages/new_skip_condition_answer_comparison/comparison-1.page.js"; -import Comparison2Page from "../../../generated_pages/new_skip_condition_answer_comparison/comparison-2.page.js"; - -describe("Test skip condition answer comparisons", () => { - beforeEach(() => { - browser.openQuestionnaire("test_new_skip_condition_answer_comparison.json"); - }); - - it("Given we start the skip condition survey, when we enter the same answers, then the interstitial should show that the answers are the same", () => { - $(Comparison1Page.answer()).setValue(1); - $(Comparison1Page.submit()).click(); - $(Comparison2Page.answer()).setValue(1); - $(Comparison2Page.submit()).click(); - expect($("#main-content > p").getText()).to.contain("Your second number was equal to your first number"); - }); - it("Given we start the skip condition survey, when we enter a high number then a low number, then the interstitial should show that the answers are low then high", () => { - $(Comparison1Page.answer()).setValue(3); - $(Comparison1Page.submit()).click(); - $(Comparison2Page.answer()).setValue(2); - $(Comparison2Page.submit()).click(); - expect($("#main-content > p").getText()).to.contain("Your first answer was greater than your second number"); - }); - it("Given we start the skip condition survey, when we enter a low number then a high number, then the interstitial should show that the answers are high then low", () => { - $(Comparison1Page.answer()).setValue(1); - $(Comparison1Page.submit()).click(); - $(Comparison2Page.answer()).setValue(2); - $(Comparison2Page.submit()).click(); - expect($("#main-content > p").getText()).to.contain("Your first answer was less than your second number"); - }); -}); diff --git a/tests/functional/spec/features/submit_page/submit_page.spec.js b/tests/functional/spec/features/submit_page/submit_page.spec.js index 00ad357e17..ef7d682cd9 100644 --- a/tests/functional/spec/features/submit_page/submit_page.spec.js +++ b/tests/functional/spec/features/submit_page/submit_page.spec.js @@ -1,23 +1,24 @@ import BreakfastPage from "../../../generated_pages/submit_with_custom_submission_text/breakfast.page.js"; import { SubmitPage } from "../../../base_pages/submit.page"; import { IntroductionPage } from "../../../base_pages/introduction.page"; +import { click, verifyUrlContains } from "../../../helpers"; describe("Given I launch a linear flow questionnaire without summary", () => { - beforeEach("Load the questionnaire", () => { - browser.openQuestionnaire("test_submit_with_custom_submission_text.json"); - $(IntroductionPage.getStarted()).click(); + beforeEach("Load the questionnaire", async () => { + await browser.openQuestionnaire("test_submit_with_custom_submission_text.json"); + await $(IntroductionPage.getStarted()).click(); }); - it("When I complete the questionnaire, then I should be taken to the submit page without a summary", () => { - $(BreakfastPage.answer()).setValue("Bacon"); - $(BreakfastPage.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.url()); - expect($(SubmitPage.summary()).isExisting()).to.be.false; + it("When I complete the questionnaire, then I should be taken to the submit page without a summary", async () => { + await $(BreakfastPage.answer()).setValue("Bacon"); + await click(BreakfastPage.submit()); + await verifyUrlContains(SubmitPage.url()); + await expect(await $(SubmitPage.summary()).isExisting()).toBe(false); }); - it("When I complete the questionnaire and submit the questionnaire, then the submission is successful", () => { - $(BreakfastPage.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.url()); - $(SubmitPage.submit()).click(); + it("When I complete the questionnaire and submit the questionnaire, then the submission is successful", async () => { + await click(BreakfastPage.submit()); + await verifyUrlContains(SubmitPage.url()); + await click(SubmitPage.submit()); }); }); diff --git a/tests/functional/spec/features/submit_page/submit_page_collapsible_summary.spec.js b/tests/functional/spec/features/submit_page/submit_page_collapsible_summary.spec.js index 45db6ceab9..c7e83b7353 100644 --- a/tests/functional/spec/features/submit_page/submit_page_collapsible_summary.spec.js +++ b/tests/functional/spec/features/submit_page/submit_page_collapsible_summary.spec.js @@ -7,38 +7,39 @@ import ListedPage from "../../../generated_pages/section_summary/listed.page.js" import NumberOfPeoplePage from "../../../generated_pages/section_summary/number-of-people.page.js"; import PropertyDetailsSummaryPage from "../../../generated_pages/section_summary/property-details-section-summary.page.js"; import SubmitPage from "../../../generated_pages/section_summary/submit.page.js"; +import { click } from "../../../helpers"; describe("Collapsible Summary", () => { describe("Given I complete a questionnaire with collapsible summary enabled", () => { - beforeEach(() => { - browser.openQuestionnaire("test_section_summary.json"); - $(InsuranceTypePage.both()).click(); - $(InsuranceTypePage.submit()).click(); - $(InsuranceAddressPage.submit()).click(); - $(ListedPage.submit()).click(); - $(PropertyDetailsSummaryPage.submit()).click(); - $(HouseType.submit()).click(); - $(HouseholdDetailsSummaryPage.submit()).click(); - $(NumberOfPeoplePage.answer()).setValue(3); - $(NumberOfPeoplePage.submit()).click(); - $(HouseholdCountSectionSummaryPage.submit()).click(); + beforeEach(async () => { + await browser.openQuestionnaire("test_section_summary.json"); + await $(InsuranceTypePage.both()).click(); + await click(InsuranceTypePage.submit()); + await click(InsuranceAddressPage.submit()); + await click(ListedPage.submit()); + await click(PropertyDetailsSummaryPage.submit()); + await click(HouseType.submit()); + await click(HouseholdDetailsSummaryPage.submit()); + await $(NumberOfPeoplePage.answer()).setValue(3); + await click(NumberOfPeoplePage.submit()); + await click(HouseholdCountSectionSummaryPage.submit()); }); - it("When I am on the submit page, Then a collapsed summary should be displayed with the group title and questions should not be displayed", () => { - expect($(SubmitPage.collapsibleSummary()).isDisplayed()).to.be.true; + it("When I am on the submit page, Then a collapsed summary should be displayed with the group title and questions should not be displayed", async () => { + await expect(await $(SubmitPage.collapsibleSummary()).isDisplayed()).toBe(true); - expect($(SubmitPage.collapsibleSummary()).getText()).to.contain("Property Details"); - expect($(SubmitPage.collapsibleSummary()).getText()).to.contain("House Details"); + await expect(await $(SubmitPage.collapsibleSummary()).getText()).toContain("Property Details"); + await expect(await $(SubmitPage.collapsibleSummary()).getText()).toContain("House Details"); - expect($(SubmitPage.insuranceAddressQuestion()).isDisplayed()).to.be.false; - expect($(SubmitPage.numberOfPeopleQuestion()).isDisplayed()).to.be.false; + await expect(await $(SubmitPage.insuranceAddressQuestion()).getText()).toBe(""); + await expect(await $(SubmitPage.numberOfPeopleQuestion()).getText()).toBe(""); }); - it("When I click the Show all button, Then the summary should be expanded and questions should be displayed", () => { - $(SubmitPage.summaryShowAllButton()).click(); + it("When I click the Show all button, Then the summary should be expanded and questions should be displayed", async () => { + await $(SubmitPage.summaryShowAllButton()).click(); - expect($(SubmitPage.insuranceAddressQuestion()).isDisplayed()).to.be.true; - expect($(SubmitPage.numberOfPeopleQuestion()).isDisplayed()).to.be.true; + await expect(await $(SubmitPage.insuranceAddressQuestion()).getText()).toBe("What is the address you would like to insure?"); + await expect(await $(SubmitPage.numberOfPeopleQuestion()).getText()).toBe("Title"); }); }); }); diff --git a/tests/functional/spec/features/submit_page/submit_page_custom_submission_text.spec.js b/tests/functional/spec/features/submit_page/submit_page_custom_submission_text.spec.js index 43a2155a65..db91730754 100644 --- a/tests/functional/spec/features/submit_page/submit_page_custom_submission_text.spec.js +++ b/tests/functional/spec/features/submit_page/submit_page_custom_submission_text.spec.js @@ -1,17 +1,17 @@ import DessertBlockPage from "../../../generated_pages/submit_with_summary_custom_submission_text/dessert-block.page.js"; import SubmitPage from "../../../generated_pages/submit_with_summary_custom_submission_text/submit.page.js"; - +import { click } from "../../../helpers"; describe("Summary Screen", () => { - beforeEach("Load the questionnaire", () => { - browser.openQuestionnaire("test_submit_with_summary_custom_submission_text.json"); + beforeEach("Load the questionnaire", async () => { + await browser.openQuestionnaire("test_submit_with_summary_custom_submission_text.json"); }); - it("Given a questionnaire with a summary and custom submission content has been completed, then the correct submission content should be displayed", () => { - $(DessertBlockPage.dessert()).setValue("Crème BrÃģlÊe"); - $(DessertBlockPage.submit()).click(); - expect($(SubmitPage.heading()).getText()).to.contain("Submission title"); - expect($(SubmitPage.warning()).getText()).to.contain("Submission warning"); - expect($(SubmitPage.guidance()).getText()).to.contain("Submission guidance"); - expect($(SubmitPage.submit()).getText()).to.contain("Submission button"); + it("Given a questionnaire with a summary and custom submission content has been completed, then the correct submission content should be displayed", async () => { + await $(DessertBlockPage.dessert()).setValue("Crème BrÃģlÊe"); + await click(DessertBlockPage.submit()); + await expect(await $(SubmitPage.heading()).getText()).toBe("Submission title"); + await expect(await $(SubmitPage.warning()).getText()).toBe("Submission warning"); + await expect(await $(SubmitPage.guidance()).getText()).toBe("Submission guidance"); + await expect(await $(SubmitPage.submit()).getText()).toBe("Submission button"); }); }); diff --git a/tests/functional/spec/features/submit_page/submit_page_summary.spec.js b/tests/functional/spec/features/submit_page/submit_page_summary.spec.js index 103eb193be..53d5cb5084 100644 --- a/tests/functional/spec/features/submit_page/submit_page_summary.spec.js +++ b/tests/functional/spec/features/submit_page/submit_page_summary.spec.js @@ -3,107 +3,107 @@ import DessertConfirmationPage from "../../../generated_pages/submit_with_summar import NumbersPage from "../../../generated_pages/submit_with_summary/numbers.page.js"; import RadioPage from "../../../generated_pages/submit_with_summary/radio.page.js"; import SubmitPage from "../../../generated_pages/submit_with_summary/submit.page.js"; - +import { click, verifyUrlContains } from "../../../helpers"; describe("Submit Page with Summary", () => { - beforeEach("Load the questionnaire", () => { - browser.openQuestionnaire("test_submit_with_summary.json"); + beforeEach("Load the questionnaire", async () => { + await browser.openQuestionnaire("test_submit_with_summary.json"); }); - it("Given a questionnaire with a summary has been completed when the submit page is displayed, then it should contain a summary of all answers", () => { - completeAllQuestions(); + it("Given a questionnaire with a summary has been completed when the submit page is displayed, then it should contain a summary of all answers", async () => { + await completeAllQuestions(); - expect($(SubmitPage.radioAnswer()).getText()).to.contain("Bacon"); - expect($(SubmitPage.dessertGroupTitle()).getText()).to.contain("Dessert"); - expect($(SubmitPage.dessertAnswer()).getText()).to.contain("Crème BrÃģlÊe"); - expect($(SubmitPage.dessertConfirmationAnswer()).getText()).to.contain("Yes"); - expect($(SubmitPage.numbersCurrencyAnswer()).getText()).to.contain("ÂŖ1,234.00"); - expect($(SubmitPage.numbersUnitAnswer()).getText()).to.contain("123,456 km²"); - expect($(SubmitPage.numbersDecimalAnswer()).getText()).to.contain("123,456.78"); + await expect(await $(SubmitPage.radioAnswer()).getText()).toBe("Bacon"); + await expect(await $(SubmitPage.dessertGroupTitle()).getText()).toBe("Dessert"); + await expect(await $(SubmitPage.dessertAnswer()).getText()).toBe("Crème BrÃģlÊe"); + await expect(await $(SubmitPage.dessertConfirmationAnswer()).getText()).toBe("Yes"); + await expect(await $(SubmitPage.numbersCurrencyAnswer()).getText()).toBe("ÂŖ1,234.00"); + await expect(await $(SubmitPage.numbersUnitAnswer()).getText()).toBe("123,456 km²"); + await expect(await $(SubmitPage.numbersDecimalAnswer()).getText()).toBe("123,456.78"); }); - it("Given a questionnaire with a summary has been completed when the submit page is displayed then I should be able to submit the answers", () => { - completeAllQuestions(); + it("Given a questionnaire with a summary has been completed when the submit page is displayed then I should be able to submit the answers", async () => { + await completeAllQuestions(); - $(SubmitPage.submit()).click(); - expect(browser.getUrl()).to.contain("thank-you"); + await click(SubmitPage.submit()); + await verifyUrlContains("thank-you"); }); - it("Given a questionnaire with a summary has been completed when a summary page edit link is clicked then it should return to that question", () => { - completeAllQuestions(); + it("Given a questionnaire with a summary has been completed when a summary page edit link is clicked then it should return to that question", async () => { + await completeAllQuestions(); - $(SubmitPage.radioAnswerEdit()).click(); + await $(SubmitPage.radioAnswerEdit()).click(); - expect($(RadioPage.bacon()).isSelected()).to.be.true; + await expect(await $(RadioPage.bacon()).isSelected()).toBe(true); }); - it("Given a questionnaire with a summary has been completed and a summary page edit link is clicked, when I click previous, then it should return to the summary", () => { - completeAllQuestions(); + it("Given a questionnaire with a summary has been completed and a summary page edit link is clicked, when I click previous, then it should return to the summary", async () => { + await completeAllQuestions(); - $(SubmitPage.radioAnswerEdit()).click(); - $(RadioPage.previous()).click(); + await $(SubmitPage.radioAnswerEdit()).click(); + await $(RadioPage.previous()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); + await verifyUrlContains(SubmitPage.pageName); }); - it("Given a questionnaire with a summary has been completed when a summary page edit link is clicked then it should return to that question then back to summary", () => { - completeAllQuestions(); + it("Given a questionnaire with a summary has been completed when a summary page edit link is clicked then it should return to that question then back to summary", async () => { + await completeAllQuestions(); - $(SubmitPage.radioAnswerEdit()).click(); - $(RadioPage.sausage()).click(); - $(RadioPage.submit()).click(); - expect($(SubmitPage.radioAnswer()).getText()).to.contain("Sausage"); + await $(SubmitPage.radioAnswerEdit()).click(); + await $(RadioPage.sausage()).click(); + await click(RadioPage.submit()); + await expect(await $(SubmitPage.radioAnswer()).getText()).toBe("Sausage"); }); - it("Given the edit link is used when a question is updated then the submit page summary should show the new answer", () => { - completeAllQuestions(); + it("Given the edit link is used when a question is updated then the submit page summary should show the new answer", async () => { + await completeAllQuestions(); - $(SubmitPage.numbersUnitAnswerEdit()).click(); - expect($(NumbersPage.unit()).isFocused()).to.be.true; - $(NumbersPage.unit()).setValue("654321"); - $(NumbersPage.submit()).click(); - expect($(SubmitPage.numbersUnitAnswer()).getText()).to.contain("654,321 km²"); + await $(SubmitPage.numbersUnitAnswerEdit()).click(); + await expect(await $(NumbersPage.unit()).isFocused()).toBe(true); + await $(NumbersPage.unit()).setValue("654321"); + await click(NumbersPage.submit()); + await expect(await $(SubmitPage.numbersUnitAnswer()).getText()).toBe("654,321 km²"); }); - it("Given a number value of zero is entered when on the submit page then formatted 0 should be displayed on the summary", () => { - $(RadioPage.submit()).click(); - $(DessertPage.answer()).setValue("Cake"); - $(DessertPage.submit()).click(); - $(DessertConfirmationPage.yes()).click(); - $(DessertConfirmationPage.submit()).click(); - $(NumbersPage.currency()).setValue("0"); - $(NumbersPage.submit()).click(); - expect($(SubmitPage.numbersCurrencyAnswer()).getText()).to.contain("ÂŖ0.00"); + it("Given a number value of zero is entered when on the submit page then formatted 0 should be displayed on the summary", async () => { + await click(RadioPage.submit()); + await $(DessertPage.answer()).setValue("Cake"); + await click(DessertPage.submit()); + await $(DessertConfirmationPage.yes()).click(); + await click(DessertConfirmationPage.submit()); + await $(NumbersPage.currency()).setValue("0"); + await click(NumbersPage.submit()); + await expect(await $(SubmitPage.numbersCurrencyAnswer()).getText()).toBe("ÂŖ0.00"); }); - it("Given no value is entered when on the submit page summary then the correct response should be displayed", () => { - $(RadioPage.submit()).click(); - $(DessertPage.answer()).setValue("Cake"); - $(DessertPage.submit()).click(); - $(DessertConfirmationPage.yes()).click(); - $(DessertConfirmationPage.submit()).click(); - $(NumbersPage.submit()).click(); - expect($(SubmitPage.numbersCurrencyAnswer()).getText()).to.contain("No answer provided"); + it("Given no value is entered when on the submit page summary then the correct response should be displayed", async () => { + await click(RadioPage.submit()); + await $(DessertPage.answer()).setValue("Cake"); + await click(DessertPage.submit()); + await $(DessertConfirmationPage.yes()).click(); + await click(DessertConfirmationPage.submit()); + await click(NumbersPage.submit()); + await expect(await $(SubmitPage.numbersCurrencyAnswer()).getText()).toBe("No answer provided"); }); - it("Given a questionnaire with a summary has been completed, when submission content has not been set in the schema, then the default content should be displayed", () => { - completeAllQuestions(); + it("Given a questionnaire with a summary has been completed, when submission content has not been set in the schema, then the default content should be displayed", async () => { + await completeAllQuestions(); - expect($(SubmitPage.heading()).getText()).to.contain("Check your answers and submit"); - expect($(SubmitPage.submit()).getText()).to.contain("Submit answers"); + await expect(await $(SubmitPage.heading()).getText()).toBe("Check your answers and submit"); + await expect(await $(SubmitPage.submit()).getText()).toBe("Submit answers"); }); - function completeAllQuestions() { - $(RadioPage.bacon()).click(); - $(RadioPage.submit()).click(); - $(DessertPage.answer()).setValue("Crème BrÃģlÊe"); - $(DessertPage.submit()).click(); - $(DessertConfirmationPage.yes()).click(); - $(DessertConfirmationPage.submit()).click(); - $(NumbersPage.currency()).setValue("1234"); - $(NumbersPage.unit()).setValue("123456"); - $(NumbersPage.decimal()).setValue("123456.78"); - $(NumbersPage.submit()).click(); - - expect(browser.getUrl()).to.contain(SubmitPage.pageName); + async function completeAllQuestions() { + await $(RadioPage.bacon()).click(); + await click(RadioPage.submit()); + await $(DessertPage.answer()).setValue("Crème BrÃģlÊe"); + await click(DessertPage.submit()); + await $(DessertConfirmationPage.yes()).click(); + await click(DessertConfirmationPage.submit()); + await $(NumbersPage.currency()).setValue("1234"); + await $(NumbersPage.unit()).setValue("123456"); + await $(NumbersPage.decimal()).setValue("123456.78"); + await click(NumbersPage.submit()); + + await verifyUrlContains(SubmitPage.pageName); } }); diff --git a/tests/functional/spec/features/submit_with_summary_return_to_answer.spec.js b/tests/functional/spec/features/submit_with_summary_return_to_answer.spec.js index ef350192ad..07de09d41f 100644 --- a/tests/functional/spec/features/submit_with_summary_return_to_answer.spec.js +++ b/tests/functional/spec/features/submit_with_summary_return_to_answer.spec.js @@ -6,82 +6,84 @@ import HouseholdDetailsSummaryPage from "../../generated_pages/submit_with_summa import SubmitPage from "../../generated_pages/submit_with_summary_return_to_answer/submit.page.js"; import AddressDurationPage from "../../generated_pages/submit_with_summary_return_to_answer/address-duration.page.js"; import NamePage from "../../generated_pages/submit_with_summary_return_to_answer/name.page.js"; - +import { click, verifyUrlContains } from "../../helpers"; describe("Summary Anchor Scrolling", () => { describe("Given I start a Test Section Summary survey", () => { - beforeEach(() => { - browser.openQuestionnaire("test_submit_with_summary_return_to_answer.json"); - $(NamePage.submit()).click(); - $(InsuranceTypePage.both()).click(); - $(InsuranceTypePage.submit()).click(); + beforeEach(async () => { + await browser.openQuestionnaire("test_submit_with_summary_return_to_answer.json"); + await click(NamePage.submit()); + await $(InsuranceTypePage.both()).click(); + await click(InsuranceTypePage.submit()); }); - it("When I have provided an answer and click through to the next question, Then the Previous link url shouldn't contain any anchors or reference to return_to or return_to_answer_id", () => { - expect($(InsuranceAddressPage.previous()).getAttribute("href")).not.to.contain("#"); - expect($(InsuranceAddressPage.previous()).getAttribute("href")).not.to.contain("return_to"); - expect($(InsuranceAddressPage.previous()).getAttribute("href")).not.to.contain("return_to_answer_id"); + it("When I have provided an answer and click through to the next question, Then the Previous link url shouldn't contain any anchors or reference to return_to or return_to_answer_id", async () => { + await expect(await $(InsuranceAddressPage.previous()).getAttribute("href")).not.toContain("#"); + await expect(await $(InsuranceAddressPage.previous()).getAttribute("href")).not.toContain("return_to"); + await expect(await $(InsuranceAddressPage.previous()).getAttribute("href")).not.toContain("return_to_answer_id"); }); - it("When I reach the section summary page, Then the Change link url should contain return_to, return_to_answer_id query params", () => { - $(InsuranceAddressPage.submit()).click(); - $(AddressDurationPage.submit()).click(); - expect($(PropertyDetailsSummaryPage.insuranceAddressAnswer2Edit()).getAttribute("href")).to.contain( - "insurance-address/?return_to=section-summary&return_to_answer_id=insurance-address-answer2#insurance-address-answer2" + it("When I reach the section summary page, Then the Change link url should contain return_to, return_to_answer_id query params", async () => { + await click(InsuranceAddressPage.submit()); + await click(AddressDurationPage.submit()); + await expect(await $(PropertyDetailsSummaryPage.insuranceAddressAnswer2Edit()).getAttribute("href")).toContain( + "insurance-address/?return_to=section-summary&return_to_answer_id=insurance-address-answer2#insurance-address-answer2", ); }); - it("When I reach the section summary page, Then the Change link url for a concatenated answer should contain return_to, return_to_answer_id query params", () => { - $(InsuranceAddressPage.submit()).click(); - $(AddressDurationPage.submit()).click(); - expect($(PropertyDetailsSummaryPage.summaryRowState("name-question-concatenated-answer-edit")).getAttribute("href")).to.contain( - "name/?return_to=section-summary&return_to_answer_id=name-question-concatenated-answer#name-question-concatenated-answer" + it("When I reach the section summary page, Then the Change link url for a concatenated answer should contain return_to, return_to_answer_id query params", async () => { + await click(InsuranceAddressPage.submit()); + await click(AddressDurationPage.submit()); + await expect(await $(PropertyDetailsSummaryPage.summaryRowState("name-question-concatenated-answer-edit")).getAttribute("href")).toContain( + "name/?return_to=section-summary&return_to_answer_id=name-question-concatenated-answer#first-name", ); }); - it("When I edit an answer from the section summary page, Then the Previous link url should contain an anchor referencing the answer id of the answer I am changing", () => { - $(InsuranceAddressPage.submit()).click(); - $(AddressDurationPage.submit()).click(); - $(PropertyDetailsSummaryPage.insuranceAddressAnswer2Edit()).click(); - expect($(InsuranceAddressPage.previous()).getAttribute("href")).to.contain("property-details-section/#insurance-address-answer2"); + it("When I edit an answer from the section summary page, Then the Previous link url should contain an anchor referencing the answer id of the answer I am changing", async () => { + await click(InsuranceAddressPage.submit()); + await click(AddressDurationPage.submit()); + await $(PropertyDetailsSummaryPage.insuranceAddressAnswer2Edit()).click(); + await expect(await $(InsuranceAddressPage.previous()).getAttribute("href")).toContain("property-details-section/#insurance-address-answer2"); }); - it("When I edit an answer from the section summary page and click the Previous link, Then the browser url should contain an anchor referencing the answer id of the answer I am changing", () => { - $(InsuranceAddressPage.submit()).click(); - $(AddressDurationPage.submit()).click(); - $(PropertyDetailsSummaryPage.insuranceAddressAnswer2Edit()).click(); - $(InsuranceAddressPage.previous()).click(); - expect(browser.getUrl()).to.contain("property-details-section/#insurance-address-answer2"); + it("When I edit an answer from the section summary page and click the Previous link, Then the browser url should contain an anchor referencing the answer id of the answer I am changing", async () => { + await click(InsuranceAddressPage.submit()); + await click(AddressDurationPage.submit()); + await $(PropertyDetailsSummaryPage.insuranceAddressAnswer2Edit()).click(); + await $(InsuranceAddressPage.previous()).click(); + await verifyUrlContains("property-details-section/#insurance-address-answer2"); }); - it("When I edit an answer from the section summary page and click the Submit button, Then I am taken to the summary page and the browser url should contain an anchor referencing the answer id of the answer I am changing", () => { - $(InsuranceAddressPage.submit()).click(); - $(AddressDurationPage.submit()).click(); - $(PropertyDetailsSummaryPage.insuranceAddressAnswer2Edit()).click(); - $(InsuranceAddressPage.submit()).click(); - expect(browser.getUrl()).to.contain("property-details-section/#insurance-address-answer2"); + it("When I edit an answer from the section summary page and click the Submit button, Then I am taken to the summary page and the browser url should contain an anchor referencing the answer id of the answer I am changing", async () => { + await click(InsuranceAddressPage.submit()); + await click(AddressDurationPage.submit()); + await $(PropertyDetailsSummaryPage.insuranceAddressAnswer2Edit()).click(); + await click(InsuranceAddressPage.submit()); + await verifyUrlContains("property-details-section/#insurance-address-answer2"); }); - it("When I am on the final summary page, Then the Change link url should contain return_to, return_to_answer_id query params", () => { - $(InsuranceAddressPage.submit()).click(); - $(AddressDurationPage.submit()).click(); - $(PropertyDetailsSummaryPage.submit()).click(); - $(HouseType.submit()).click(); - $(HouseholdDetailsSummaryPage.submit()).click(); - $(SubmitPage.summaryShowAllButton()).click(); - expect($(SubmitPage.insuranceAddressAnswer2Edit()).getAttribute("href")).to.contain( - "?return_to=final-summary&return_to_answer_id=insurance-address-answer2#insurance-address-answer2" + it("When I am on the final summary page, Then the Change link url should contain return_to, return_to_answer_id query params", async () => { + await click(InsuranceAddressPage.submit()); + await click(AddressDurationPage.submit()); + await click(PropertyDetailsSummaryPage.submit()); + await click(HouseType.submit()); + await click(HouseholdDetailsSummaryPage.submit()); + await $(SubmitPage.summaryShowAllButton()).click(); + await expect(await $(SubmitPage.insuranceAddressAnswer2Edit()).getAttribute("href")).toContain( + "?return_to=final-summary&return_to_answer_id=insurance-address-answer2#insurance-address-answer2", ); }); - it("When I edit an answer from the final summary page, Then the browser url contains return_to, return_to_answer_id query params", () => { - $(InsuranceAddressPage.submit()).click(); - $(AddressDurationPage.submit()).click(); - $(PropertyDetailsSummaryPage.submit()).click(); - $(HouseType.submit()).click(); - $(HouseholdDetailsSummaryPage.submit()).click(); - $(SubmitPage.summaryShowAllButton()).click(); - $(SubmitPage.insuranceAddressAnswer2Edit()).click(); - expect(browser.getUrl()).to.contain("?return_to=final-summary&return_to_answer_id=insurance-address-answer2#insurance-address-answer2"); + it("When I edit an answer from the final summary page, Then the browser url contains return_to, return_to_answer_id query params", async () => { + await click(InsuranceAddressPage.submit()); + await click(AddressDurationPage.submit()); + await click(PropertyDetailsSummaryPage.submit()); + await click(HouseType.submit()); + await click(HouseholdDetailsSummaryPage.submit()); + await $(SubmitPage.summaryShowAllButton()).click(); + await $(SubmitPage.insuranceAddressAnswer2Edit()).click(); + await expect(browser).toHaveUrl( + expect.stringContaining("?return_to=final-summary&return_to_answer_id=insurance-address-answer2#insurance-address-answer2"), + ); }); }); }); diff --git a/tests/functional/spec/features/units.spec.js b/tests/functional/spec/features/units.spec.js index f58f65a84d..1b13079f3e 100644 --- a/tests/functional/spec/features/units.spec.js +++ b/tests/functional/spec/features/units.spec.js @@ -2,42 +2,75 @@ import SetLengthUnitsBlockPage from "../../generated_pages/unit_patterns/set-len import SetDurationUnitsBlockPage from "../../generated_pages/unit_patterns/set-duration-units-block.page.js"; import SetAreaUnitsBlockPage from "../../generated_pages/unit_patterns/set-area-units-block.page.js"; import SetVolumeUnitsBlockPage from "../../generated_pages/unit_patterns/set-volume-units-block.page.js"; +import SetWeightUnitsBlockPage from "../../generated_pages/unit_patterns/set-weight-units-block.page.js"; import SubmitPage from "../../generated_pages/unit_patterns/submit.page.js"; - +import { click } from "../../helpers"; describe("Units", () => { - it("Given we do not set a language code and run the questionnaire, when we enter values for durations, they should be displayed on the summary with their units.", () => { - browser.openQuestionnaire("test_unit_patterns.json", { language: "en" }); - $(SetLengthUnitsBlockPage.submit()).click(); - expect($(SetDurationUnitsBlockPage.durationHourUnit()).getText()).to.equal("hours"); - expect($(SetDurationUnitsBlockPage.durationYearUnit()).getText()).to.equal("years"); - $(SetDurationUnitsBlockPage.durationHour()).setValue(6); - $(SetDurationUnitsBlockPage.durationYear()).setValue(20); - $(SetDurationUnitsBlockPage.submit()).click(); - $(SetAreaUnitsBlockPage.submit()).click(); - $(SetVolumeUnitsBlockPage.submit()).click(); - expect($(SubmitPage.durationHour()).getText()).to.equal("6 hours"); - expect($(SubmitPage.durationYear()).getText()).to.equal("20 years"); + it("Given we do not set a language code and run the questionnaire, when we enter values for durations, they should be displayed on the summary with their units.", async () => { + await browser.openQuestionnaire("test_unit_patterns.json", { language: "en" }); + await click(SetLengthUnitsBlockPage.submit()); + await expect(await $(SetDurationUnitsBlockPage.durationHourUnit()).getText()).toBe("hours"); + await expect(await $(SetDurationUnitsBlockPage.durationYearUnit()).getText()).toBe("years"); + await $(SetDurationUnitsBlockPage.durationHour()).setValue(6); + await $(SetDurationUnitsBlockPage.durationYear()).setValue(20); + await click(SetDurationUnitsBlockPage.submit()); + await click(SetAreaUnitsBlockPage.submit()); + await click(SetVolumeUnitsBlockPage.submit()); + await click(SetWeightUnitsBlockPage.submit()); + await expect(await $(SubmitPage.durationHour()).getText()).toBe("6 hours"); + await expect(await $(SubmitPage.durationYear()).getText()).toBe("20 years"); + }); + + it("Given we set a language code for welsh and run the questionnaire, when we enter values for durations, they should be displayed on the summary with their units.", async () => { + await browser.openQuestionnaire("test_unit_patterns.json", { language: "cy" }); + await $(SetLengthUnitsBlockPage.submit()).scrollIntoView(); + await click(SetLengthUnitsBlockPage.submit()); + await expect(await $(SetDurationUnitsBlockPage.durationHourUnit()).getText()).toBe("awr"); + await expect(await $(SetDurationUnitsBlockPage.durationYearUnit()).getText()).toBe("flynedd"); + await $(SetDurationUnitsBlockPage.durationHour()).setValue(6); + await $(SetDurationUnitsBlockPage.durationYear()).setValue(20); + await $(SetDurationUnitsBlockPage.submit()).scrollIntoView(); + await click(SetDurationUnitsBlockPage.submit()); + await click(SetAreaUnitsBlockPage.submit()); + await click(SetVolumeUnitsBlockPage.submit()); + await click(SetWeightUnitsBlockPage.submit()); + await expect(await $(SubmitPage.durationHour()).getText()).toBe("6 awr"); + await expect(await $(SubmitPage.durationYear()).getText()).toBe("20 mlynedd"); + }); + + it("Given we open a questionnaire with unit labels, when the label is highlighted by the tooltip, then the long unit label should be displayed.", async () => { + await browser.openQuestionnaire("test_unit_patterns.json", { language: "en" }); + await expect(await $(SetLengthUnitsBlockPage.centimetresUnit()).getAttribute("title")).toBe("centimetres"); + await expect(await $(SetLengthUnitsBlockPage.metresUnit()).getAttribute("title")).toBe("metres"); + await expect(await $(SetLengthUnitsBlockPage.kilometresUnit()).getAttribute("title")).toBe("kilometres"); + await expect(await $(SetLengthUnitsBlockPage.milesUnit()).getAttribute("title")).toBe("miles"); }); - it("Given we set a language code for welsh and run the questionnaire, when we enter values for durations, they should be displayed on the summary with their units.", () => { - browser.openQuestionnaire("test_unit_patterns.json", { language: "cy" }); - $(SetLengthUnitsBlockPage.submit()).click(); - expect($(SetDurationUnitsBlockPage.durationHourUnit()).getText()).to.equal("awr"); - expect($(SetDurationUnitsBlockPage.durationYearUnit()).getText()).to.equal("flynedd"); - $(SetDurationUnitsBlockPage.durationHour()).setValue(6); - $(SetDurationUnitsBlockPage.durationYear()).setValue(20); - $(SetDurationUnitsBlockPage.submit()).click(); - $(SetAreaUnitsBlockPage.submit()).click(); - $(SetVolumeUnitsBlockPage.submit()).click(); - expect($(SubmitPage.durationHour()).getText()).to.equal("6 awr"); - expect($(SubmitPage.durationYear()).getText()).to.equal("20 mlynedd"); + it("Given we open a questionnaire with unit labels, when the weight unit label is highlighted by the tooltip, then the correct unit label should be displayed.", async () => { + await browser.openQuestionnaire("test_unit_patterns.json", { language: "en" }); + await click(SetLengthUnitsBlockPage.submit()); + await click(SetDurationUnitsBlockPage.submit()); + await click(SetAreaUnitsBlockPage.submit()); + await click(SetVolumeUnitsBlockPage.submit()); + await expect(await $("body").getText()).toContain("tonnes"); }); - it("Given we open a questionnaire with unit labels, when the label is highlighted by the tooltip, then the long unit label should be displayed.", () => { - browser.openQuestionnaire("test_unit_patterns.json", { language: "en" }); - expect($(SetLengthUnitsBlockPage.centimetres()).getAttribute("title")).to.equal("centimeters"); - expect($(SetLengthUnitsBlockPage.metres()).getAttribute("title")).to.equal("meters"); - expect($(SetLengthUnitsBlockPage.kilometres()).getAttribute("title")).to.equal("kilometers"); - expect($(SetLengthUnitsBlockPage.miles()).getAttribute("title")).to.equal("miles"); + it("Given we open a questionnaire with unit inputs, when the unit allows a maximum of 6 decimal places, then the correct number of decimal places should be displayed on the summary.", async () => { + await browser.openQuestionnaire("test_unit_patterns.json", { language: "en" }); + await $(SetLengthUnitsBlockPage.submit()).click(); + await $(SetDurationUnitsBlockPage.submit()).click(); + await $(SetAreaUnitsBlockPage.submit()).click(); + await $(SetVolumeUnitsBlockPage.cubicCentimetres()).setValue(1.1); + await $(SetVolumeUnitsBlockPage.cubicMetres()).setValue(1.12); + await $(SetVolumeUnitsBlockPage.litres()).setValue(1.123); + await $(SetVolumeUnitsBlockPage.hectolitres()).setValue(1.1234); + await $(SetVolumeUnitsBlockPage.megalitres()).setValue("1.10000"); + await $(SetVolumeUnitsBlockPage.submit()).click(); + await $(SetWeightUnitsBlockPage.submit()).click(); + await expect(await $(SubmitPage.cubicCentimetres()).getText()).toBe("1.1 cmÂŗ"); + await expect(await $(SubmitPage.cubicMetres()).getText()).toBe("1.12 mÂŗ"); + await expect(await $(SubmitPage.litres()).getText()).toBe("1.123 l"); + await expect(await $(SubmitPage.hectolitres()).getText()).toBe("1.1234 hl"); + await expect(await $(SubmitPage.megalitres()).getText()).toBe("1.10000 Ml"); }); }); diff --git a/tests/functional/spec/features/validation/date_validation/date-combined-mm-yyyy.spec.js b/tests/functional/spec/features/validation/date_validation/date-combined-mm-yyyy.spec.js index b6475742d7..89f1669f7c 100644 --- a/tests/functional/spec/features/validation/date_validation/date-combined-mm-yyyy.spec.js +++ b/tests/functional/spec/features/validation/date_validation/date-combined-mm-yyyy.spec.js @@ -1,92 +1,92 @@ import DateRangePage from "../../../../generated_pages/date_validation_mm_yyyy_combined/date-range-block.page"; import SubmitPage from "../../../../generated_pages/date_validation_mm_yyyy_combined/submit.page"; - +import { click } from "../../../../helpers"; describe("Feature: Combined question level and single validation for MM-YYYY dates", () => { - before(() => { - browser.openQuestionnaire("test_date_validation_mm_yyyy_combined.json"); + before(async () => { + await browser.openQuestionnaire("test_date_validation_mm_yyyy_combined.json"); }); describe("Period Validation", () => { describe("Given I enter dates", () => { - it("When I enter a month but no year, Then I should see only a single invalid date error", () => { - $(DateRangePage.dateRangeFromYear()).setValue(2018); - - $(DateRangePage.dateRangeToMonth()).setValue(4); - $(DateRangePage.dateRangeToYear()).setValue(2017); - $(DateRangePage.submit()).click(); - expect($(DateRangePage.errorNumber(1)).getText()).to.contain("Enter a valid date"); - expect($(DateRangePage.errorNumber(2)).isExisting()).to.be.false; + it("When I enter a month but no year, Then I should see only a single invalid date error", async () => { + await $(DateRangePage.dateRangeFromYear()).setValue(2018); + + await $(DateRangePage.dateRangeToMonth()).setValue(4); + await $(DateRangePage.dateRangeToYear()).setValue(2017); + await click(DateRangePage.submit()); + await expect(await $(DateRangePage.errorNumber(1)).getText()).toBe("Enter a valid date"); + await expect(await $(DateRangePage.errorNumber(2)).isExisting()).toBe(false); }); - it("When I enter a year but no month, Then I should see only a single invalid date error", () => { - $(DateRangePage.dateRangeFromMonth()).setValue(10); - $(DateRangePage.dateRangeFromYear()).setValue(""); + it("When I enter a year but no month, Then I should see only a single invalid date error", async () => { + await $(DateRangePage.dateRangeFromMonth()).setValue(10); + await $(DateRangePage.dateRangeFromYear()).setValue(""); - $(DateRangePage.dateRangeToMonth()).setValue(4); - $(DateRangePage.dateRangeToYear()).setValue(2017); - $(DateRangePage.submit()).click(); - expect($(DateRangePage.errorNumber(1)).getText()).to.contain("Enter a valid date"); - expect($(DateRangePage.errorNumber(2)).isExisting()).to.be.false; + await $(DateRangePage.dateRangeToMonth()).setValue(4); + await $(DateRangePage.dateRangeToYear()).setValue(2017); + await click(DateRangePage.submit()); + await expect(await $(DateRangePage.errorNumber(1)).getText()).toBe("Enter a valid date"); + await expect(await $(DateRangePage.errorNumber(2)).isExisting()).toBe(false); }); - it("When I enter a year of 0, Then I should see only a single invalid date error", () => { - $(DateRangePage.dateRangeFromMonth()).setValue(10); - $(DateRangePage.dateRangeFromYear()).setValue(0); + it("When I enter a year of 0, Then I should see only a single invalid date error", async () => { + await $(DateRangePage.dateRangeFromMonth()).setValue(10); + await $(DateRangePage.dateRangeFromYear()).setValue(0); - $(DateRangePage.dateRangeToMonth()).setValue(4); - $(DateRangePage.dateRangeToYear()).setValue(2017); - $(DateRangePage.submit()).click(); - expect($(DateRangePage.errorNumber(1)).getText()).to.contain("Enter a valid date"); - expect($(DateRangePage.errorNumber(2)).isExisting()).to.be.false; + await $(DateRangePage.dateRangeToMonth()).setValue(4); + await $(DateRangePage.dateRangeToYear()).setValue(2017); + await click(DateRangePage.submit()); + await expect(await $(DateRangePage.errorNumber(1)).getText()).toBe("Enter the year in a valid format. For example, 2023."); + await expect(await $(DateRangePage.errorNumber(2)).isExisting()).toBe(false); }); - it("When I enter a single dates that are too early/late, Then I should see a single validation errors", () => { - $(DateRangePage.dateRangeFromMonth()).setValue(10); - $(DateRangePage.dateRangeFromYear()).setValue(2016); + it("When I enter a single dates that are too early/late, Then I should see a single validation errors", async () => { + await $(DateRangePage.dateRangeFromMonth()).setValue(10); + await $(DateRangePage.dateRangeFromYear()).setValue(2016); - $(DateRangePage.dateRangeToMonth()).setValue(6); - $(DateRangePage.dateRangeToYear()).setValue(2017); - $(DateRangePage.submit()).click(); - expect($(DateRangePage.errorNumber(1)).getText()).to.contain("Enter a date after November 2016"); - expect($(DateRangePage.errorNumber(2)).getText()).to.contain("Enter a date before June 2017"); + await $(DateRangePage.dateRangeToMonth()).setValue(6); + await $(DateRangePage.dateRangeToYear()).setValue(2017); + await click(DateRangePage.submit()); + await expect(await $(DateRangePage.errorNumber(1)).getText()).toBe("Enter a date after November 2016"); + await expect(await $(DateRangePage.errorNumber(2)).getText()).toBe("Enter a date before June 2017"); }); - it("When I enter a range too large, Then I should see a range validation error", () => { - $(DateRangePage.dateRangeFromMonth()).setValue(12); - $(DateRangePage.dateRangeFromYear()).setValue(2016); + it("When I enter a range too large, Then I should see a range validation error", async () => { + await $(DateRangePage.dateRangeFromMonth()).setValue(12); + await $(DateRangePage.dateRangeFromYear()).setValue(2016); - $(DateRangePage.dateRangeToMonth()).setValue(5); - $(DateRangePage.dateRangeToYear()).setValue(2017); - $(DateRangePage.submit()).click(); - expect($(DateRangePage.errorNumber(1)).getText()).to.contain("Enter a reporting period less than or equal to 3 months"); + await $(DateRangePage.dateRangeToMonth()).setValue(5); + await $(DateRangePage.dateRangeToYear()).setValue(2017); + await click(DateRangePage.submit()); + await expect(await $(DateRangePage.errorNumber(1)).getText()).toBe("Enter a reporting period less than or equal to 3 months"); }); - it("When I enter a range too small, Then I should see a range validation error", () => { - $(DateRangePage.dateRangeFromMonth()).setValue(12); - $(DateRangePage.dateRangeFromYear()).setValue(2016); + it("When I enter a range too small, Then I should see a range validation error", async () => { + await $(DateRangePage.dateRangeFromMonth()).setValue(12); + await $(DateRangePage.dateRangeFromYear()).setValue(2016); - $(DateRangePage.dateRangeToMonth()).setValue(1); - $(DateRangePage.dateRangeToYear()).setValue(2017); - $(DateRangePage.submit()).click(); - expect($(DateRangePage.errorNumber(1)).getText()).to.contain("Enter a reporting period greater than or equal to 2 months"); + await $(DateRangePage.dateRangeToMonth()).setValue(1); + await $(DateRangePage.dateRangeToYear()).setValue(2017); + await click(DateRangePage.submit()); + await expect(await $(DateRangePage.errorNumber(1)).getText()).toBe("Enter a reporting period greater than or equal to 2 months"); }); - it("When I enter valid dates, Then I should see the summary page", () => { - $(DateRangePage.dateRangeFromMonth()).setValue(1); - $(DateRangePage.dateRangeFromYear()).setValue(2017); + it("When I enter valid dates, Then I should see the summary page", async () => { + await $(DateRangePage.dateRangeFromMonth()).setValue(1); + await $(DateRangePage.dateRangeFromYear()).setValue(2017); // Min range - $(DateRangePage.dateRangeToMonth()).setValue(3); - $(DateRangePage.dateRangeToYear()).setValue(2017); - $(DateRangePage.submit()).click(); - expect($(SubmitPage.dateRangeFrom()).getText()).to.contain("January 2017 to March 2017"); + await $(DateRangePage.dateRangeToMonth()).setValue(3); + await $(DateRangePage.dateRangeToYear()).setValue(2017); + await click(DateRangePage.submit()); + await expect(await $(SubmitPage.dateRangeFrom()).getText()).toBe("January 2017 to March 2017"); // Max range - $(SubmitPage.dateRangeFromEdit()).click(); - $(DateRangePage.dateRangeToMonth()).setValue(4); - $(DateRangePage.dateRangeToYear()).setValue(2017); - $(DateRangePage.submit()).click(); - expect($(SubmitPage.dateRangeFrom()).getText()).to.contain("January 2017 to April 2017"); + await $(SubmitPage.dateRangeFromEdit()).click(); + await $(DateRangePage.dateRangeToMonth()).setValue(4); + await $(DateRangePage.dateRangeToYear()).setValue(2017); + await click(DateRangePage.submit()); + await expect(await $(SubmitPage.dateRangeFrom()).getText()).toBe("January 2017 to April 2017"); }); }); }); diff --git a/tests/functional/spec/features/validation/date_validation/date-combined-yyyy.spec.js b/tests/functional/spec/features/validation/date_validation/date-combined-yyyy.spec.js index eb0e665296..2ac4a34260 100644 --- a/tests/functional/spec/features/validation/date_validation/date-combined-yyyy.spec.js +++ b/tests/functional/spec/features/validation/date_validation/date-combined-yyyy.spec.js @@ -1,47 +1,47 @@ import DateRangePage from "../../../../generated_pages/date_validation_yyyy_combined/date-range-block.page"; import SubmitPage from "../../../../generated_pages/date_validation_yyyy_combined/submit.page"; - +import { click } from "../../../../helpers"; describe("Feature: Combined question level and single validation for MM-YYYY dates", () => { - before(() => { - browser.openQuestionnaire("test_date_validation_yyyy_combined.json"); + before(async () => { + await browser.openQuestionnaire("test_date_validation_yyyy_combined.json"); }); describe("Period Validation", () => { describe("Given I enter dates", () => { - it("When I enter dates that are too early and too late, Then I should see two validation errors", () => { - $(DateRangePage.dateRangeFromYear()).setValue(2015); - $(DateRangePage.dateRangeToYear()).setValue(2021); - $(DateRangePage.submit()).click(); - expect($(DateRangePage.errorNumber(1)).getText()).to.contain("Enter a date after 2015"); - expect($(DateRangePage.errorNumber(2)).getText()).to.contain("Enter a date before 2021"); + it("When I enter dates that are too early and too late, Then I should see two validation errors", async () => { + await $(DateRangePage.dateRangeFromYear()).setValue(2015); + await $(DateRangePage.dateRangeToYear()).setValue(2021); + await click(DateRangePage.submit()); + await expect(await $(DateRangePage.errorNumber(1)).getText()).toBe("Enter a date after 2015"); + await expect(await $(DateRangePage.errorNumber(2)).getText()).toBe("Enter a date before 2021"); }); - it("When I enter a range too large, Then I should see a range validation error", () => { - $(DateRangePage.dateRangeFromYear()).setValue(2016); - $(DateRangePage.dateRangeToYear()).setValue(2020); - $(DateRangePage.submit()).click(); - expect($(DateRangePage.errorNumber(1)).getText()).to.contain("Enter a reporting period less than or equal to 3 years"); + it("When I enter a range too large, Then I should see a range validation error", async () => { + await $(DateRangePage.dateRangeFromYear()).setValue(2016); + await $(DateRangePage.dateRangeToYear()).setValue(2020); + await click(DateRangePage.submit()); + await expect(await $(DateRangePage.errorNumber(1)).getText()).toBe("Enter a reporting period less than or equal to 3 years"); }); - it("When I enter a range too small, Then I should see a range validation error", () => { - $(DateRangePage.dateRangeFromYear()).setValue(2016); - $(DateRangePage.dateRangeToYear()).setValue(2017); - $(DateRangePage.submit()).click(); - expect($(DateRangePage.errorNumber(1)).getText()).to.contain("Enter a reporting period greater than or equal to 2 years"); + it("When I enter a range too small, Then I should see a range validation error", async () => { + await $(DateRangePage.dateRangeFromYear()).setValue(2016); + await $(DateRangePage.dateRangeToYear()).setValue(2017); + await click(DateRangePage.submit()); + await expect(await $(DateRangePage.errorNumber(1)).getText()).toBe("Enter a reporting period greater than or equal to 2 years"); }); - it("When I enter valid dates, Then I should see the summary page", () => { - $(DateRangePage.dateRangeFromYear()).setValue(2016); + it("When I enter valid dates, Then I should see the summary page", async () => { + await $(DateRangePage.dateRangeFromYear()).setValue(2016); // Min range - $(DateRangePage.dateRangeToYear()).setValue(2018); - $(DateRangePage.submit()).click(); - expect($(SubmitPage.dateRangeFrom()).getText()).to.contain("2016 to 2018"); + await $(DateRangePage.dateRangeToYear()).setValue(2018); + await click(DateRangePage.submit()); + await expect(await $(SubmitPage.dateRangeFrom()).getText()).toBe("2016 to 2018"); // Max range - $(SubmitPage.dateRangeFromEdit()).click(); - $(DateRangePage.dateRangeToYear()).setValue(2019); - $(DateRangePage.submit()).click(); - expect($(SubmitPage.dateRangeFrom()).getText()).to.contain("2016 to 2019"); + await $(SubmitPage.dateRangeFromEdit()).click(); + await $(DateRangePage.dateRangeToYear()).setValue(2019); + await click(DateRangePage.submit()); + await expect(await $(SubmitPage.dateRangeFrom()).getText()).toBe("2016 to 2019"); }); }); }); diff --git a/tests/functional/spec/features/validation/date_validation/date-combined.spec.js b/tests/functional/spec/features/validation/date_validation/date-combined.spec.js index 5470152fc6..c108a6b6ed 100644 --- a/tests/functional/spec/features/validation/date_validation/date-combined.spec.js +++ b/tests/functional/spec/features/validation/date_validation/date-combined.spec.js @@ -1,69 +1,70 @@ import DateRangePage from "../../../../generated_pages/date_validation_combined/date-range-block.page"; import SubmitPage from "../../../../generated_pages/date_validation_combined/submit.page"; +import { click } from "../../../../helpers"; describe("Feature: Combined question level and single validation for dates", () => { - before(() => { - browser.openQuestionnaire("test_date_validation_combined.json"); + before(async () => { + await browser.openQuestionnaire("test_date_validation_combined.json"); }); describe("Period Validation", () => { describe("Given I enter dates", () => { - it("When I enter a single dates that are too early/late, Then I should see a single validation errors", () => { - $(DateRangePage.dateRangeFromday()).setValue(12); - $(DateRangePage.dateRangeFrommonth()).setValue(12); - $(DateRangePage.dateRangeFromyear()).setValue(2016); + it("When I enter a single dates that are too early/late, Then I should see a single validation errors", async () => { + await $(DateRangePage.dateRangeFromday()).setValue(12); + await $(DateRangePage.dateRangeFrommonth()).setValue(12); + await $(DateRangePage.dateRangeFromyear()).setValue(2016); - $(DateRangePage.dateRangeToday()).setValue(22); - $(DateRangePage.dateRangeTomonth()).setValue(2); - $(DateRangePage.dateRangeToyear()).setValue(2017); - $(DateRangePage.submit()).click(); - expect($(DateRangePage.errorNumber(1)).getText()).to.contain("Enter a date after 12 December 2016"); - expect($(DateRangePage.errorNumber(2)).getText()).to.contain("Enter a date before 22 February 2017"); + await $(DateRangePage.dateRangeToday()).setValue(22); + await $(DateRangePage.dateRangeTomonth()).setValue(2); + await $(DateRangePage.dateRangeToyear()).setValue(2017); + await click(DateRangePage.submit()); + await expect(await $(DateRangePage.errorNumber(1)).getText()).toBe("Enter a date after 12 December 2016"); + await expect(await $(DateRangePage.errorNumber(2)).getText()).toBe("Enter a date before 22 February 2017"); }); - it("When I enter a range too large, Then I should see a range validation error", () => { - $(DateRangePage.dateRangeFromday()).setValue(13); - $(DateRangePage.dateRangeFrommonth()).setValue(12); - $(DateRangePage.dateRangeFromyear()).setValue(2016); + it("When I enter a range too large, Then I should see a range validation error", async () => { + await $(DateRangePage.dateRangeFromday()).setValue(13); + await $(DateRangePage.dateRangeFrommonth()).setValue(12); + await $(DateRangePage.dateRangeFromyear()).setValue(2016); - $(DateRangePage.dateRangeToday()).setValue(21); - $(DateRangePage.dateRangeTomonth()).setValue(2); - $(DateRangePage.dateRangeToyear()).setValue(2017); - $(DateRangePage.submit()).click(); - expect($(DateRangePage.errorNumber(1)).getText()).to.contain("Enter a reporting period less than or equal to 50 days"); + await $(DateRangePage.dateRangeToday()).setValue(21); + await $(DateRangePage.dateRangeTomonth()).setValue(2); + await $(DateRangePage.dateRangeToyear()).setValue(2017); + await click(DateRangePage.submit()); + await expect(await $(DateRangePage.errorNumber(1)).getText()).toBe("Enter a reporting period less than or equal to 50 days"); }); - it("When I enter a range too small, Then I should see a range validation error", () => { - $(DateRangePage.dateRangeFromday()).setValue(1); - $(DateRangePage.dateRangeFrommonth()).setValue(1); - $(DateRangePage.dateRangeFromyear()).setValue(2017); + it("When I enter a range too small, Then I should see a range validation error", async () => { + await $(DateRangePage.dateRangeFromday()).setValue(1); + await $(DateRangePage.dateRangeFrommonth()).setValue(1); + await $(DateRangePage.dateRangeFromyear()).setValue(2017); - $(DateRangePage.dateRangeToday()).setValue(10); - $(DateRangePage.dateRangeTomonth()).setValue(1); - $(DateRangePage.dateRangeToyear()).setValue(2017); - $(DateRangePage.submit()).click(); - expect($(DateRangePage.errorNumber(1)).getText()).to.contain("Enter a reporting period greater than or equal to 10 days"); + await $(DateRangePage.dateRangeToday()).setValue(10); + await $(DateRangePage.dateRangeTomonth()).setValue(1); + await $(DateRangePage.dateRangeToyear()).setValue(2017); + await click(DateRangePage.submit()); + await expect(await $(DateRangePage.errorNumber(1)).getText()).toBe("Enter a reporting period greater than or equal to 10 days"); }); - it("When I enter valid dates, Then I should see the summary page", () => { - $(DateRangePage.dateRangeFromday()).setValue(1); - $(DateRangePage.dateRangeFrommonth()).setValue(1); - $(DateRangePage.dateRangeFromyear()).setValue(2017); + it("When I enter valid dates, Then I should see the summary page", async () => { + await $(DateRangePage.dateRangeFromday()).setValue(1); + await $(DateRangePage.dateRangeFrommonth()).setValue(1); + await $(DateRangePage.dateRangeFromyear()).setValue(2017); // Min range - $(DateRangePage.dateRangeToday()).setValue(11); - $(DateRangePage.dateRangeTomonth()).setValue(1); - $(DateRangePage.dateRangeToyear()).setValue(2017); - $(DateRangePage.submit()).click(); - expect($(SubmitPage.dateRangeFrom()).getText()).to.contain("1 January 2017 to 11 January 2017"); + await $(DateRangePage.dateRangeToday()).setValue(11); + await $(DateRangePage.dateRangeTomonth()).setValue(1); + await $(DateRangePage.dateRangeToyear()).setValue(2017); + await click(DateRangePage.submit()); + await expect(await $(SubmitPage.dateRangeFrom()).getText()).toBe("1 January 2017 to 11 January 2017"); // Max range - $(SubmitPage.dateRangeFromEdit()).click(); - $(DateRangePage.dateRangeToday()).setValue(20); - $(DateRangePage.dateRangeTomonth()).setValue(2); - $(DateRangePage.dateRangeToyear()).setValue(2017); - $(DateRangePage.submit()).click(); - expect($(SubmitPage.dateRangeFrom()).getText()).to.contain("1 January 2017 to 20 February 2017"); + await $(SubmitPage.dateRangeFromEdit()).click(); + await $(DateRangePage.dateRangeToday()).setValue(20); + await $(DateRangePage.dateRangeTomonth()).setValue(2); + await $(DateRangePage.dateRangeToyear()).setValue(2017); + await click(DateRangePage.submit()); + await expect(await $(SubmitPage.dateRangeFrom()).getText()).toBe("1 January 2017 to 20 February 2017"); }); }); }); diff --git a/tests/functional/spec/features/validation/date_validation/date-range.spec.js b/tests/functional/spec/features/validation/date_validation/date-range.spec.js index b3cb191c77..1f790e6f3a 100644 --- a/tests/functional/spec/features/validation/date_validation/date-range.spec.js +++ b/tests/functional/spec/features/validation/date_validation/date-range.spec.js @@ -1,95 +1,95 @@ import DateRangePage from "../../../../generated_pages/date_validation_range/date-range-block.page"; import SubmitPage from "../../../../generated_pages/date_validation_range/submit.page"; - +import { click, verifyUrlContains } from "../../../../helpers"; describe("Feature: Question level validation for date ranges", () => { - beforeEach(() => { - browser.openQuestionnaire("test_date_validation_range.json"); + beforeEach(async () => { + await browser.openQuestionnaire("test_date_validation_range.json"); }); describe("Period Validation", () => { describe("Given I enter a date period greater than the max period limit", () => { - it("When I continue, Then I should see a period validation error", () => { - $(DateRangePage.dateRangeFromday()).setValue(1); - $(DateRangePage.dateRangeFrommonth()).setValue(1); - $(DateRangePage.dateRangeFromyear()).setValue(2018); + it("When I continue, Then I should see a period validation error", async () => { + await $(DateRangePage.dateRangeFromday()).setValue(1); + await $(DateRangePage.dateRangeFrommonth()).setValue(1); + await $(DateRangePage.dateRangeFromyear()).setValue(2018); - $(DateRangePage.dateRangeToday()).setValue(3); - $(DateRangePage.dateRangeTomonth()).setValue(3); - $(DateRangePage.dateRangeToyear()).setValue(2018); - $(DateRangePage.submit()).click(); - expect($(DateRangePage.errorNumber(1)).getText()).to.contain("Enter a reporting period less than or equal to 1 month, 20 days"); + await $(DateRangePage.dateRangeToday()).setValue(3); + await $(DateRangePage.dateRangeTomonth()).setValue(3); + await $(DateRangePage.dateRangeToyear()).setValue(2018); + await click(DateRangePage.submit()); + await expect(await $(DateRangePage.errorNumber(1)).getText()).toBe("Enter a reporting period less than or equal to 1 month, 20 days"); }); }); describe("Given I enter a date period less than the min period limit", () => { - it("When I continue, Then I should see a period validation error", () => { - $(DateRangePage.dateRangeFromday()).setValue(1); - $(DateRangePage.dateRangeFrommonth()).setValue(1); - $(DateRangePage.dateRangeFromyear()).setValue(2018); + it("When I continue, Then I should see a period validation error", async () => { + await $(DateRangePage.dateRangeFromday()).setValue(1); + await $(DateRangePage.dateRangeFrommonth()).setValue(1); + await $(DateRangePage.dateRangeFromyear()).setValue(2018); - $(DateRangePage.dateRangeToday()).setValue(3); - $(DateRangePage.dateRangeTomonth()).setValue(1); - $(DateRangePage.dateRangeToyear()).setValue(2018); - $(DateRangePage.submit()).click(); - expect($(DateRangePage.errorNumber(1)).getText()).to.contain("Enter a reporting period greater than or equal to 23 days"); + await $(DateRangePage.dateRangeToday()).setValue(3); + await $(DateRangePage.dateRangeTomonth()).setValue(1); + await $(DateRangePage.dateRangeToyear()).setValue(2018); + await click(DateRangePage.submit()); + await expect(await $(DateRangePage.errorNumber(1)).getText()).toBe("Enter a reporting period greater than or equal to 23 days"); }); }); describe("Given I enter a date period within the set period limits", () => { - it("When I continue, Then I should be able to reach the summary", () => { - $(DateRangePage.dateRangeFromday()).setValue(1); - $(DateRangePage.dateRangeFrommonth()).setValue(1); - $(DateRangePage.dateRangeFromyear()).setValue(2018); + it("When I continue, Then I should be able to reach the summary", async () => { + await $(DateRangePage.dateRangeFromday()).setValue(1); + await $(DateRangePage.dateRangeFrommonth()).setValue(1); + await $(DateRangePage.dateRangeFromyear()).setValue(2018); - $(DateRangePage.dateRangeToday()).setValue(3); - $(DateRangePage.dateRangeTomonth()).setValue(2); - $(DateRangePage.dateRangeToyear()).setValue(2018); - $(DateRangePage.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); + await $(DateRangePage.dateRangeToday()).setValue(3); + await $(DateRangePage.dateRangeTomonth()).setValue(2); + await $(DateRangePage.dateRangeToyear()).setValue(2018); + await click(DateRangePage.submit()); + await verifyUrlContains(SubmitPage.pageName); }); }); }); describe("Date Range Validation", () => { describe('Given I enter a "to date" which is earlier than the "from date"', () => { - it("When I continue, Then I should see a validation error", () => { - $(DateRangePage.dateRangeFromday()).setValue(1); - $(DateRangePage.dateRangeFrommonth()).setValue(2); - $(DateRangePage.dateRangeFromyear()).setValue(2018); + it("When I continue, Then I should see a validation error", async () => { + await $(DateRangePage.dateRangeFromday()).setValue(1); + await $(DateRangePage.dateRangeFrommonth()).setValue(2); + await $(DateRangePage.dateRangeFromyear()).setValue(2018); - $(DateRangePage.dateRangeToday()).setValue(3); - $(DateRangePage.dateRangeTomonth()).setValue(1); - $(DateRangePage.dateRangeToyear()).setValue(2018); - $(DateRangePage.submit()).click(); - expect($(DateRangePage.errorNumber(1)).getText()).to.contain("Enter a 'period to' date later than the 'period from' date"); + await $(DateRangePage.dateRangeToday()).setValue(3); + await $(DateRangePage.dateRangeTomonth()).setValue(1); + await $(DateRangePage.dateRangeToyear()).setValue(2018); + await click(DateRangePage.submit()); + await expect(await $(DateRangePage.errorNumber(1)).getText()).toBe("Enter a 'period to' date later than the 'period from' date"); }); }); describe('Given I enter matching dates for the "from" and "to" dates', () => { - it("When I continue, Then I should see a validation error", () => { - $(DateRangePage.dateRangeFromday()).setValue(1); - $(DateRangePage.dateRangeFrommonth()).setValue(1); - $(DateRangePage.dateRangeFromyear()).setValue(2018); + it("When I continue, Then I should see a validation error", async () => { + await $(DateRangePage.dateRangeFromday()).setValue(1); + await $(DateRangePage.dateRangeFrommonth()).setValue(1); + await $(DateRangePage.dateRangeFromyear()).setValue(2018); - $(DateRangePage.dateRangeToday()).setValue(1); - $(DateRangePage.dateRangeTomonth()).setValue(1); - $(DateRangePage.dateRangeToyear()).setValue(2018); - $(DateRangePage.submit()).click(); - expect($(DateRangePage.errorNumber(1)).getText()).to.contain("Enter a 'period to' date later than the 'period from' date"); + await $(DateRangePage.dateRangeToday()).setValue(1); + await $(DateRangePage.dateRangeTomonth()).setValue(1); + await $(DateRangePage.dateRangeToyear()).setValue(2018); + await click(DateRangePage.submit()); + await expect(await $(DateRangePage.errorNumber(1)).getText()).toBe("Enter a 'period to' date later than the 'period from' date"); }); }); describe("Given I enter a valid date range", () => { - it("When I continue, Then I should be able to reach the summary", () => { - $(DateRangePage.dateRangeFromday()).setValue(1); - $(DateRangePage.dateRangeFrommonth()).setValue(1); - $(DateRangePage.dateRangeFromyear()).setValue(2018); + it("When I continue, Then I should be able to reach the summary", async () => { + await $(DateRangePage.dateRangeFromday()).setValue(1); + await $(DateRangePage.dateRangeFrommonth()).setValue(1); + await $(DateRangePage.dateRangeFromyear()).setValue(2018); - $(DateRangePage.dateRangeToday()).setValue(3); - $(DateRangePage.dateRangeTomonth()).setValue(2); - $(DateRangePage.dateRangeToyear()).setValue(2018); - $(DateRangePage.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); + await $(DateRangePage.dateRangeToday()).setValue(3); + await $(DateRangePage.dateRangeTomonth()).setValue(2); + await $(DateRangePage.dateRangeToyear()).setValue(2018); + await click(DateRangePage.submit()); + await verifyUrlContains(SubmitPage.pageName); }); }); }); diff --git a/tests/functional/spec/features/validation/date_validation/date-single.spec.js b/tests/functional/spec/features/validation/date_validation/date-single.spec.js index 7c66aab5bd..43e7c06a67 100644 --- a/tests/functional/spec/features/validation/date_validation/date-single.spec.js +++ b/tests/functional/spec/features/validation/date_validation/date-single.spec.js @@ -1,77 +1,77 @@ import DatePage from "../../../../generated_pages/date_validation_single/date-block.page"; import DatePeriodPage from "../../../../generated_pages/date_validation_single/date-range-block.page"; import SubmitPage from "../../../../generated_pages/date_validation_single/submit.page"; - +import { click, verifyUrlContains } from "../../../../helpers"; describe("Feature: Validation for single date periods", () => { - beforeEach(() => { - browser.openQuestionnaire("test_date_validation_single.json"); - completeFirstDatePage(); + beforeEach(async () => { + await browser.openQuestionnaire("test_date_validation_single.json"); + await completeFirstDatePage(); }); describe("Given I enter a date before the minimum offset meta date", () => { - it("When I continue, Then I should see a period validation error", () => { - $(DatePeriodPage.dateRangeFromday()).setValue(13); - $(DatePeriodPage.dateRangeFrommonth()).setValue(2); - $(DatePeriodPage.dateRangeFromyear()).setValue(2016); - $(DatePeriodPage.submit()).click(); + it("When I continue, Then I should see a period validation error", async () => { + await $(DatePeriodPage.dateRangeFromday()).setValue(13); + await $(DatePeriodPage.dateRangeFrommonth()).setValue(2); + await $(DatePeriodPage.dateRangeFromyear()).setValue(2016); + await click(DatePeriodPage.submit()); - $(DatePeriodPage.dateRangeToday()).setValue(3); - $(DatePeriodPage.dateRangeTomonth()).setValue(3); - $(DatePeriodPage.dateRangeToyear()).setValue(2018); - $(DatePeriodPage.submit()).click(); - expect($(DatePeriodPage.errorNumber(1)).getText()).to.contain("Enter a date after 12 December 2016"); + await $(DatePeriodPage.dateRangeToday()).setValue(3); + await $(DatePeriodPage.dateRangeTomonth()).setValue(3); + await $(DatePeriodPage.dateRangeToyear()).setValue(2018); + await click(DatePeriodPage.submit()); + await expect(await $(DatePeriodPage.errorNumber(1)).getText()).toBe("Enter a date after 12 December 2016"); }); }); describe("Given I enter a date after the maximum offset value date", () => { - it("When I continue, Then I should see a period validation error", () => { - $(DatePeriodPage.dateRangeFromday()).setValue(13); - $(DatePeriodPage.dateRangeFrommonth()).setValue(7); - $(DatePeriodPage.dateRangeFromyear()).setValue(2017); - $(DatePeriodPage.submit()).click(); + it("When I continue, Then I should see a period validation error", async () => { + await $(DatePeriodPage.dateRangeFromday()).setValue(13); + await $(DatePeriodPage.dateRangeFrommonth()).setValue(7); + await $(DatePeriodPage.dateRangeFromyear()).setValue(2017); + await click(DatePeriodPage.submit()); - $(DatePeriodPage.dateRangeToday()).setValue(3); - $(DatePeriodPage.dateRangeTomonth()).setValue(3); - $(DatePeriodPage.dateRangeToyear()).setValue(2018); - $(DatePeriodPage.submit()).click(); - expect($(DatePeriodPage.errorNumber(1)).getText()).to.contain("Enter a date before 2 July 2017"); + await $(DatePeriodPage.dateRangeToday()).setValue(3); + await $(DatePeriodPage.dateRangeTomonth()).setValue(3); + await $(DatePeriodPage.dateRangeToyear()).setValue(2018); + await click(DatePeriodPage.submit()); + await expect(await $(DatePeriodPage.errorNumber(1)).getText()).toBe("Enter a date before 2 July 2017"); }); }); describe("Given I enter a date before the minimum offset answer id date", () => { - it("When I continue, Then I should see a period validation error", () => { - $(DatePeriodPage.dateRangeFromday()).setValue(13); - $(DatePeriodPage.dateRangeFrommonth()).setValue(11); - $(DatePeriodPage.dateRangeFromyear()).setValue(2016); - $(DatePeriodPage.submit()).click(); + it("When I continue, Then I should see a period validation error", async () => { + await $(DatePeriodPage.dateRangeFromday()).setValue(13); + await $(DatePeriodPage.dateRangeFrommonth()).setValue(11); + await $(DatePeriodPage.dateRangeFromyear()).setValue(2016); + await click(DatePeriodPage.submit()); - $(DatePeriodPage.dateRangeToday()).setValue(13); - $(DatePeriodPage.dateRangeTomonth()).setValue(1); - $(DatePeriodPage.dateRangeToyear()).setValue(2018); - $(DatePeriodPage.submit()).click(); - expect($(DatePeriodPage.errorNumber(2)).getText()).to.contain("Enter a date after 10 February 2018"); + await $(DatePeriodPage.dateRangeToday()).setValue(13); + await $(DatePeriodPage.dateRangeTomonth()).setValue(1); + await $(DatePeriodPage.dateRangeToyear()).setValue(2018); + await click(DatePeriodPage.submit()); + await expect(await $(DatePeriodPage.errorNumber(2)).getText()).toBe("Enter a date after 10 February 2018"); }); }); describe("Given I enter a date in between the minimum offset meta date and the maximum offset value date", () => { - it("When I continue, Then I should be able to reach the summary", () => { - $(DatePeriodPage.dateRangeFromday()).setValue(13); - $(DatePeriodPage.dateRangeFrommonth()).setValue(12); - $(DatePeriodPage.dateRangeFromyear()).setValue(2016); - $(DatePeriodPage.submit()).click(); + it("When I continue, Then I should be able to reach the summary", async () => { + await $(DatePeriodPage.dateRangeFromday()).setValue(13); + await $(DatePeriodPage.dateRangeFrommonth()).setValue(12); + await $(DatePeriodPage.dateRangeFromyear()).setValue(2016); + await click(DatePeriodPage.submit()); - $(DatePeriodPage.dateRangeToday()).setValue(11); - $(DatePeriodPage.dateRangeTomonth()).setValue(2); - $(DatePeriodPage.dateRangeToyear()).setValue(2018); - $(DatePeriodPage.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); + await $(DatePeriodPage.dateRangeToday()).setValue(11); + await $(DatePeriodPage.dateRangeTomonth()).setValue(2); + await $(DatePeriodPage.dateRangeToyear()).setValue(2018); + await click(DatePeriodPage.submit()); + await verifyUrlContains(SubmitPage.pageName); }); }); - function completeFirstDatePage() { - $(DatePage.day()).setValue(1); - $(DatePage.month()).setValue(1); - $(DatePage.year()).setValue(2018); - $(DatePage.submit()).click(); + async function completeFirstDatePage() { + await $(DatePage.day()).setValue(1); + await $(DatePage.month()).setValue(1); + await $(DatePage.year()).setValue(2018); + await click(DatePage.submit()); } }); diff --git a/tests/functional/spec/features/validation/sum/dynamic.spec.js b/tests/functional/spec/features/validation/sum/dynamic.spec.js new file mode 100644 index 0000000000..71c218eb53 --- /dev/null +++ b/tests/functional/spec/features/validation/sum/dynamic.spec.js @@ -0,0 +1,148 @@ +import ListCollectorPage from "../../../../generated_pages/validation_sum_against_total_dynamic_answers/list-collector.page"; +import ListCollectorAddPage from "../../../../generated_pages/validation_sum_against_total_dynamic_answers/list-collector-add.page"; +import DynamicAnswerPage from "../../../../generated_pages/validation_sum_against_total_dynamic_answers/dynamic-answer.page"; +import DynamicAnswerOnlyPage from "../../../../generated_pages/validation_sum_against_total_dynamic_answers/dynamic-answer-only.page"; +import TotalBlockPage from "../../../../generated_pages/validation_sum_against_total_dynamic_answers/total-block.page"; +import DriverPage from "../../../../generated_pages/validation_sum_against_total_dynamic_answers/any-supermarket.page"; +import SectionSummaryPage from "../../../../generated_pages/validation_sum_against_total_dynamic_answers/dynamic-answers-section-summary.page"; +import ListCollectorRemovePage from "../../../../generated_pages/validation_sum_against_total_dynamic_answers/list-collector-remove.page"; +import ListCollectorEditPage from "../../../../generated_pages/validation_sum_against_total_dynamic_answers/list-collector-edit.page"; +import HubPage from "../../../../base_pages/hub.page"; +import TotalBlockOtherPage from "../../../../generated_pages/validation_sum_against_total_dynamic_answers/total-block-other.page"; +import { click, verifyUrlContains } from "../../../../helpers"; + +describe("Feature: Sum of dynamic answers based on list and optional static answers equal to validation against total ", () => { + const summaryTitles = 'dt[class="ons-summary__item-title"]'; + beforeEach(async () => { + await browser.openQuestionnaire("test_validation_sum_against_total_dynamic_answers.json"); + }); + + describe("Given I add list items with hardcoded total used for validation of dynamic answers", () => { + it("When I continue and enter numbers on dynamic and static answers page that don't add up to that total, Then validation error should be displayed with appropriate message", async () => { + await $(TotalBlockPage.acceptCookies()).click(); + await addTwoSupermarkets(); + await verifyUrlContains(DynamicAnswerPage.pageName); + await expect(await $$(DynamicAnswerPage.labels()).length).toBe(3); + await $$(DynamicAnswerPage.inputs())[0].setValue(33); + await $$(DynamicAnswerPage.inputs())[1].setValue(33); + await $(DynamicAnswerPage.percentageOfShoppingElsewhere()).setValue(33); + await click(DynamicAnswerPage.submit()); + await expect(await $(DynamicAnswerPage.errorNumber(1)).getText()).toBe("Enter answers that add up to 100"); + }); + }); + describe("Given I add list items with hardcoded total used for validation of dynamic answers", () => { + it("When I continue and enter numbers on dynamic and static answers page that add up to that total, Then I should be able to get to the subsequent question", async () => { + await addTwoSupermarkets(); + await verifyUrlContains(DynamicAnswerPage.pageName); + await expect(await $$(DynamicAnswerPage.labels()).length).toBe(3); + await $$(DynamicAnswerPage.inputs())[0].setValue(34); + await $$(DynamicAnswerPage.inputs())[1].setValue(33); + await $(DynamicAnswerPage.percentageOfShoppingElsewhere()).setValue(33); + await click(DynamicAnswerPage.submit()); + await verifyUrlContains(TotalBlockOtherPage.pageName); + }); + }); + describe("Given I add list items with custom total used for validation of dynamic answers", () => { + it("When I continue and enter numbers on dynamic answers only page that don't add up to that total, Then validation error should be displayed with appropriate message", async () => { + await addTwoSupermarkets(); + await verifyUrlContains(DynamicAnswerPage.pageName); + await expect(await $$(DynamicAnswerPage.labels()).length).toBe(3); + await $$(DynamicAnswerPage.inputs())[0].setValue(34); + await $$(DynamicAnswerPage.inputs())[1].setValue(33); + await $(DynamicAnswerPage.percentageOfShoppingElsewhere()).setValue(33); + await click(DynamicAnswerPage.submit()); + await $(TotalBlockOtherPage.totalOther()).setValue(100); + await click(TotalBlockOtherPage.submit()); + await verifyUrlContains(DynamicAnswerOnlyPage.pageName); + await $$(DynamicAnswerOnlyPage.inputs())[0].setValue(50); + await $$(DynamicAnswerOnlyPage.inputs())[1].setValue(0); + await click(DynamicAnswerOnlyPage.submit()); + await expect(await $(DynamicAnswerOnlyPage.errorNumber(1)).getText()).toBe("Enter answers that add up to ÂŖ100.00"); + }); + }); + describe("Given I add list items with custom total used for validation of dynamic answers", () => { + it("When I continue and enter numbers on dynamic answers only page that add up to that total, Then I should be able to get to the summary", async () => { + await addTwoSupermarkets(); + await verifyUrlContains(DynamicAnswerPage.pageName); + await fillDynamicAnswers(); + await verifyUrlContains(SectionSummaryPage.pageName); + }); + }); + describe("Given I add list items and fill all the dynamic answers", () => { + it("When I continue and add another list item, Then I should be revisiting dynamic answers which should be updated to reflect the changes", async () => { + await addTwoSupermarkets(); + await expect(await $$(DynamicAnswerPage.labels()).length).toBe(3); + await fillDynamicAnswers(); + await $(SectionSummaryPage.supermarketsListAddLink()).click(); + await $(ListCollectorAddPage.supermarketName()).setValue("Morrisons"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await verifyUrlContains(DynamicAnswerPage.pageName); + await expect(await $$(DynamicAnswerPage.labels()).length).toBe(4); + }); + }); + describe("Given I add list items and fill all the dynamic answers", () => { + it("When I continue and remove existing list item, Then I should be revisiting dynamic answers which should be updated to reflect the changes", async () => { + await addTwoSupermarkets(); + await fillDynamicAnswers(); + await $(SectionSummaryPage.supermarketsListRemoveLink(1)).click(); + await $(ListCollectorRemovePage.yes()).click(); + await click(ListCollectorRemovePage.submit()); + await verifyUrlContains(DynamicAnswerPage.pageName); + await expect(await $$(DynamicAnswerPage.labels()).length).toBe(2); + }); + }); + describe("Given I add list items and fill all the dynamic answers", () => { + it("When I continue and edit existing list item, Then I should return straight to the summary because the dynamic answers do not depend on the supermarket name", async () => { + await addTwoSupermarkets(); + await fillDynamicAnswers(); + await $(SectionSummaryPage.supermarketsListEditLink(1)).click(); + await $(ListCollectorEditPage.supermarketName()).setValue("Aldi"); + await click(ListCollectorEditPage.submit()); + await verifyUrlContains(SectionSummaryPage.pageName); + await expect(await $(SectionSummaryPage.groupContent(2)).$$(summaryTitles)[0].getText()).toBe("Percentage of shopping at Aldi"); + }); + }); + describe("Given I add list items and fill all the dynamic answers", () => { + it("When I journey backwards, Then I should be revisiting all the previous blocks", async () => { + await addTwoSupermarkets(); + await fillDynamicAnswers(); + await verifyUrlContains(SectionSummaryPage.pageName); + await $(SectionSummaryPage.previous()).click(); + await $(DynamicAnswerOnlyPage.previous()).click(); + await $(TotalBlockOtherPage.previous()).click(); + await $(DynamicAnswerPage.previous()).click(); + await $(ListCollectorPage.previous()).click(); + await verifyUrlContains(DriverPage.pageName); + }); + }); +}); + +async function addTwoSupermarkets() { + await $(TotalBlockPage.total()).setValue(100); + await click(TotalBlockPage.submit()); + await $(HubPage.summaryRowLink("dynamic-answers-section")).click(); + await $(DriverPage.yes()).click(); + await click(DriverPage.submit()); + await $(ListCollectorAddPage.supermarketName()).setValue("Tesco"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.supermarketName()).setValue("Asda"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); +} + +async function fillDynamicAnswers() { + await $$(DynamicAnswerPage.inputs())[0].setValue(34); + await $$(DynamicAnswerPage.inputs())[1].setValue(33); + await $(DynamicAnswerPage.percentageOfShoppingElsewhere()).setValue(33); + await click(DynamicAnswerPage.submit()); + await $(TotalBlockOtherPage.totalOther()).setValue(100); + await click(TotalBlockOtherPage.submit()); + await $$(DynamicAnswerOnlyPage.inputs())[0].setValue(50); + await $$(DynamicAnswerOnlyPage.inputs())[1].setValue(50); + await click(DynamicAnswerOnlyPage.submit()); +} diff --git a/tests/functional/spec/features/validation/sum/equal.spec.js b/tests/functional/spec/features/validation/sum/equal.spec.js index cda66d9cc7..fc3694525f 100644 --- a/tests/functional/spec/features/validation/sum/equal.spec.js +++ b/tests/functional/spec/features/validation/sum/equal.spec.js @@ -1,70 +1,70 @@ import TotalAnswerPage from "../../../../generated_pages/validation_sum_against_total_equal/total-block.page"; import BreakdownAnswerPage from "../../../../generated_pages/validation_sum_against_total_equal/breakdown-block.page"; import SubmitPage from "../../../../generated_pages/validation_sum_against_total_equal/submit.page"; - -const answerAndSubmitBreakdownQuestion = (breakdown1, breakdown2, breakdown3, breakdown4) => { - $(BreakdownAnswerPage.breakdown1()).setValue(breakdown1); - $(BreakdownAnswerPage.breakdown2()).setValue(breakdown2); - $(BreakdownAnswerPage.breakdown3()).setValue(breakdown3); - $(BreakdownAnswerPage.breakdown4()).setValue(breakdown4); - $(BreakdownAnswerPage.submit()).click(); +import { click, verifyUrlContains } from "../../../../helpers"; +const answerAndSubmitBreakdownQuestion = async (breakdown1, breakdown2, breakdown3, breakdown4) => { + await $(BreakdownAnswerPage.breakdown1()).setValue(breakdown1); + await $(BreakdownAnswerPage.breakdown2()).setValue(breakdown2); + await $(BreakdownAnswerPage.breakdown3()).setValue(breakdown3); + await $(BreakdownAnswerPage.breakdown4()).setValue(breakdown4); + await click(BreakdownAnswerPage.submit()); }; describe("Feature: Sum of grouped answers equal to validation against total ", () => { - beforeEach(() => { - browser.openQuestionnaire("test_validation_sum_against_total_equal.json"); + beforeEach(async () => { + await browser.openQuestionnaire("test_validation_sum_against_total_equal.json"); }); describe("Given I start a grouped answer validation survey and enter 12 into the total", () => { - it("When I continue and enter 3 in each breakdown field, Then I should be able to get to the summary", () => { - $(TotalAnswerPage.total()).setValue("12"); - $(TotalAnswerPage.submit()).click(); + it("When I continue and enter 3 in each breakdown field, Then I should be able to get to the summary", async () => { + await $(TotalAnswerPage.total()).setValue("12"); + await click(TotalAnswerPage.submit()); - answerAndSubmitBreakdownQuestion("3", "3", "3", "3"); + await answerAndSubmitBreakdownQuestion("3", "3", "3", "3"); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); + await verifyUrlContains(SubmitPage.pageName); }); }); describe("Given I completed a grouped answer validation question and I am on the summary", () => { - it("When I go back from the summary and change the total, Then I must reconfirm the breakdown question with valid answers before I can get to the summary", () => { - $(TotalAnswerPage.total()).setValue("12"); - $(TotalAnswerPage.submit()).click(); - answerAndSubmitBreakdownQuestion("3", "3", "3", "3"); + it("When I go back from the summary and change the total, Then I must reconfirm the breakdown question with valid answers before I can get to the summary", async () => { + await $(TotalAnswerPage.total()).setValue("12"); + await click(TotalAnswerPage.submit()); + await answerAndSubmitBreakdownQuestion("3", "3", "3", "3"); - $(SubmitPage.totalAnswerEdit()).click(); - $(TotalAnswerPage.total()).setValue("15"); - $(TotalAnswerPage.submit()).click(); + await $(SubmitPage.totalAnswerEdit()).click(); + await $(TotalAnswerPage.total()).setValue("15"); + await click(TotalAnswerPage.submit()); - browser.url(SubmitPage.url()); - expect(browser.getUrl()).to.contain(BreakdownAnswerPage.pageName); + await browser.url(SubmitPage.url()); + await verifyUrlContains(BreakdownAnswerPage.pageName); - $(BreakdownAnswerPage.submit()).click(); - expect($(BreakdownAnswerPage.errorNumber(1)).getText()).to.contain("Enter answers that add up to 15"); + await click(BreakdownAnswerPage.submit()); + await expect(await $(BreakdownAnswerPage.errorNumber(1)).getText()).toBe("Enter answers that add up to 15"); - answerAndSubmitBreakdownQuestion("6", "3", "3", "3"); + await answerAndSubmitBreakdownQuestion("6", "3", "3", "3"); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); + await verifyUrlContains(SubmitPage.pageName); }); }); describe("Given I start a grouped answer validation survey and enter 5 into the total", () => { - it("When I continue and enter 5 into breakdown 1 and leave the others empty, Then I should be able to get to the summary", () => { - $(TotalAnswerPage.total()).setValue("5"); - $(TotalAnswerPage.submit()).click(); - answerAndSubmitBreakdownQuestion("5", "", "", ""); + it("When I continue and enter 5 into breakdown 1 and leave the others empty, Then I should be able to get to the summary", async () => { + await $(TotalAnswerPage.total()).setValue("5"); + await click(TotalAnswerPage.submit()); + await answerAndSubmitBreakdownQuestion("5", "", "", ""); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); + await verifyUrlContains(SubmitPage.pageName); }); }); describe("Given I start a grouped answer validation survey and enter 5 into the total", () => { - it("When I continue and enter 3 in each breakdown field, Then I should see a validation error", () => { - $(TotalAnswerPage.total()).setValue("5"); - $(TotalAnswerPage.submit()).click(); - answerAndSubmitBreakdownQuestion("3", "3", "3", "3"); + it("When I continue and enter 3 in each breakdown field, Then I should see a validation error", async () => { + await $(TotalAnswerPage.total()).setValue("5"); + await click(TotalAnswerPage.submit()); + await answerAndSubmitBreakdownQuestion("3", "3", "3", "3"); - expect($(BreakdownAnswerPage.errorNumber(1)).getText()).to.contain("Enter answers that add up to 5"); + await expect(await $(BreakdownAnswerPage.errorNumber(1)).getText()).toBe("Enter answers that add up to 5"); }); }); }); diff --git a/tests/functional/spec/features/validation/sum/equal_multiple.spec.js b/tests/functional/spec/features/validation/sum/equal_multiple.spec.js index aafc0ad04c..185358053f 100644 --- a/tests/functional/spec/features/validation/sum/equal_multiple.spec.js +++ b/tests/functional/spec/features/validation/sum/equal_multiple.spec.js @@ -1,51 +1,51 @@ import TotalAnswerPage from "../../../../generated_pages/validation_sum_against_total_multiple/total-block.page"; import BreakdownAnswerPage from "../../../../generated_pages/validation_sum_against_total_multiple/breakdown-block.page"; import SubmitPage from "../../../../generated_pages/validation_sum_against_total_multiple/submit.page"; - +import { click, verifyUrlContains } from "../../../../helpers"; describe("Feature: Sum validation (Multi Rule Equals)", () => { - beforeEach(() => { - browser.openQuestionnaire("test_validation_sum_against_total_multiple.json"); + beforeEach(async () => { + await browser.openQuestionnaire("test_validation_sum_against_total_multiple.json"); }); describe("Given I start a grouped answer with multi rule validation survey and enter 10 into the total", () => { - it("When I continue and enter nothing, all zeros or 10 at breakdown level, Then I should be able to get to the summary", () => { - $(TotalAnswerPage.total()).setValue("10"); - $(TotalAnswerPage.submit()).click(); - $(BreakdownAnswerPage.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); + it("When I continue and enter nothing, all zeros or 10 at breakdown level, Then I should be able to get to the summary", async () => { + await $(TotalAnswerPage.total()).setValue("10"); + await click(TotalAnswerPage.submit()); + await click(BreakdownAnswerPage.submit()); + await verifyUrlContains(SubmitPage.pageName); - $(SubmitPage.previous()).click(); - $(BreakdownAnswerPage.breakdown1()).setValue("0"); - $(BreakdownAnswerPage.breakdown2()).setValue("0"); - $(BreakdownAnswerPage.breakdown3()).setValue("0"); - $(BreakdownAnswerPage.breakdown4()).setValue("0"); - $(BreakdownAnswerPage.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); + await $(SubmitPage.previous()).click(); + await $(BreakdownAnswerPage.breakdown1()).setValue("0"); + await $(BreakdownAnswerPage.breakdown2()).setValue("0"); + await $(BreakdownAnswerPage.breakdown3()).setValue("0"); + await $(BreakdownAnswerPage.breakdown4()).setValue("0"); + await click(BreakdownAnswerPage.submit()); + await verifyUrlContains(SubmitPage.pageName); - $(SubmitPage.previous()).click(); - $(BreakdownAnswerPage.breakdown1()).setValue("1"); - $(BreakdownAnswerPage.breakdown2()).setValue("2"); - $(BreakdownAnswerPage.breakdown3()).setValue("3"); - $(BreakdownAnswerPage.breakdown4()).setValue("4"); - $(BreakdownAnswerPage.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); + await $(SubmitPage.previous()).click(); + await $(BreakdownAnswerPage.breakdown1()).setValue("1"); + await $(BreakdownAnswerPage.breakdown2()).setValue("2"); + await $(BreakdownAnswerPage.breakdown3()).setValue("3"); + await $(BreakdownAnswerPage.breakdown4()).setValue("4"); + await click(BreakdownAnswerPage.submit()); + await verifyUrlContains(SubmitPage.pageName); }); }); describe("Given I start a grouped answer with multi rule validation survey and enter 10 into the total", () => { - it("When I continue and enter less between 1 - 9 or greater than 10, Then it should error", () => { - $(TotalAnswerPage.total()).setValue("10"); - $(TotalAnswerPage.submit()).click(); - $(BreakdownAnswerPage.breakdown1()).setValue("1"); - $(BreakdownAnswerPage.submit()).click(); + it("When I continue and enter less between 1 - 9 or greater than 10, Then it should error", async () => { + await $(TotalAnswerPage.total()).setValue("10"); + await click(TotalAnswerPage.submit()); + await $(BreakdownAnswerPage.breakdown1()).setValue("1"); + await click(BreakdownAnswerPage.submit()); - expect($(BreakdownAnswerPage.errorNumber(1)).getText()).to.contain("Enter answers that add up to 10"); + await expect(await $(BreakdownAnswerPage.errorNumber(1)).getText()).toBe("Enter answers that add up to 10"); - $(BreakdownAnswerPage.breakdown2()).setValue("2"); - $(BreakdownAnswerPage.breakdown3()).setValue("3"); - $(BreakdownAnswerPage.breakdown4()).setValue("5"); - $(BreakdownAnswerPage.submit()).click(); - expect($(BreakdownAnswerPage.errorNumber(1)).getText()).to.contain("Enter answers that add up to 10"); + await $(BreakdownAnswerPage.breakdown2()).setValue("2"); + await $(BreakdownAnswerPage.breakdown3()).setValue("3"); + await $(BreakdownAnswerPage.breakdown4()).setValue("5"); + await click(BreakdownAnswerPage.submit()); + await expect(await $(BreakdownAnswerPage.errorNumber(1)).getText()).toBe("Enter answers that add up to 10"); }); }); }); diff --git a/tests/functional/spec/features/validation/sum/equal_or_less_than.spec.js b/tests/functional/spec/features/validation/sum/equal_or_less_than.spec.js index 331718baa0..e8f774150e 100644 --- a/tests/functional/spec/features/validation/sum/equal_or_less_than.spec.js +++ b/tests/functional/spec/features/validation/sum/equal_or_less_than.spec.js @@ -1,74 +1,74 @@ import TotalAnswerPage from "../../../../generated_pages/validation_sum_against_total_equal_or_less_than/total-block.page"; import BreakdownAnswerPage from "../../../../generated_pages/validation_sum_against_total_equal_or_less_than/breakdown-block.page"; import SubmitPage from "../../../../generated_pages/validation_sum_against_total_equal_or_less_than/submit.page"; - +import { click, verifyUrlContains } from "../../../../helpers"; describe("Feature: Sum of grouped answers validation (equal or less than) against total", () => { - beforeEach(() => { - browser.openQuestionnaire("test_validation_sum_against_total_equal_or_less_than.json"); + beforeEach(async () => { + await browser.openQuestionnaire("test_validation_sum_against_total_equal_or_less_than.json"); }); describe("Given I start a grouped answer validation survey and enter 12 into the total", () => { - it("When I continue and enter 2 in each breakdown field, Then I should be able to get to the summary", () => { - $(TotalAnswerPage.total()).setValue("12"); - $(TotalAnswerPage.submit()).click(); - $(BreakdownAnswerPage.breakdown1()).setValue("2"); - $(BreakdownAnswerPage.breakdown2()).setValue("2"); - $(BreakdownAnswerPage.breakdown3()).setValue("2"); - $(BreakdownAnswerPage.breakdown4()).setValue("2"); - $(BreakdownAnswerPage.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); + it("When I continue and enter 2 in each breakdown field, Then I should be able to get to the summary", async () => { + await $(TotalAnswerPage.total()).setValue("12"); + await click(TotalAnswerPage.submit()); + await $(BreakdownAnswerPage.breakdown1()).setValue("2"); + await $(BreakdownAnswerPage.breakdown2()).setValue("2"); + await $(BreakdownAnswerPage.breakdown3()).setValue("2"); + await $(BreakdownAnswerPage.breakdown4()).setValue("2"); + await click(BreakdownAnswerPage.submit()); + await verifyUrlContains(SubmitPage.pageName); }); }); describe("Given I start a grouped answer validation survey and enter 12 into the total", () => { - it("When I continue and enter 3 in each breakdown field, Then I should be able to get to the summary", () => { - $(TotalAnswerPage.total()).setValue("12"); - $(TotalAnswerPage.submit()).click(); - $(BreakdownAnswerPage.breakdown1()).setValue("3"); - $(BreakdownAnswerPage.breakdown2()).setValue("3"); - $(BreakdownAnswerPage.breakdown3()).setValue("3"); - $(BreakdownAnswerPage.breakdown4()).setValue("3"); - $(BreakdownAnswerPage.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); + it("When I continue and enter 3 in each breakdown field, Then I should be able to get to the summary", async () => { + await $(TotalAnswerPage.total()).setValue("12"); + await click(TotalAnswerPage.submit()); + await $(BreakdownAnswerPage.breakdown1()).setValue("3"); + await $(BreakdownAnswerPage.breakdown2()).setValue("3"); + await $(BreakdownAnswerPage.breakdown3()).setValue("3"); + await $(BreakdownAnswerPage.breakdown4()).setValue("3"); + await click(BreakdownAnswerPage.submit()); + await verifyUrlContains(SubmitPage.pageName); }); }); describe("Given I start a grouped answer validation survey and enter 5 into the total", () => { - it("When I continue and enter 4 into breakdown 1 and leave the others empty, Then I should be able to get to the summary", () => { - $(TotalAnswerPage.total()).setValue("5"); - $(TotalAnswerPage.submit()).click(); - $(BreakdownAnswerPage.breakdown1()).setValue("4"); - $(BreakdownAnswerPage.breakdown2()).setValue(""); - $(BreakdownAnswerPage.breakdown3()).setValue(""); - $(BreakdownAnswerPage.breakdown4()).setValue(""); - $(BreakdownAnswerPage.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); + it("When I continue and enter 4 into breakdown 1 and leave the others empty, Then I should be able to get to the summary", async () => { + await $(TotalAnswerPage.total()).setValue("5"); + await click(TotalAnswerPage.submit()); + await $(BreakdownAnswerPage.breakdown1()).setValue("4"); + await $(BreakdownAnswerPage.breakdown2()).setValue(""); + await $(BreakdownAnswerPage.breakdown3()).setValue(""); + await $(BreakdownAnswerPage.breakdown4()).setValue(""); + await click(BreakdownAnswerPage.submit()); + await verifyUrlContains(SubmitPage.pageName); }); }); describe("Given I start a grouped answer validation survey and enter 12 into the total", () => { - it("When I continue and enter 4 in each breakdown field, Then I should see a validation error", () => { - $(TotalAnswerPage.total()).setValue("12"); - $(TotalAnswerPage.submit()).click(); - $(BreakdownAnswerPage.breakdown1()).setValue("4"); - $(BreakdownAnswerPage.breakdown2()).setValue("4"); - $(BreakdownAnswerPage.breakdown3()).setValue("4"); - $(BreakdownAnswerPage.breakdown4()).setValue("4"); - $(BreakdownAnswerPage.submit()).click(); - expect($(BreakdownAnswerPage.errorNumber(1)).getText()).to.contain("Enter answers that add up to or are less than 12"); + it("When I continue and enter 4 in each breakdown field, Then I should see a validation error", async () => { + await $(TotalAnswerPage.total()).setValue("12"); + await click(TotalAnswerPage.submit()); + await $(BreakdownAnswerPage.breakdown1()).setValue("4"); + await $(BreakdownAnswerPage.breakdown2()).setValue("4"); + await $(BreakdownAnswerPage.breakdown3()).setValue("4"); + await $(BreakdownAnswerPage.breakdown4()).setValue("4"); + await click(BreakdownAnswerPage.submit()); + await expect(await $(BreakdownAnswerPage.errorNumber(1)).getText()).toBe("Enter answers that add up to or are less than 12"); }); }); describe("Given I start a grouped answer validation survey and enter 5 into the total", () => { - it("When I continue and enter 3 in each breakdown field, Then I should see a validation error", () => { - $(TotalAnswerPage.total()).setValue("5"); - $(TotalAnswerPage.submit()).click(); - $(BreakdownAnswerPage.breakdown1()).setValue("3"); - $(BreakdownAnswerPage.breakdown2()).setValue("3"); - $(BreakdownAnswerPage.breakdown3()).setValue("3"); - $(BreakdownAnswerPage.breakdown4()).setValue("3"); - $(BreakdownAnswerPage.submit()).click(); - expect($(BreakdownAnswerPage.errorNumber(1)).getText()).to.contain("Enter answers that add up to or are less than 5"); + it("When I continue and enter 3 in each breakdown field, Then I should see a validation error", async () => { + await $(TotalAnswerPage.total()).setValue("5"); + await click(TotalAnswerPage.submit()); + await $(BreakdownAnswerPage.breakdown1()).setValue("3"); + await $(BreakdownAnswerPage.breakdown2()).setValue("3"); + await $(BreakdownAnswerPage.breakdown3()).setValue("3"); + await $(BreakdownAnswerPage.breakdown4()).setValue("3"); + await click(BreakdownAnswerPage.submit()); + await expect(await $(BreakdownAnswerPage.errorNumber(1)).getText()).toBe("Enter answers that add up to or are less than 5"); }); }); }); diff --git a/tests/functional/spec/features/validation/sum/equal_total_in_separate_section_hub.spec.js b/tests/functional/spec/features/validation/sum/equal_total_in_separate_section_hub.spec.js index 903f567557..d9ca116e3b 100644 --- a/tests/functional/spec/features/validation/sum/equal_total_in_separate_section_hub.spec.js +++ b/tests/functional/spec/features/validation/sum/equal_total_in_separate_section_hub.spec.js @@ -8,119 +8,119 @@ import BreakdownSectionSummary from "../../../../generated_pages/validation_sum_ import HubPage from "../../../../base_pages/hub.page"; import ThankYouPage from "../../../../base_pages/thank-you.page"; - +import { click, verifyUrlContains } from "../../../../helpers"; const companyOverviewSectionID = "company-overview-section"; const breakdownSectionId = "breakdown-section"; -const answerAndSubmitTurnoverBreakdownQuestion = (breakdown1, breakdown2, breakdown3) => { - $(TurnoverBreakdownPage.turnoverBreakdown1()).setValue(breakdown1); - $(TurnoverBreakdownPage.turnoverBreakdown2()).setValue(breakdown2); - $(TurnoverBreakdownPage.turnoverBreakdown3()).setValue(breakdown3); - $(TurnoverBreakdownPage.submit()).click(); +const answerAndSubmitTurnoverBreakdownQuestion = async (breakdown1, breakdown2, breakdown3) => { + await $(TurnoverBreakdownPage.turnoverBreakdown1()).setValue(breakdown1); + await $(TurnoverBreakdownPage.turnoverBreakdown2()).setValue(breakdown2); + await $(TurnoverBreakdownPage.turnoverBreakdown3()).setValue(breakdown3); + await click(TurnoverBreakdownPage.submit()); }; -const answerAndSubmitEmployeeBreakdownQuestion = (breakdown1, breakdown2) => { - $(EmployeesBreakdownPage.employeesBreakdown1()).setValue(breakdown1); - $(EmployeesBreakdownPage.employeesBreakdown2()).setValue(breakdown2); - $(EmployeesBreakdownPage.submit()).click(); +const answerAndSubmitEmployeeBreakdownQuestion = async (breakdown1, breakdown2) => { + await $(EmployeesBreakdownPage.employeesBreakdown1()).setValue(breakdown1); + await $(EmployeesBreakdownPage.employeesBreakdown2()).setValue(breakdown2); + await click(EmployeesBreakdownPage.submit()); }; -const answerAndSubmitTotalTurnoverQuestion = (total) => { - $(TotalTurnoverPage.totalTurnover()).setValue(total); - $(TotalTurnoverPage.submit()).click(); +const answerAndSubmitTotalTurnoverQuestion = async (total) => { + await $(TotalTurnoverPage.totalTurnover()).setValue(total); + await click(TotalTurnoverPage.submit()); }; -const answerAndSubmitTotalEmployeesQuestion = (total) => { - $(TotalEmployeesPage.totalEmployees()).setValue(total); - $(TotalEmployeesPage.submit()).click(); +const answerAndSubmitTotalEmployeesQuestion = async (total) => { + await $(TotalEmployeesPage.totalEmployees()).setValue(total); + await click(TotalEmployeesPage.submit()); }; describe("Feature: Validation - Sum of grouped answers to equal total (Total in separate section)", () => { describe("Given I start a grouped answer validation with dependent sections and complete the total turnover and total employees questions", () => { - before(() => { - browser.openQuestionnaire("test_validation_sum_against_total_hub_with_dependent_section.json"); - answerAndSubmitTotalTurnoverQuestion(1000); - answerAndSubmitTotalEmployeesQuestion(10); - $(CompanySectionSummary.submit()).click(); + before(async () => { + await browser.openQuestionnaire("test_validation_sum_against_total_hub_with_dependent_section.json"); + await answerAndSubmitTotalTurnoverQuestion(1000); + await answerAndSubmitTotalEmployeesQuestion(10); + await click(CompanySectionSummary.submit()); - expect($(HubPage.summaryRowState(companyOverviewSectionID)).getText()).to.equal("Completed"); + await expect(await $(HubPage.summaryRowState(companyOverviewSectionID)).getText()).toBe("Completed"); }); - it("When I am on the hub, Then the breakdown section should be marked as 'Not Started'", () => { - expect($(HubPage.summaryRowState(breakdownSectionId)).getText()).to.equal("Not started"); + it("When I am on the hub, Then the breakdown section should be marked as 'Not Started'", async () => { + await expect(await $(HubPage.summaryRowState(breakdownSectionId)).getText()).toBe("Not started"); }); - it("When I start the breakdown section and enter an answer that is not equal to the total for the turnover question, Then I should see a validation error", () => { - $(HubPage.submit()).click(); - answerAndSubmitTurnoverBreakdownQuestion(1000, 250, 250); + it("When I start the breakdown section and enter an answer that is not equal to the total for the turnover question, Then I should see a validation error", async () => { + await click(HubPage.submit()); + await answerAndSubmitTurnoverBreakdownQuestion(1000, 250, 250); - expect($(TurnoverBreakdownPage.errorNumber(1)).getText()).to.contain("Enter answers that add up to ÂŖ1,000.00"); + await expect(await $(TurnoverBreakdownPage.errorNumber(1)).getText()).toBe("Enter answers that add up to ÂŖ1,000.00"); }); - it("When I start the breakdown section and enter answers that are equal the total, Then I should be able to get to the section summary and the breakdown section should be marked as 'Completed'", () => { - answerAndSubmitTurnoverBreakdownQuestion(500, 250, 250); - answerAndSubmitEmployeeBreakdownQuestion(5, 5); + it("When I start the breakdown section and enter answers that are equal the total, Then I should be able to get to the section summary and the breakdown section should be marked as 'Completed'", async () => { + await answerAndSubmitTurnoverBreakdownQuestion(500, 250, 250); + await answerAndSubmitEmployeeBreakdownQuestion(5, 5); - expect(browser.getUrl()).to.contain(BreakdownSectionSummary.pageName); - $(BreakdownSectionSummary.submit()).click(); + await verifyUrlContains(BreakdownSectionSummary.pageName); + await click(BreakdownSectionSummary.submit()); - expect($(HubPage.summaryRowState(breakdownSectionId)).getText()).to.equal("Completed"); + await expect(await $(HubPage.summaryRowState(breakdownSectionId)).getText()).toBe("Completed"); }); }); describe("Given I start a grouped answer validation with dependent sections and complete the overview and breakdown sections", () => { - before(() => { - browser.openQuestionnaire("test_validation_sum_against_total_hub_with_dependent_section.json"); + before(async () => { + await browser.openQuestionnaire("test_validation_sum_against_total_hub_with_dependent_section.json"); // Complete overview section - answerAndSubmitTotalTurnoverQuestion(1000); - answerAndSubmitTotalEmployeesQuestion(10); - $(CompanySectionSummary.submit()).click(); + await answerAndSubmitTotalTurnoverQuestion(1000); + await answerAndSubmitTotalEmployeesQuestion(10); + await click(CompanySectionSummary.submit()); // Complete breakdown section - $(HubPage.submit()).click(); - answerAndSubmitTurnoverBreakdownQuestion(500, 250, 250); - answerAndSubmitEmployeeBreakdownQuestion(5, 5); - $(BreakdownSectionSummary.submit()).click(); + await click(HubPage.submit()); + await answerAndSubmitTurnoverBreakdownQuestion(500, 250, 250); + await answerAndSubmitEmployeeBreakdownQuestion(5, 5); + await click(BreakdownSectionSummary.submit()); - expect($(HubPage.summaryRowState(breakdownSectionId)).getText()).to.equal("Completed"); + await expect(await $(HubPage.summaryRowState(breakdownSectionId)).getText()).toBe("Completed"); }); - it("When I change my answer for the total turnover question, Then the breakdown section should be marked as 'Partially completed'", () => { - $(HubPage.summaryRowLink(companyOverviewSectionID)).click(); - $(CompanySectionSummary.totalTurnoverAnswerEdit()).click(); + it("When I change my answer for the total turnover question, Then the breakdown section should be marked as 'Partially completed'", async () => { + await $(HubPage.summaryRowLink(companyOverviewSectionID)).click(); + await $(CompanySectionSummary.totalTurnoverAnswerEdit()).click(); - answerAndSubmitTotalTurnoverQuestion(1500); - $(CompanySectionSummary.submit()).click(); - expect($(HubPage.summaryRowState(breakdownSectionId)).getText()).to.equal("Partially completed"); + await answerAndSubmitTotalTurnoverQuestion(1500); + await click(CompanySectionSummary.submit()); + await expect(await $(HubPage.summaryRowState(breakdownSectionId)).getText()).toBe("Partially completed"); }); - it("When I click 'Continue with section' on the breakdown section, Then I should be taken to the turnover breakdown question and my previous answers should be prefilled", () => { - $(HubPage.summaryRowLink(breakdownSectionId)).click(); + it("When I click 'Continue with section' on the breakdown section, Then I should be taken to the turnover breakdown question and my previous answers should be prefilled", async () => { + await $(HubPage.summaryRowLink(breakdownSectionId)).click(); - expect($(TurnoverBreakdownPage.turnoverBreakdown1()).getValue()).to.equal("500.00"); - expect($(TurnoverBreakdownPage.turnoverBreakdown2()).getValue()).to.equal("250.00"); - expect($(TurnoverBreakdownPage.turnoverBreakdown3()).getValue()).to.equal("250.00"); + await expect(await $(TurnoverBreakdownPage.turnoverBreakdown1()).getValue()).toBe("500.00"); + await expect(await $(TurnoverBreakdownPage.turnoverBreakdown2()).getValue()).toBe("250.00"); + await expect(await $(TurnoverBreakdownPage.turnoverBreakdown3()).getValue()).toBe("250.00"); }); - it("When I submit the turnover breakdown question with no changes, Then I should see a validation error", () => { - $(TurnoverBreakdownPage.submit()).click(); + it("When I submit the turnover breakdown question with no changes, Then I should see a validation error", async () => { + await click(TurnoverBreakdownPage.submit()); - expect($(TurnoverBreakdownPage.errorNumber(1)).getText()).to.contain("Enter answers that add up to ÂŖ1,500.00"); + await expect(await $(TurnoverBreakdownPage.errorNumber(1)).getText()).toBe("Enter answers that add up to ÂŖ1,500.00"); }); - it("When I update my answers to equal the new total turnover, Then I should be able to get to the section summary and the breakdown section should be marked as 'Completed'", () => { - answerAndSubmitTurnoverBreakdownQuestion(500, 500, 500); + it("When I update my answers to equal the new total turnover, Then I should be able to get to the section summary and the breakdown section should be marked as 'Completed'", async () => { + await answerAndSubmitTurnoverBreakdownQuestion(500, 500, 500); - expect(browser.getUrl()).to.contain(BreakdownSectionSummary.pageName); - $(BreakdownSectionSummary.submit()).click(); - expect($(HubPage.summaryRowState(breakdownSectionId)).getText()).to.equal("Completed"); + await verifyUrlContains(BreakdownSectionSummary.pageName); + await click(BreakdownSectionSummary.submit()); + await expect(await $(HubPage.summaryRowState(breakdownSectionId)).getText()).toBe("Completed"); }); - it("When I submit the questionnaire, Then I should see the thank you page", () => { - $(HubPage.submit()).click(); - - expect(browser.getUrl()).to.contain(ThankYouPage.pageName); + it("When I submit the questionnaire, Then I should see the thank you page", async () => { + await $(HubPage.submit()).scrollIntoView(); + await click(HubPage.submit()); + await verifyUrlContains(ThankYouPage.pageName); }); }); }); diff --git a/tests/functional/spec/features/validation/sum/equal_total_in_separate_section_repeating.spec.js b/tests/functional/spec/features/validation/sum/equal_total_in_separate_section_repeating.spec.js index 03324c4532..24e868d9f1 100644 --- a/tests/functional/spec/features/validation/sum/equal_total_in_separate_section_repeating.spec.js +++ b/tests/functional/spec/features/validation/sum/equal_total_in_separate_section_repeating.spec.js @@ -3,181 +3,199 @@ import ListCollectorAddPage from "../../../../generated_pages/validation_sum_aga import ListCollectorSummaryPage from "../../../../generated_pages/validation_sum_against_total_repeating_with_dependent_section/householders-section-summary.page"; import TotalSpendingPage from "../../../../generated_pages/validation_sum_against_total_repeating_with_dependent_section/total-spending-block.page"; +import EntertainmentSpendingPage from "../../../../generated_pages/validation_sum_against_total_repeating_with_dependent_section/entertainment-spending-block.page"; import HouseholdOverviewSectionSummary from "../../../../generated_pages/validation_sum_against_total_repeating_with_dependent_section/household-overview-section-summary.page"; import BreakdownDrivingPage from "../../../../generated_pages/validation_sum_against_total_repeating_with_dependent_section/breakdown-driving-block.page"; import SpendingBreakdownPage from "../../../../generated_pages/validation_sum_against_total_repeating_with_dependent_section/spending-breakdown-block.page"; +import EntertainmentBreakdownPage from "../../../../generated_pages/validation_sum_against_total_repeating_with_dependent_section/second-spending-breakdown-block.page"; import BreakdownSectionSummary from "../../../../generated_pages/validation_sum_against_total_repeating_with_dependent_section/breakdown-section-summary.page"; import HubPage from "../../../../base_pages/hub.page"; import ThankYouPage from "../../../../base_pages/thank-you.page"; - +import { click, verifyUrlContains } from "../../../../helpers"; const householderSectionId = "householders-section"; const householdOverviewSectionId = "household-overview-section"; const repeatingSectionId = (repeatIndex) => { return `breakdown-section-${repeatIndex}`; }; -const addPersonToHousehold = (firstName, lastName) => { - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue(firstName); - $(ListCollectorAddPage.lastName()).setValue(lastName); - $(ListCollectorAddPage.submit()).click(); +const addPersonToHousehold = async (firstName, lastName) => { + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue(firstName); + await $(ListCollectorAddPage.lastName()).setValue(lastName); + await click(ListCollectorAddPage.submit()); +}; + +const answerAndSubmitTotalSpendingQuestion = async (total) => { + await $(TotalSpendingPage.totalSpending()).setValue(total); + await click(TotalSpendingPage.submit()); +}; + +const answerAndSubmitEntertainmentSpendingQuestion = async (total) => { + await $(EntertainmentSpendingPage.entertainmentSpending()).setValue(total); + await click(EntertainmentSpendingPage.submit()); }; -const answerAndSubmitTotalSpendingQuestion = (total) => { - $(TotalSpendingPage.totalSpending()).setValue(total); - $(TotalSpendingPage.submit()).click(); +const answerAndSubmitSpendingBreakdownQuestion = async (breakdown1, breakdown2, breakdown3) => { + await $(SpendingBreakdownPage.spendingBreakdown1()).setValue(breakdown1); + await $(SpendingBreakdownPage.spendingBreakdown2()).setValue(breakdown2); + await $(SpendingBreakdownPage.spendingBreakdown3()).setValue(breakdown3); + await click(SpendingBreakdownPage.submit()); }; -const answerAndSubmitSpendingBreakdownQuestion = (breakdown1, breakdown2, breakdown3) => { - $(SpendingBreakdownPage.spendingBreakdown1()).setValue(breakdown1); - $(SpendingBreakdownPage.spendingBreakdown2()).setValue(breakdown2); - $(SpendingBreakdownPage.spendingBreakdown3()).setValue(breakdown3); - $(SpendingBreakdownPage.submit()).click(); +const assertSpendingBreakdownAnswer = async (breakdown1, breakdown2, breakdown3) => { + await expect(await $(SpendingBreakdownPage.spendingBreakdown1()).getValue()).toBe(breakdown1); + await expect(await $(SpendingBreakdownPage.spendingBreakdown2()).getValue()).toBe(breakdown2); + await expect(await $(SpendingBreakdownPage.spendingBreakdown3()).getValue()).toBe(breakdown3); }; -const assertSpendingBreakdownAnswer = (breakdown1, breakdown2, breakdown3) => { - expect($(SpendingBreakdownPage.spendingBreakdown1()).getValue()).to.equal(breakdown1); - expect($(SpendingBreakdownPage.spendingBreakdown2()).getValue()).to.equal(breakdown2); - expect($(SpendingBreakdownPage.spendingBreakdown3()).getValue()).to.equal(breakdown3); +const answerAndSubmitEntertainmentBreakdownQuestion = async (breakdown1, breakdown2, breakdown3) => { + await $(EntertainmentBreakdownPage.secondSpendingBreakdown1()).setValue(breakdown1); + await $(EntertainmentBreakdownPage.secondSpendingBreakdown2()).setValue(breakdown2); + await $(EntertainmentBreakdownPage.secondSpendingBreakdown3()).setValue(breakdown3); + await click(EntertainmentBreakdownPage.submit()); }; const assertRepeatingSectionOnChange = (repeatIndex, currentBreakdown1, currentBreakdown2, currentBreakdown3, newTotal) => { - it(`When I click 'Continue with section' on repeating section ${repeatIndex}, Then I should be taken to the spending breakdown question and my previous answers should be prefilled`, () => { - $(HubPage.summaryRowLink(repeatingSectionId(repeatIndex))).click(); + it(`When I click 'Continue with section' on repeating section ${repeatIndex}, Then I should be taken to the spending breakdown question and my previous answers should be prefilled`, async () => { + await $(HubPage.summaryRowLink(repeatingSectionId(repeatIndex))).click(); - assertSpendingBreakdownAnswer(currentBreakdown1, currentBreakdown2, currentBreakdown3); + await assertSpendingBreakdownAnswer(currentBreakdown1, currentBreakdown2, currentBreakdown3); }); - it("When I submit the spending breakdown question with no changes, Then I should see a validation error", () => { - $(SpendingBreakdownPage.submit()).click(); + it("When I submit the spending breakdown question with no changes, Then I should see a validation error", async () => { + await click(SpendingBreakdownPage.submit()); - expect($(SpendingBreakdownPage.errorNumber(1)).getText()).to.contain(`Enter answers that add up to ÂŖ${newTotal}`); + await expect(await $(SpendingBreakdownPage.errorNumber(1)).getText()).toBe(`Enter answers that add up to ÂŖ${newTotal}`); }); - it("When I update my answers to equal the new total spending, Then I should be able to get to the section summary and the breakdown section should be marked as 'Completed'", () => { - answerAndSubmitSpendingBreakdownQuestion(newTotal, 0, 0); + it("When I update my answers to equal the new total spending, Then I should be able to get to the section summary and the breakdown section should be marked as 'Completed'", async () => { + await answerAndSubmitSpendingBreakdownQuestion(newTotal, 0, 0); - expect(browser.getUrl()).to.contain(BreakdownSectionSummary.pageName); - $(BreakdownSectionSummary.submit()).click(); - expect($(HubPage.summaryRowState(repeatingSectionId(repeatIndex))).getText()).to.equal("Completed"); + await verifyUrlContains(BreakdownSectionSummary.pageName); + await click(BreakdownSectionSummary.submit()); + await expect(await $(HubPage.summaryRowState(repeatingSectionId(repeatIndex))).getText()).toBe("Completed"); }); }; describe("Feature: Validation - Sum of grouped answers to equal total (Repeating section) (Total in separate section)", () => { describe("Given I start a repeating grouped answer validation with dependent sections and add 2 householders and complete the household overview section", () => { - before(() => { - browser.openQuestionnaire("test_validation_sum_against_total_repeating_with_dependent_section.json"); + before(async () => { + await browser.openQuestionnaire("test_validation_sum_against_total_repeating_with_dependent_section.json"); // Add 2 householders - addPersonToHousehold("John", "Doe"); - addPersonToHousehold("Jane", "Doe"); - $(ListCollectorPage.no()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorSummaryPage.submit()).click(); + await addPersonToHousehold("John", "Doe"); + await addPersonToHousehold("Jane", "Doe"); + await $(ListCollectorPage.no()).click(); + await $(ListCollectorPage.submit()).scrollIntoView(); + await click(ListCollectorPage.submit()); + await click(ListCollectorSummaryPage.submit()); // Complete household overview section - answerAndSubmitTotalSpendingQuestion(1000); - $(HouseholdOverviewSectionSummary.submit()).click(); + await answerAndSubmitTotalSpendingQuestion(1000); + await answerAndSubmitEntertainmentSpendingQuestion(500); + await click(HouseholdOverviewSectionSummary.submit()); - expect($(HubPage.summaryRowState(householderSectionId)).getText()).to.equal("Completed"); - expect($(HubPage.summaryRowState(householdOverviewSectionId)).getText()).to.equal("Completed"); + await expect(await $(HubPage.summaryRowState(householderSectionId)).getText()).toBe("Completed"); + await expect(await $(HubPage.summaryRowState(householdOverviewSectionId)).getText()).toBe("Completed"); }); - it("When I am on the hub, Then the two repeating breakdown sections should be marked as 'Not Started'", () => { - expect($(HubPage.summaryRowState(repeatingSectionId(1))).getText()).to.equal("Not started"); - expect($(HubPage.summaryRowState(repeatingSectionId(2))).getText()).to.equal("Not started"); + it("When I am on the hub, Then the two repeating breakdown sections should be marked as 'Not Started'", async () => { + await expect(await $(HubPage.summaryRowState(repeatingSectionId(1))).getText()).toBe("Not started"); + await expect(await $(HubPage.summaryRowState(repeatingSectionId(2))).getText()).toBe("Not started"); }); - it("When I start a repeating section and don't skip the calculated question, and enter an answer that is not equal to the total for the spending question, Then I should see a validation error", () => { - $(HubPage.summaryRowLink(repeatingSectionId(1))).click(); - $(BreakdownDrivingPage.yes()).click(); - $(BreakdownDrivingPage.submit()).click(); + it("When I start a repeating section and don't skip the calculated question, and enter an answer that is not equal to the total for the spending question, Then I should see a validation error", async () => { + await $(HubPage.summaryRowLink(repeatingSectionId(1))).click(); + await $(BreakdownDrivingPage.yes()).click(); + await click(BreakdownDrivingPage.submit()); - answerAndSubmitSpendingBreakdownQuestion(500, 500, 500); + await answerAndSubmitSpendingBreakdownQuestion(500, 500, 500); - expect($(SpendingBreakdownPage.errorNumber(1)).getText()).to.contain("Enter answers that add up to ÂŖ1,000.00"); + await expect(await $(SpendingBreakdownPage.errorNumber(1)).getText()).toBe("Enter answers that add up to ÂŖ1,000.00"); }); - it("When I enter an answer that is equal to the total for the spending question, Then I should be able to get to the section summary and the repeating section should be marked as 'Completed'", () => { - answerAndSubmitSpendingBreakdownQuestion(500, 250, 250); + it("When I enter an answer that is equal to the total for the spending question, Then I should be able to get to the section summary and the repeating section should be marked as 'Completed'", async () => { + await answerAndSubmitSpendingBreakdownQuestion(500, 250, 250); + await answerAndSubmitEntertainmentBreakdownQuestion(250, 150, 100); - expect(browser.getUrl()).to.contain(BreakdownSectionSummary.pageName); - $(BreakdownSectionSummary.submit()).click(); + await verifyUrlContains(BreakdownSectionSummary.pageName); + await click(BreakdownSectionSummary.submit()); - expect($(HubPage.summaryRowState(repeatingSectionId(1))).getText()).to.equal("Completed"); + await expect(await $(HubPage.summaryRowState(repeatingSectionId(1))).getText()).toBe("Completed"); }); - it("When I start another repeating section and answer 'No' to the driving question, Then I should not have to answer the breakdown question and the section is marked as 'Completed'", () => { - $(HubPage.summaryRowLink(repeatingSectionId(2))).click(); - $(BreakdownDrivingPage.no()).click(); - $(BreakdownDrivingPage.submit()).click(); + it("When I start another repeating section and answer 'No' to the driving question, Then I should not have to answer the breakdown question and the section is marked as 'Completed'", async () => { + await $(HubPage.summaryRowLink(repeatingSectionId(2))).click(); + await $(BreakdownDrivingPage.no()).click(); + await click(BreakdownDrivingPage.submit()); - expect(browser.getUrl()).to.contain(BreakdownSectionSummary.pageName); - $(BreakdownSectionSummary.submit()).click(); + await verifyUrlContains(BreakdownSectionSummary.pageName); + await click(BreakdownSectionSummary.submit()); - expect($(HubPage.summaryRowState(repeatingSectionId(2))).getText()).to.equal("Completed"); + await expect(await $(HubPage.summaryRowState(repeatingSectionId(2))).getText()).toBe("Completed"); }); - it("When I change my answer for the total spending question, Then the first repeating section should be marked as 'Partially completed' and section repeating section should stay as 'Completed'", () => { - $(HubPage.summaryRowLink(householdOverviewSectionId)).click(); - $(HouseholdOverviewSectionSummary.totalSpendingAnswerEdit()).click(); + it("When I change my answer for the total spending question, Then the first repeating section should be marked as 'Partially completed' and section repeating section should stay as 'Completed'", async () => { + await $(HubPage.summaryRowLink(householdOverviewSectionId)).click(); + await $(HouseholdOverviewSectionSummary.totalSpendingAnswerEdit()).click(); - answerAndSubmitTotalSpendingQuestion(1500); - $(HouseholdOverviewSectionSummary.submit()).click(); - expect($(HubPage.summaryRowState(repeatingSectionId(1))).getText()).to.equal("Partially completed"); + await answerAndSubmitTotalSpendingQuestion(1500); + await click(HouseholdOverviewSectionSummary.submit()); + await expect(await $(HubPage.summaryRowState(repeatingSectionId(1))).getText()).toBe("Partially completed"); // The 2nd repeating section skipped the breakdown question, therefore progress should updated for sections that have // calculated questions on the path. - expect($(HubPage.summaryRowState(repeatingSectionId(2))).getText()).to.equal("Completed"); + await expect(await $(HubPage.summaryRowState(repeatingSectionId(2))).getText()).toBe("Completed"); }); assertRepeatingSectionOnChange(1, "500.00", "250.00", "250.00", "1,500.00"); - it("When I change my answer to the driving question to 'Yes' for the 2nd repeating section, Then I am able to answer the breakdown question and complete the section", () => { - $(HubPage.summaryRowLink(repeatingSectionId(2))).click(); - $(BreakdownSectionSummary.breakdownDrivingAnswerEdit()).click(); - $(BreakdownDrivingPage.yes()).click(); - $(BreakdownDrivingPage.submit()).click(); + it("When I change my answer to the driving question to 'Yes' for the 2nd repeating section, Then I am able to answer the breakdown question and complete the section", async () => { + await $(HubPage.summaryRowLink(repeatingSectionId(2))).click(); + await $(BreakdownSectionSummary.breakdownDrivingAnswerEdit()).click(); + await $(BreakdownDrivingPage.yes()).click(); + await click(BreakdownDrivingPage.submit()); - answerAndSubmitSpendingBreakdownQuestion(1000, 500, 0); - $(BreakdownSectionSummary.submit()).click(); - expect($(HubPage.summaryRowState(repeatingSectionId(2))).getText()).to.equal("Completed"); + await answerAndSubmitSpendingBreakdownQuestion(1000, 500, 0); + await answerAndSubmitEntertainmentBreakdownQuestion(250, 150, 100); + await click(BreakdownSectionSummary.submit()); + await expect(await $(HubPage.summaryRowState(repeatingSectionId(2))).getText()).toBe("Completed"); }); - it("When I change my answer for the total spending question, Then both repeating section should be marked as 'Partially completed'", () => { - $(HubPage.summaryRowLink(householdOverviewSectionId)).click(); - $(HouseholdOverviewSectionSummary.totalSpendingAnswerEdit()).click(); + it("When I change my answer for the total spending question, Then both repeating section should be marked as 'Partially completed'", async () => { + await $(HubPage.summaryRowLink(householdOverviewSectionId)).click(); + await $(HouseholdOverviewSectionSummary.totalSpendingAnswerEdit()).click(); - answerAndSubmitTotalSpendingQuestion(2500); - $(HouseholdOverviewSectionSummary.submit()).click(); - expect($(HubPage.summaryRowState(repeatingSectionId(1))).getText()).to.equal("Partially completed"); + await answerAndSubmitTotalSpendingQuestion(2500); + await click(HouseholdOverviewSectionSummary.submit()); + await expect(await $(HubPage.summaryRowState(repeatingSectionId(1))).getText()).toBe("Partially completed"); // The 2nd repeating section is now on the path, therefore, its status should have been updated. - expect($(HubPage.summaryRowState(repeatingSectionId(2))).getText()).to.equal("Partially completed"); + await expect(await $(HubPage.summaryRowState(repeatingSectionId(2))).getText()).toBe("Partially completed"); }); assertRepeatingSectionOnChange(1, "1500.00", "0.00", "0.00", "2,500.00"); assertRepeatingSectionOnChange(2, "1000.00", "500.00", "0.00", "2,500.00"); - it("When I edit and resubmit the total spending question without changing the value, Then the repeating section's status should stay as 'Completed'", () => { - $(HubPage.summaryRowLink(householdOverviewSectionId)).click(); - $(HouseholdOverviewSectionSummary.totalSpendingAnswerEdit()).click(); + it("When I edit and resubmit the total spending question without changing the value, Then the repeating section's status should stay as 'Completed'", async () => { + await $(HubPage.summaryRowLink(householdOverviewSectionId)).click(); + await $(HouseholdOverviewSectionSummary.totalSpendingAnswerEdit()).click(); - expect($(TotalSpendingPage.totalSpending()).getValue()).to.equal("2500.00"); - $(TotalSpendingPage.submit()).click(); - $(HouseholdOverviewSectionSummary.submit()).click(); + await expect(await $(TotalSpendingPage.totalSpending()).getValue()).toBe("2500.00"); + await click(TotalSpendingPage.submit()); + await click(HouseholdOverviewSectionSummary.submit()); - expect($(HubPage.summaryRowState(repeatingSectionId(1))).getText()).to.equal("Completed"); - expect($(HubPage.summaryRowState(repeatingSectionId(2))).getText()).to.equal("Completed"); + await expect(await $(HubPage.summaryRowState(repeatingSectionId(1))).getText()).toBe("Completed"); + await expect(await $(HubPage.summaryRowState(repeatingSectionId(2))).getText()).toBe("Completed"); }); - it("When I submit the questionnaire, Then I should see the thank you page", () => { - $(HubPage.submit()).click(); + it("When I submit the questionnaire, Then I should see the thank you page", async () => { + await click(HubPage.submit()); - expect(browser.getUrl()).to.contain(ThankYouPage.pageName); + await verifyUrlContains(ThankYouPage.pageName); }); }); }); diff --git a/tests/functional/spec/features/validation/sum/less_than.spec.js b/tests/functional/spec/features/validation/sum/less_than.spec.js index 1f662b7e9e..4a9a0d4269 100644 --- a/tests/functional/spec/features/validation/sum/less_than.spec.js +++ b/tests/functional/spec/features/validation/sum/less_than.spec.js @@ -1,61 +1,61 @@ import TotalAnswerPage from "../../../../generated_pages/validation_sum_against_total_less_than/total-block.page"; import BreakdownAnswerPage from "../../../../generated_pages/validation_sum_against_total_less_than/breakdown-block.page"; import SubmitPage from "../../../../generated_pages/validation_sum_against_total_less_than/submit.page"; - +import { click, verifyUrlContains } from "../../../../helpers"; describe("Feature: Sum of grouped answers validation (less than) against total", () => { - beforeEach(() => { - browser.openQuestionnaire("test_validation_sum_against_total_less_than.json"); + beforeEach(async () => { + await browser.openQuestionnaire("test_validation_sum_against_total_less_than.json"); }); describe("Given I start a grouped answer validation survey and enter 12 into the total", () => { - it("When I continue and enter 2 in each breakdown field, Then I should be able to get to the summary", () => { - $(TotalAnswerPage.total()).setValue("12"); - $(TotalAnswerPage.submit()).click(); - $(BreakdownAnswerPage.breakdown1()).setValue("2"); - $(BreakdownAnswerPage.breakdown2()).setValue("2"); - $(BreakdownAnswerPage.breakdown3()).setValue("2"); - $(BreakdownAnswerPage.breakdown4()).setValue("2"); - $(BreakdownAnswerPage.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); + it("When I continue and enter 2 in each breakdown field, Then I should be able to get to the summary", async () => { + await $(TotalAnswerPage.total()).setValue("12"); + await click(TotalAnswerPage.submit()); + await $(BreakdownAnswerPage.breakdown1()).setValue("2"); + await $(BreakdownAnswerPage.breakdown2()).setValue("2"); + await $(BreakdownAnswerPage.breakdown3()).setValue("2"); + await $(BreakdownAnswerPage.breakdown4()).setValue("2"); + await click(BreakdownAnswerPage.submit()); + await verifyUrlContains(SubmitPage.pageName); }); }); describe("Given I start a grouped answer validation survey and enter 5 into the total", () => { - it("When I continue and enter 4 into breakdown 1 and leave the others empty, Then I should be able to get to the summary", () => { - $(TotalAnswerPage.total()).setValue("5"); - $(TotalAnswerPage.submit()).click(); - $(BreakdownAnswerPage.breakdown1()).setValue("4"); - $(BreakdownAnswerPage.breakdown2()).setValue(""); - $(BreakdownAnswerPage.breakdown3()).setValue(""); - $(BreakdownAnswerPage.breakdown4()).setValue(""); - $(BreakdownAnswerPage.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); + it("When I continue and enter 4 into breakdown 1 and leave the others empty, Then I should be able to get to the summary", async () => { + await $(TotalAnswerPage.total()).setValue("5"); + await click(TotalAnswerPage.submit()); + await $(BreakdownAnswerPage.breakdown1()).setValue("4"); + await $(BreakdownAnswerPage.breakdown2()).setValue(""); + await $(BreakdownAnswerPage.breakdown3()).setValue(""); + await $(BreakdownAnswerPage.breakdown4()).setValue(""); + await click(BreakdownAnswerPage.submit()); + await verifyUrlContains(SubmitPage.pageName); }); }); describe("Given I start a grouped answer validation survey and enter 12 into the total", () => { - it("When I continue and enter 3 in each breakdown field, Then I should see a validation error", () => { - $(TotalAnswerPage.total()).setValue("12"); - $(TotalAnswerPage.submit()).click(); - $(BreakdownAnswerPage.breakdown1()).setValue("3"); - $(BreakdownAnswerPage.breakdown2()).setValue("3"); - $(BreakdownAnswerPage.breakdown3()).setValue("3"); - $(BreakdownAnswerPage.breakdown4()).setValue("3"); - $(BreakdownAnswerPage.submit()).click(); - expect($(BreakdownAnswerPage.errorNumber(1)).getText()).to.contain("Enter answers that add up to less than ÂŖ12.00"); + it("When I continue and enter 3 in each breakdown field, Then I should see a validation error", async () => { + await $(TotalAnswerPage.total()).setValue("12"); + await click(TotalAnswerPage.submit()); + await $(BreakdownAnswerPage.breakdown1()).setValue("3"); + await $(BreakdownAnswerPage.breakdown2()).setValue("3"); + await $(BreakdownAnswerPage.breakdown3()).setValue("3"); + await $(BreakdownAnswerPage.breakdown4()).setValue("3"); + await click(BreakdownAnswerPage.submit()); + await expect(await $(BreakdownAnswerPage.errorNumber(1)).getText()).toBe("Enter answers that add up to less than ÂŖ12.00"); }); }); describe("Given I start a grouped answer validation survey and enter 5 into the total", () => { - it("When I continue and enter 3 in each breakdown field, Then I should see a validation error", () => { - $(TotalAnswerPage.total()).setValue("5"); - $(TotalAnswerPage.submit()).click(); - $(BreakdownAnswerPage.breakdown1()).setValue("3"); - $(BreakdownAnswerPage.breakdown2()).setValue("3"); - $(BreakdownAnswerPage.breakdown3()).setValue("3"); - $(BreakdownAnswerPage.breakdown4()).setValue("3"); - $(BreakdownAnswerPage.submit()).click(); - expect($(BreakdownAnswerPage.errorNumber(1)).getText()).to.contain("Enter answers that add up to less than ÂŖ5.00"); + it("When I continue and enter 3 in each breakdown field, Then I should see a validation error", async () => { + await $(TotalAnswerPage.total()).setValue("5"); + await click(TotalAnswerPage.submit()); + await $(BreakdownAnswerPage.breakdown1()).setValue("3"); + await $(BreakdownAnswerPage.breakdown2()).setValue("3"); + await $(BreakdownAnswerPage.breakdown3()).setValue("3"); + await $(BreakdownAnswerPage.breakdown4()).setValue("3"); + await click(BreakdownAnswerPage.submit()); + await expect(await $(BreakdownAnswerPage.errorNumber(1)).getText()).toBe("Enter answers that add up to less than ÂŖ5.00"); }); }); }); diff --git a/tests/functional/spec/features/validation/sum/value_source.spec.js b/tests/functional/spec/features/validation/sum/value_source.spec.js new file mode 100644 index 0000000000..c6b1ab6884 --- /dev/null +++ b/tests/functional/spec/features/validation/sum/value_source.spec.js @@ -0,0 +1,186 @@ +import TotalAnswerPage from "../../../../generated_pages/validation_sum_against_value_source/total-block.page"; +import BreakdownAnswerPage from "../../../../generated_pages/validation_sum_against_value_source/breakdown-block.page"; +import TotalPlaybackPage from "../../../../generated_pages/validation_sum_against_value_source/number-total-playback.page"; +import SecondBreakdownAnswerPage from "../../../../generated_pages/validation_sum_against_value_source/second-breakdown-block.page"; +import SubmitPage from "../../../../generated_pages/validation_sum_against_total_equal/submit.page"; +import AnotherTotalPlaybackPage from "../../../../generated_pages/validation_sum_against_value_source/another-number-total-playback.page"; +import { click, verifyUrlContains } from "../../../../helpers"; +const answerAndSubmitBreakdownQuestion = async (breakdown1, breakdown2, breakdown3, breakdown4) => { + await $(BreakdownAnswerPage.breakdown1()).setValue(breakdown1); + await $(BreakdownAnswerPage.breakdown2()).setValue(breakdown2); + await $(BreakdownAnswerPage.breakdown3()).setValue(breakdown3); + await $(BreakdownAnswerPage.breakdown4()).setValue(breakdown4); + await click(BreakdownAnswerPage.submit()); +}; + +const answerAndSubmitSecondBreakdownQuestion = async (breakdown1, breakdown2, breakdown3, breakdown4) => { + await $(SecondBreakdownAnswerPage.secondBreakdown1()).setValue(breakdown1); + await $(SecondBreakdownAnswerPage.secondBreakdown2()).setValue(breakdown2); + await $(SecondBreakdownAnswerPage.secondBreakdown3()).setValue(breakdown3); + await $(SecondBreakdownAnswerPage.secondBreakdown4()).setValue(breakdown4); + await click(SecondBreakdownAnswerPage.submit()); +}; + +const answerBothBreakdownQuestions = async (array1, array2) => { + await answerAndSubmitBreakdownQuestion(array1[0], array1[1], array1[2], array1[3]); + + await click(TotalPlaybackPage.submit()); + + await answerAndSubmitSecondBreakdownQuestion(array2[0], array2[1], array2[2], array2[3]); +}; + +describe("Feature: Sum of grouped answers equal to validation against value source ", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_validation_sum_against_value_source.json"); + }); + + describe("Given I start a grouped answer validation survey and enter 12 into the total", () => { + it("When I continue and enter 3 in each breakdown field, Then I should be able to get to the total playback page", async () => { + await $(TotalAnswerPage.total()).setValue("12"); + await click(TotalAnswerPage.submit()); + + await answerAndSubmitBreakdownQuestion("3", "3", "3", "3"); + + await verifyUrlContains(TotalPlaybackPage.pageName); + }); + }); + + describe("Given I have a calculated summary value of 12", () => { + it("When I continue to second breakdown and enter values equal to calculated summary total, Then I should be able to get to the next calculated summary", async () => { + await $(TotalAnswerPage.total()).setValue("12"); + await click(TotalAnswerPage.submit()); + + await answerBothBreakdownQuestions(["3", "3", "3", "3"], ["2", "2", "1", "1"]); + + await verifyUrlContains(AnotherTotalPlaybackPage.pageName); + }); + }); + + describe("Given I completed both grouped answer validation questions and I am on the summary", () => { + it("When I go back from the summary and change the total, Then I must reconfirm both breakdown questions with valid answers before I can get to the next calculated summary", async () => { + await $(TotalAnswerPage.total()).setValue("12"); + await click(TotalAnswerPage.submit()); + + await answerBothBreakdownQuestions(["3", "3", "3", "3"], ["2", "2", "1", "1"]); + + await click(AnotherTotalPlaybackPage.submit()); + + await $(SubmitPage.totalAnswerEdit()).click(); + await $(TotalAnswerPage.total()).setValue("15"); + await click(TotalAnswerPage.submit()); + + await click(BreakdownAnswerPage.submit()); + + await expect(await $(BreakdownAnswerPage.singleErrorLink()).isDisplayed()).toBe(true); + + await expect(await $(BreakdownAnswerPage.errorNumber(1)).getText()).toBe("Enter answers that add up to 15"); + + await answerBothBreakdownQuestions(["6", "3", "3", "3"], ["3", "3", "2", "1"]); + + await verifyUrlContains(AnotherTotalPlaybackPage.pageName); + }); + }); + + describe("Given I completed both grouped answer validation questions and I am on the summary", () => { + it("When I go back from the summary and change the total, Then I must reconfirm the breakdown question based on answer value source with valid answers before I can continue", async () => { + await $(TotalAnswerPage.total()).setValue("12"); + await click(TotalAnswerPage.submit()); + + await answerBothBreakdownQuestions(["3", "3", "3", "3"], ["2", "2", "1", "1"]); + + await click(AnotherTotalPlaybackPage.submit()); + + await $(SubmitPage.totalAnswerEdit()).click(); + await $(TotalAnswerPage.total()).setValue("15"); + await click(TotalAnswerPage.submit()); + + await answerAndSubmitBreakdownQuestion("0", "3", "3", "3"); + + await expect(await $(BreakdownAnswerPage.singleErrorLink()).isDisplayed()).toBe(true); + + await expect(await $(BreakdownAnswerPage.errorNumber(1)).getText()).toBe("Enter answers that add up to 15"); + + await answerBothBreakdownQuestions(["5", "4", "4", "2"], ["3", "3", "2", "1"]); + + await verifyUrlContains(AnotherTotalPlaybackPage.pageName); + }); + }); + + describe("Given I completed both grouped answer validation questions and I am on the summary", () => { + it("When I go back from the summary and change the first breakdown question answers so its total changes, Then I must reconfirm the second breakdown question based on calculated summary value source with valid answers before I can continue", async () => { + await $(TotalAnswerPage.total()).setValue("12"); + await click(TotalAnswerPage.submit()); + + await answerBothBreakdownQuestions(["3", "3", "3", "3"], ["2", "2", "1", "1"]); + + await click(AnotherTotalPlaybackPage.submit()); + + await $(SubmitPage.breakdown1Edit()).click(); + + await answerAndSubmitBreakdownQuestion("6", "3", "2", "1"); + + await click(TotalPlaybackPage.submit()); + + await click(SecondBreakdownAnswerPage.submit()); + + await expect(await $(SecondBreakdownAnswerPage.singleErrorLink()).isDisplayed()).toBe(true); + + await expect(await $(SecondBreakdownAnswerPage.errorNumber(1)).getText()).toBe("Enter answers that add up to 9"); + + await answerAndSubmitSecondBreakdownQuestion("5", "4", "0", "0"); + + await expect(await $(SecondBreakdownAnswerPage.singleErrorLink()).isDisplayed()).toBe(false); + + await verifyUrlContains(AnotherTotalPlaybackPage.pageName); + }); + }); + + describe("Given I start a grouped answer validation survey and enter 5 into the total", () => { + it("When I continue and enter 3 in each breakdown field, Then I should see a validation error", async () => { + await $(TotalAnswerPage.total()).setValue("5"); + await click(TotalAnswerPage.submit()); + + await answerAndSubmitBreakdownQuestion("3", "3", "3", "3"); + + await expect(await $(BreakdownAnswerPage.errorNumber(1)).getText()).toBe("Enter answers that add up to 5"); + }); + }); + + describe("Given I start a grouped answer validation survey and enter 5 into the total", () => { + it("When I enter 3 in each breakdown field and continue to second breakdown and enter 3 in each field, Then I should see a validation error", async () => { + await $(TotalAnswerPage.total()).setValue("5"); + await click(TotalAnswerPage.submit()); + + await answerBothBreakdownQuestions(["2", "1", "1", "1"], ["3", "3", "3", "3"]); + + await expect(await $(SecondBreakdownAnswerPage.errorNumber(1)).getText()).toBe("Enter answers that add up to 3"); + }); + }); + describe("Given I edit a question from a Calculated Summary page", () => { + it("When I change the answer and there is a question that needs to be revisited before I can return to the Calculated Summary Page, Then I revisit the relevant page before I route back to the Calculated Summary page", async () => { + await $(TotalAnswerPage.total()).setValue("5"); + await click(TotalAnswerPage.submit()); + + await answerBothBreakdownQuestions(["2", "1", "1", "1"], ["1", "2", "0", "0"]); + + await $(AnotherTotalPlaybackPage.breakdown1Edit()).click(); + + await $(BreakdownAnswerPage.breakdown1()).setValue("1"); + await $(BreakdownAnswerPage.breakdown2()).setValue("2"); + + await click(BreakdownAnswerPage.submit()); + + await click(TotalPlaybackPage.previous()); + + await click(BreakdownAnswerPage.submit()); + + await click(TotalPlaybackPage.submit()); + + await click(SecondBreakdownAnswerPage.submit()); + + await click(AnotherTotalPlaybackPage.submit()); + + await verifyUrlContains(SubmitPage.pageName); + }); + }); +}); diff --git a/tests/functional/spec/features/view_submitted_response/view_submitted_response.spec.js b/tests/functional/spec/features/view_submitted_response/view_submitted_response.spec.js index c9be417e64..ec66ee4280 100644 --- a/tests/functional/spec/features/view_submitted_response/view_submitted_response.spec.js +++ b/tests/functional/spec/features/view_submitted_response/view_submitted_response.spec.js @@ -3,41 +3,152 @@ import NameBlockPage from "../../../generated_pages/view_submitted_response/name import SubmitPage from "../../../generated_pages/view_submitted_response/submit.page.js"; import ThankYouPage from "../../../base_pages/thank-you.page"; import ViewSubmittedResponsePage from "../../../generated_pages/view_submitted_response/view-submitted-response.page.js"; +import ViewSubmittedResponseRepeatingPage from "../../../generated_pages/view_submitted_response_repeating_sections/view-submitted-response.page.js"; +import HubPage from "../../../base_pages/hub.page"; +import PrimaryPersonListCollectorPage from "../../../generated_pages/view_submitted_response_repeating_sections/primary-person-list-collector.page"; +import PrimaryPersonListCollectorAddPage from "../../../generated_pages/view_submitted_response_repeating_sections/primary-person-list-collector-add.page"; +import ListCollectorPage from "../../../generated_pages/view_submitted_response_repeating_sections/list-collector.page"; +import SkipFirstNumberBlockPageSectionOne from "../../../generated_pages/view_submitted_response_repeating_sections/skip-first-block.page"; +import FirstNumberBlockPageSectionOne from "../../../generated_pages/view_submitted_response_repeating_sections/first-number-block.page"; +import FirstAndAHalfNumberBlockPageSectionOne from "../../../generated_pages/view_submitted_response_repeating_sections/first-and-a-half-number-block.page"; +import SecondNumberBlockPageSectionOne from "../../../generated_pages/view_submitted_response_repeating_sections/second-number-block.page"; +import CalculatedSummarySectionOne from "../../../generated_pages/view_submitted_response_repeating_sections/currency-total-playback-1.page"; +import SectionSummarySectionOne from "../../../generated_pages/view_submitted_response_repeating_sections/questions-section-summary.page"; +import ThirdNumberBlockPageSectionTwo from "../../../generated_pages/view_submitted_response_repeating_sections/third-number-block.page"; +import CalculatedSummarySectionTwo from "../../../generated_pages/view_submitted_response_repeating_sections/currency-total-playback-2.page"; +import DependencyQuestionSectionTwo from "../../../generated_pages/view_submitted_response_repeating_sections/mutually-exclusive-checkbox.page"; +import SkippableBlockSectionTwo from "../../../generated_pages/view_submitted_response_repeating_sections/skippable-block.page"; +import SectionSummarySectionTwo from "../../../generated_pages/new_calculated_summary_cross_section_dependencies_repeating/calculated-summary-section-summary.page"; +import ListCollectorAddPage from "../../../generated_pages/view_submitted_response_repeating_sections//list-collector-add.page"; +import { click, verifyUrlContains } from "../../../helpers"; describe("View Submitted Response", () => { - beforeEach("Load the questionnaire", () => { - browser.openQuestionnaire("test_view_submitted_response.json"); - $(NameBlockPage.answer()).setValue("John Smith"); - $(NameBlockPage.submit()).click(); - $(AddressBlockPage.answer()).setValue("NP10 8XG"); - $(AddressBlockPage.submit()).click(); - $(SubmitPage.submit()).click(); - expect(browser.getUrl()).to.contain(ThankYouPage.pageName); - expect($(ThankYouPage.title()).getHTML()).to.contain("Thank you for completing the Test"); - $(ThankYouPage.savePrintAnswersLink()).click(); - expect(browser.getUrl()).to.contain(ViewSubmittedResponsePage.pageName); + beforeEach("Load the questionnaire", async () => { + await browser.openQuestionnaire("test_view_submitted_response.json"); + await $(NameBlockPage.answer()).setValue("John Smith"); + await click(NameBlockPage.submit()); + await $(AddressBlockPage.answer()).setValue("NP10 8XG"); + await click(AddressBlockPage.submit()); + await click(SubmitPage.submit()); + await verifyUrlContains(ThankYouPage.pageName); + await expect(await $(ThankYouPage.title()).getHTML()).toContain("Thank you for completing the Test"); + await $(ThankYouPage.savePrintAnswersLink()).click(); + await verifyUrlContains(ViewSubmittedResponsePage.pageName); }); - it("Given I have completed a questionnaire with view submitted response enabled, When I am on the view submitted response page within 45 minutes of submission, Then the summary is displayed correctly", () => { - expect($(ViewSubmittedResponsePage.informationPanel()).isDisplayed()).to.be.false; - expect($(ViewSubmittedResponsePage.printButton()).isDisplayed()).to.be.true; - expect($(ViewSubmittedResponsePage.heading()).getText()).to.equal("Answers submitted for Apple"); - expect($(ViewSubmittedResponsePage.metadataTerm(1)).getText()).to.equal("Submitted on:"); - expect($(ViewSubmittedResponsePage.metadataTerm(2)).getText()).to.equal("Submission reference:"); - expect($(ViewSubmittedResponsePage.personalDetailsGroupTitle()).getText()).to.equal("Personal Details"); - expect($(ViewSubmittedResponsePage.nameQuestion()).getText()).to.equal("What is your name?"); - expect($(ViewSubmittedResponsePage.nameAnswer()).getText()).to.equal("John Smith"); - expect($(ViewSubmittedResponsePage.addressDetailsGroupTitle()).getText()).to.equal("Address Details"); - expect($(ViewSubmittedResponsePage.addressQuestion()).getText()).to.equal("What is your address?"); - expect($(ViewSubmittedResponsePage.addressAnswer()).getText()).to.equal("NP10 8XG"); + it("Given I have completed a questionnaire with view submitted response enabled, When I am on the view submitted response page within 45 minutes of submission, Then the summary is displayed correctly", async () => { + await expect(await $(ViewSubmittedResponsePage.informationPanel()).isDisplayed()).toBe(false); + await expect(await $(ViewSubmittedResponsePage.printButton()).isDisplayed()).toBe(true); + await expect(await $(ViewSubmittedResponsePage.heading()).getText()).toBe("Answers submitted for Apple (Apple)"); + await expect(await $(ViewSubmittedResponsePage.metadataTerm(1)).getText()).toBe("Submitted on:"); + await expect(await $(ViewSubmittedResponsePage.metadataTerm(2)).getText()).toBe("Submission reference:"); + await expect(await $(ViewSubmittedResponsePage.personalDetailsGroupTitle()).getText()).toBe("Personal Details"); + await expect(await $(ViewSubmittedResponsePage.nameQuestion()).getText()).toBe("What is your name?"); + await expect(await $(ViewSubmittedResponsePage.nameAnswer()).getText()).toBe("John Smith"); + await expect(await $(ViewSubmittedResponsePage.addressDetailsGroupTitle()).getText()).toBe("Address Details"); + await expect(await $(ViewSubmittedResponsePage.addressQuestion()).getText()).toBe("What is your address?"); + await expect(await $(ViewSubmittedResponsePage.addressAnswer()).getText()).toBe("NP10 8XG"); }); describe("Given I am on the view submitted response page and I submitted over 45 minutes ago", () => { - it("When I click the Download as PDF button, Then I should be redirected to a page informing me that I can no longer view or get a copy of my answers", () => { - browser.pause(40000); // Waiting 40 seconds for the timeout to expire (45 minute timeout changed to 35 seconds by overriding VIEW_SUBMITTED_RESPONSE_EXPIRATION_IN_SECONDS for the purpose of the functional test) - $(ViewSubmittedResponsePage.downloadButton()).click(); - expect($(ViewSubmittedResponsePage.informationPanel()).isDisplayed()).to.be.true; - expect($(ViewSubmittedResponsePage.informationPanel()).getHTML()).to.contain("For security, you can no longer view or get a copy of your answers"); + it("When I click the Download as PDF button, Then I should be redirected to a page informing me that I can no longer view or get a copy of my answers", async () => { + await browser.pause(40000); // Waiting 40 seconds for the timeout to expire (45 minute timeout changed to 35 seconds by overriding VIEW_SUBMITTED_RESPONSE_EXPIRATION_IN_SECONDS for the purpose of the functional test) + await $(ViewSubmittedResponsePage.downloadButton()).click(); + await expect(await $(ViewSubmittedResponsePage.informationPanel()).isDisplayed()).toBe(true); + await expect(await $(ViewSubmittedResponsePage.informationPanel()).getHTML()).toContain( + "For security, you can no longer view or get a copy of your answers", + ); }); }); }); + +const firstGroup = 'div[id="calculated-summary-0"]'; +const secondGroup = 'div[id="calculated-summary-0-1"]'; +const groupTitle = 'h3[class="ons-summary__group-title"]'; +const repeatingSectionAnswer = '[data-qa="checkbox-answer"]'; +const skippableRepeatingSectionAnswer = '[data-qa="skippable-answer"]'; + +describe("View Submitted Response Summary Page With Repeating Sections", () => { + beforeEach("Load the questionnaire", async () => { + await browser.openQuestionnaire("test_view_submitted_response_repeating_sections.json"); + await click(HubPage.submit()); + + await $(NameBlockPage.answer()).setValue("John Smith"); + await click(NameBlockPage.submit()); + await $(AddressBlockPage.answer()).setValue("NP10 8XG"); + await click(AddressBlockPage.submit()); + + await click(HubPage.submit()); + await $(PrimaryPersonListCollectorPage.yes()).click(); + await click(PrimaryPersonListCollectorPage.submit()); + await $(PrimaryPersonListCollectorAddPage.firstName()).setValue("Marcus"); + await $(PrimaryPersonListCollectorAddPage.lastName()).setValue("Twin"); + await click(PrimaryPersonListCollectorAddPage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("John"); + await $(ListCollectorAddPage.lastName()).setValue("Doe"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await click(HubPage.submit()); + + await $(SkipFirstNumberBlockPageSectionOne.no()).click(); + await click(SkipFirstNumberBlockPageSectionOne.submit()); + await $(FirstNumberBlockPageSectionOne.firstNumber()).setValue(10); + await click(FirstNumberBlockPageSectionOne.submit()); + await $(FirstAndAHalfNumberBlockPageSectionOne.firstAndAHalfNumberAlsoInTotal()).setValue(20); + await click(FirstAndAHalfNumberBlockPageSectionOne.submit()); + await $(SecondNumberBlockPageSectionOne.secondNumberAlsoInTotal()).setValue(30); + await click(SecondNumberBlockPageSectionOne.submit()); + await click(CalculatedSummarySectionOne.submit()); + await click(SectionSummarySectionOne.submit()); + await click(HubPage.submit()); + await $(ThirdNumberBlockPageSectionTwo.thirdNumber()).setValue(20); + await $(ThirdNumberBlockPageSectionTwo.thirdNumberAlsoInTotal()).setValue(20); + await click(ThirdNumberBlockPageSectionTwo.submit()); + await click(CalculatedSummarySectionTwo.submit()); + await $(DependencyQuestionSectionTwo.checkboxAnswerCalcValue2()).click(); + await click(DependencyQuestionSectionTwo.submit()); + await $(SkippableBlockSectionTwo.skippable()).setValue(100); + await click(SkippableBlockSectionTwo.submit()); + await click(SectionSummarySectionTwo.submit()); + await click(HubPage.submit()); + await $(ThirdNumberBlockPageSectionTwo.thirdNumber()).setValue(40); + await $(ThirdNumberBlockPageSectionTwo.thirdNumberAlsoInTotal()).setValue(40); + await click(ThirdNumberBlockPageSectionTwo.submit()); + await click(CalculatedSummarySectionTwo.submit()); + await $(DependencyQuestionSectionTwo.checkboxAnswerCalcValue2()).click(); + await click(DependencyQuestionSectionTwo.submit()); + await click(SectionSummarySectionTwo.submit()); + + await click(HubPage.submit()); + await verifyUrlContains(ThankYouPage.pageName); + await expect(await $(ThankYouPage.title()).getHTML()).toContain("Thank you for completing the Test"); + await $(ThankYouPage.savePrintAnswersLink()).click(); + await verifyUrlContains(ViewSubmittedResponsePage.pageName); + }); + + it("Given I have completed a questionnaire with a repeating section and view submitted response enabled, When I am on the view submitted response page within 45 minutes of submission, Then the summary is displayed correctly", async () => { + await expect(await $(ViewSubmittedResponseRepeatingPage.informationPanel()).isDisplayed()).toBe(false); + await expect(await $(ViewSubmittedResponseRepeatingPage.printButton()).isDisplayed()).toBe(true); + await expect(await $(ViewSubmittedResponseRepeatingPage.heading()).getText()).toBe("Answers submitted for Apple (Apple)"); + await expect(await $(ViewSubmittedResponseRepeatingPage.metadataTerm(1)).getText()).toBe("Submitted on:"); + await expect(await $(ViewSubmittedResponseRepeatingPage.metadataTerm(2)).getText()).toBe("Submission reference:"); + await expect(await $(ViewSubmittedResponseRepeatingPage.personalDetailsGroupTitle()).getText()).toBe("Personal Details"); + await expect(await $(ViewSubmittedResponseRepeatingPage.nameQuestion()).getText()).toBe("What is your name?"); + await expect(await $(ViewSubmittedResponseRepeatingPage.nameAnswer()).getText()).toBe("John Smith"); + await expect(await $(ViewSubmittedResponseRepeatingPage.addressDetailsGroupTitle()).getText()).toBe("Address Details"); + await expect(await $(ViewSubmittedResponseRepeatingPage.addressQuestion()).getText()).toBe("What is your address?"); + await expect(await $(ViewSubmittedResponseRepeatingPage.addressAnswer()).getText()).toBe("NP10 8XG"); + await expect(await $("body").getHTML()).toContain("Marcus Twin"); + await expect(await $(firstGroup).$$(groupTitle)[0].getText()).toBe("Calculated Summary Group"); + await expect(await $(firstGroup).$$(repeatingSectionAnswer)[0].getText()).toBe("40 - calculated summary answer (current section)"); + await expect(await $("body").getHTML()).toContain("How much did Marcus Twin spend on fruit?"); + await expect(await $(firstGroup).$$(skippableRepeatingSectionAnswer)[0].getText()).toBe("ÂŖ100"); + await expect(await $("body").getHTML()).toContain("John Doe"); + await expect(await $(secondGroup).$$(groupTitle)[0].getText()).toBe("Calculated Summary Group"); + await expect(await $(secondGroup).$$(repeatingSectionAnswer)[0].getText()).toBe("80 - calculated summary answer (current section)"); + await expect(await $("body").getHTML()).not.toContain("How much did John Doe spend on fruit?"); + }); +}); diff --git a/tests/functional/spec/feedback.spec.js b/tests/functional/spec/feedback.spec.js index 0295819b19..3d0310de0e 100644 --- a/tests/functional/spec/feedback.spec.js +++ b/tests/functional/spec/feedback.spec.js @@ -4,51 +4,52 @@ import SubmitPage from "../generated_pages/feedback/submit.page"; import FeedbackPage from "../base_pages/feedback.page"; import FeedbackSentPage from "../base_pages/feedback-sent.page"; import ThankYouPage from "../base_pages/thank-you.page"; +import { click, verifyUrlContains } from "../helpers"; describe("Feedback", () => { describe("Given I launch and complete the test feedback survey", () => { - before(() => { - browser.openQuestionnaire("test_feedback.json"); - $(SchemaFeedbackPage.submit()).click(); - $(SubmitPage.submit()).click(); + before(async () => { + await browser.openQuestionnaire("test_feedback.json"); + await click(SchemaFeedbackPage.submit()); + await click(SubmitPage.submit()); }); - it("When I view the thank you page, Then I can see the feedback call to action", () => { - expect(browser.getUrl()).to.contain(ThankYouPage.pageName); - expect($(ThankYouPage.feedback()).getText()).to.contain("What do you think about this service?"); - expect($(ThankYouPage.feedbackLink()).getText()).to.equal("Give feedback"); - expect($(ThankYouPage.feedbackLink()).getAttribute("href")).to.contain("/submitted/feedback/send"); + it("When I view the thank you page, Then I can see the feedback call to action", async () => { + await verifyUrlContains(ThankYouPage.pageName); + await expect(await $(ThankYouPage.feedback()).getText()).toContain("What do you think about this service?"); + await expect(await $(ThankYouPage.feedbackLink()).getText()).toBe("Give feedback"); + await expect(await $(ThankYouPage.feedbackLink()).getAttribute("href")).toContain("/submitted/feedback/send"); }); - it("When I try to submit without providing feedback, then I stay on the feedback page and get an error message", () => { - browser.url(FeedbackPage.url()); - expect(browser.getUrl()).to.contain(FeedbackPage.pageName); - expect($(FeedbackPage.feedbackTitle()).getText()).to.contain("Give feedback about this service"); - $(FeedbackPage.submit()).click(); - expect(browser.getUrl()).to.contain(FeedbackPage.pageName); - expect($(FeedbackPage.errorPanel()).isExisting()).to.be.true; - expect($(FeedbackPage.errorPanel()).getText()).to.contain( - "There are 2 problems with your feedback\nSelect what your feedback is about\nEnter your feedback" + it("When I try to submit without providing feedback, then I stay on the feedback page and get an error message", async () => { + await browser.url(FeedbackPage.url()); + await verifyUrlContains(FeedbackPage.pageName); + await expect(await $(FeedbackPage.feedbackTitle()).getText()).toBe("Give feedback about this service"); + await click(FeedbackPage.submit()); + await verifyUrlContains(FeedbackPage.pageName); + await expect(await $(FeedbackPage.errorPanel()).isExisting()).toBe(true); + await expect(await $(FeedbackPage.errorPanel()).getText()).toBe( + "There are 2 problems with your feedback\nSelect what your feedback is about\nEnter your feedback", ); }); - it("When I enter valid feedback, Then I can submit the feedback page and get confirmation that the feedback has been sent", () => { - browser.url(FeedbackPage.url()); - $(FeedbackPage.feedbackTypeGeneralFeedback()).click(); - $(FeedbackPage.feedbackText()).setValue("Well done!"); - $(FeedbackPage.submit()).click(); - expect(browser.getUrl()).to.contain(FeedbackSentPage.pageName); - expect($(FeedbackSentPage.feedbackThankYouText()).getText()).to.contain("Thank you for your feedback"); + it("When I enter valid feedback, Then I can submit the feedback page and get confirmation that the feedback has been sent", async () => { + await browser.url(FeedbackPage.url()); + await $(FeedbackPage.feedbackTypeGeneralFeedback()).click(); + await $(FeedbackPage.feedbackText()).setValue("Well done!"); + await click(FeedbackPage.submit()); + await verifyUrlContains(FeedbackSentPage.pageName); + await expect(await $(FeedbackSentPage.feedbackThankYouText()).getText()).toBe("Thank you for your feedback"); }); - it("When I click the done button on the feedback sent page, Then I am taken to the thank you page", () => { - browser.url(FeedbackPage.url()); - $(FeedbackPage.feedbackTypeGeneralFeedback()).click(); - $(FeedbackPage.feedbackText()).setValue("Well done!"); - $(FeedbackPage.submit()).click(); - $(FeedbackSentPage.doneButton()).click(); - expect(browser.getUrl()).to.contain("thank-you"); - expect($(ThankYouPage.title()).getText()).to.contain("Thank you for completing the Feedback test schema"); + it("When I click the done button on the feedback sent page, Then I am taken to the thank you page", async () => { + await browser.url(FeedbackPage.url()); + await $(FeedbackPage.feedbackTypeGeneralFeedback()).click(); + await $(FeedbackPage.feedbackText()).setValue("Well done!"); + await click(FeedbackPage.submit()); + await $(FeedbackSentPage.doneButton()).click(); + await verifyUrlContains("thank-you"); + await expect(await $(ThankYouPage.title()).getText()).toBe("Thank you for completing the Feedback test schema"); }); }); }); diff --git a/tests/functional/spec/hub_and_spoke/hub_and_spoke.spec.js b/tests/functional/spec/hub_and_spoke/hub_and_spoke.spec.js new file mode 100644 index 0000000000..d4ca64bd64 --- /dev/null +++ b/tests/functional/spec/hub_and_spoke/hub_and_spoke.spec.js @@ -0,0 +1,383 @@ +import AccomodationDetailsSummaryBlockPage from "../../generated_pages/hub_and_spoke/accommodation-section-summary.page.js"; +import AnyoneRelated from "../../generated_pages/hub_and_spoke/anyone-related.page.js"; +import DoesAnyoneLiveHere from "../../generated_pages/hub_and_spoke/does-anyone-live-here.page.js"; +import EmploymentStatusBlockPage from "../../generated_pages/hub_and_spoke/employment-status.page.js"; +import EmploymentTypeBlockPage from "../../generated_pages/hub_and_spoke/employment-type.page.js"; +import HouseholdSummary from "../../generated_pages/hub_and_spoke/household-section-summary.page.js"; +import HowManyPeopleLiveHere from "../../generated_pages/hub_and_spoke/how-many-people-live-here.page.js"; +import HubPage from "../../base_pages/hub.page.js"; +import ProxyPage from "../../generated_pages/hub_and_spoke/proxy.page.js"; +import RelationshipsSummary from "../../generated_pages/hub_and_spoke/relationships-section-summary.page.js"; +import ListCollectorSectionSummaryPage from "../../generated_pages/hub_section_required_with_repeat/list-collector-section-summary.page.js"; +import ProxyRepeatPage from "../../generated_pages/hub_section_required_with_repeat/proxy.page.js"; +import { click, verifyUrlContains } from "../../helpers"; +import DateOfBirthPage from "../../generated_pages/hub_section_required_with_repeat/date-of-birth.page"; +import PrimaryPersonListCollectorPage from "../../generated_pages/hub_section_required_with_repeat/primary-person-list-collector.page"; +import PrimaryPersonListCollectorAddPage from "../../generated_pages/hub_section_required_with_repeat/primary-person-list-collector-add.page"; +import ListCollectorPage from "../../generated_pages/hub_section_required_with_repeat/list-collector.page"; +import ListCollectorAddPage from "../../generated_pages/hub_section_required_with_repeat/list-collector-add.page"; +import RepeatingSummaryPage from "../../generated_pages/hub_section_required_with_repeat/personal-details-section-summary.page"; +import { getRandomString } from "../../jwt_helper"; +import LoadedSuccessfullyBlockPage from "../../generated_pages/hub_section_required_with_repeat_supplementary/loaded-successfully-block.page"; +import IntroductionBlockPage from "../../generated_pages/hub_section_required_with_repeat_supplementary/introduction-block.page"; +import ListCollectorEmployeesPage from "../../generated_pages/hub_section_required_with_repeat_supplementary/list-collector-employees.page.js"; +import LengthOfEmploymentPage from "../../generated_pages/hub_section_required_with_repeat_supplementary/length-of-employment.page.js"; +import Section3Page from "../../generated_pages/hub_section_required_with_repeat_supplementary/section-3-summary.page.js"; + +describe("Feature: Hub and Spoke", () => { + const hubAndSpokeSchema = "test_hub_and_spoke.json"; + + describe("Given I am completing the test_hub_context schema,", () => { + beforeEach("load the survey", async () => { + await browser.openQuestionnaire(hubAndSpokeSchema); + }); + + it("When a user first views the Hub, The Hub should be in a continue state", async () => { + await expect(await $(HubPage.submit()).getText()).toBe("Continue"); + await expect(await $(HubPage.heading()).getText()).toBe("Choose another section to complete"); + await expect(await $(HubPage.summaryRowState("employment-section")).getText()).toBe("Not started"); + await expect(await $(HubPage.summaryRowState("accommodation-section")).getText()).toBe("Not started"); + await expect(await $(HubPage.summaryRowState("household-section")).getText()).toBe("Not started"); + }); + + it("When a user utilises a screen reader, The visually hidden text read aloud should be the state and name of each section in the hub", async () => { + await expect(await $(HubPage.summaryRowLink("employment-section")).getHTML()).toContain("Start section: Employment"); + await expect(await $(HubPage.summaryRowLink("accommodation-section")).getHTML()).toContain("Start section: Accommodation"); + await expect(await $(HubPage.summaryRowLink("household-section")).getHTML()).toContain("Start section: Household residents"); + }); + + it("When a user views the Hub, any section with show_on_hub set to true should appear", async () => { + await expect(await $(HubPage.summaryItems()).getText()).toContain("Employment"); + await expect(await $(HubPage.summaryItems()).getText()).toContain("Accommodation"); + await expect(await $(HubPage.summaryItems()).getText()).toContain("Household residents"); + }); + + it("When a user views the Hub, any section with show_on_hub set to false should not appear", async () => { + await expect(await $(HubPage.summaryItems()).getText()).not.toBe("Relationships"); + }); + + it("When the user click the 'Save and sign out' button then they should be redirected to the correct log out url", async () => { + await $(HubPage.saveSignOut()).click(); + await verifyUrlContains("/signed-out"); + }); + + it("When a user views the Hub, Then the page title should be Choose another section to complete", async () => { + const pageTitle = await browser.getTitle(); + await expect(pageTitle).toBe("Choose another section to complete - Hub & Spoke"); + }); + }); + + describe("Given a user has not started a section", () => { + beforeEach("Open survey", async () => { + await browser.openQuestionnaire(hubAndSpokeSchema); + await expect(await $(HubPage.summaryRowState("employment-section")).getText()).toBe("Not started"); + await expect(await $(HubPage.summaryRowState("accommodation-section")).getText()).toBe("Not started"); + await expect(await $(HubPage.summaryRowState("household-section")).getText()).toBe("Not started"); + await expect(await $(HubPage.summaryRowLink("employment-section")).getHTML()).toContain("Start section: Employment"); + await expect(await $(HubPage.summaryRowLink("accommodation-section")).getHTML()).toContain("Start section: Accommodation"); + await expect(await $(HubPage.summaryRowLink("household-section")).getHTML()).toContain("Start section: Household residents"); + }); + + it("When the user starts a section, Then the first question in the section should be displayed", async () => { + await click(HubPage.submit()); + await verifyUrlContains(EmploymentStatusBlockPage.url()); + }); + + it("When the user starts a section and clicks the Previous link on the first question, Then they should be taken back to the Hub", async () => { + await click(HubPage.submit()); + await $(EmploymentStatusBlockPage.previous()).click(); + await verifyUrlPathIs(HubPage.url()); + }); + }); + + describe("Given a user has started a section", () => { + before("Start section", async () => { + await browser.openQuestionnaire(hubAndSpokeSchema); + await $(HubPage.summaryRowLink("employment-section")).click(); + await $(EmploymentStatusBlockPage.exclusiveNoneOfTheseApply()).click(); + await click(EmploymentStatusBlockPage.submit()); + }); + + it("When the user returns to the Hub, Then the Hub should be in a continue state", async () => { + await browser.url(HubPage.url()); + await expect(await $(HubPage.submit()).getText()).toBe("Continue"); + await expect(await $(HubPage.heading()).getText()).toBe("Choose another section to complete"); + }); + + it("When the user returns to the Hub, Then the section should be marked as 'Partially completed'", async () => { + await browser.url(HubPage.url()); + await expect(await $(HubPage.summaryRowState("employment-section")).getText()).toBe("Partially completed"); + await expect(await $(HubPage.summaryRowLink("employment-section")).getHTML()).toContain("Continue with section: Employment"); + }); + + it("When the user returns to the Hub and restarts the same section, Then they should be redirected to the first incomplete block", async () => { + await browser.url(HubPage.url()); + await $(HubPage.summaryRowLink("employment-section")).click(); + await verifyUrlContains(EmploymentTypeBlockPage.url()); + }); + }); + + describe("Given a user has completed a section", () => { + beforeEach("Complete section", async () => { + await browser.openQuestionnaire(hubAndSpokeSchema); + await $(HubPage.summaryRowLink("employment-section")).click(); + await $(EmploymentStatusBlockPage.exclusiveNoneOfTheseApply()).click(); + await click(EmploymentStatusBlockPage.submit()); + await $(EmploymentTypeBlockPage.studying()).click(); + }); + + it("When the user clicks the 'Continue' button, it should return them to the hub", async () => { + await click(EmploymentTypeBlockPage.submit()); + await verifyUrlPathIs(HubPage.url()); + }); + + it("When the user returns to the Hub, Then the Hub should be in a continue state", async () => { + await click(EmploymentTypeBlockPage.submit()); + await expect(await $(HubPage.submit()).getText()).toBe("Continue"); + await expect(await $(HubPage.heading()).getText()).toBe("Choose another section to complete"); + }); + + it("When the user returns to the Hub, Then the section should be marked as 'Completed'", async () => { + await click(EmploymentTypeBlockPage.submit()); + await expect(await $(HubPage.summaryRowState("employment-section")).getText()).toBe("Completed"); + await expect(await $(HubPage.summaryRowLink("employment-section")).getHTML()).toContain("View answers: Employment"); + }); + + it("When the user returns to the Hub and clicks the 'View answers' link on the Hub, if this no summary they are returned to the first block", async () => { + await click(EmploymentTypeBlockPage.submit()); + await $(HubPage.summaryRowLink("employment-section")).click(); + await verifyUrlContains(EmploymentStatusBlockPage.url()); + }); + + it("When the user returns to the Hub and continues, Then they should progress to the next section", async () => { + await click(EmploymentTypeBlockPage.submit()); + await verifyUrlContains(HubPage.url()); + await click(HubPage.submit()); + await verifyUrlContains(ProxyPage.url()); + }); + }); + + describe("Given a user has completed a section and is on the Hub page", () => { + beforeEach("Complete section", async () => { + await browser.openQuestionnaire(hubAndSpokeSchema); + await $(HubPage.summaryRowLink("employment-section")).click(); + await $(EmploymentStatusBlockPage.workingAsAnEmployee()).click(); + await click(EmploymentStatusBlockPage.submit()); + + await expect(await $(HubPage.summaryRowState("employment-section")).getText()).toBe("Completed"); + }); + + it("When the user clicks the 'View answers' link and incompletes the section, Then they the should be taken to the next incomplete question on 'Continue", async () => { + await $(HubPage.summaryRowLink("employment-section")).click(); + await verifyUrlContains(EmploymentStatusBlockPage.url()); + await $(EmploymentStatusBlockPage.exclusiveNoneOfTheseApply()).click(); + await click(EmploymentStatusBlockPage.submit()); + await verifyUrlContains(EmploymentTypeBlockPage.url()); + }); + + it("When the user clicks the 'View answers' link and incompletes the section and returns to the hub, Then the section should be marked as 'Partially completed'", async () => { + await $(HubPage.summaryRowLink("employment-section")).click(); + await verifyUrlContains(EmploymentStatusBlockPage.url()); + await $(EmploymentStatusBlockPage.exclusiveNoneOfTheseApply()).click(); + await click(EmploymentStatusBlockPage.submit()); + await browser.url(HubPage.url()); + await verifyUrlPathIs(HubPage.url()); + await expect(await $(HubPage.summaryRowState("employment-section")).getText()).toBe("Partially completed"); + await expect(await $(HubPage.summaryRowLink("employment-section")).getHTML()).toContain("Continue with section: Employment"); + }); + }); + + describe("Given a user has completed all sections", () => { + beforeEach("Complete all sections", async () => { + await browser.openQuestionnaire(hubAndSpokeSchema); + await $(HubPage.summaryRowLink("employment-section")).click(); + await $(EmploymentStatusBlockPage.exclusiveNoneOfTheseApply()).click(); + await click(EmploymentStatusBlockPage.submit()); + await $(EmploymentTypeBlockPage.studying()).click(); + await click(EmploymentTypeBlockPage.submit()); + await click(HubPage.submit()); + await $(ProxyPage.yes()).click(); + await click(ProxyPage.submit()); + await click(AccomodationDetailsSummaryBlockPage.submit()); + await click(HubPage.submit()); + await $(DoesAnyoneLiveHere.no()).click(); + await click(DoesAnyoneLiveHere.submit()); + await click(HouseholdSummary.submit()); + await click(HubPage.submit()); + await $(AnyoneRelated.yes()).click(); + await click(AnyoneRelated.submit()); + await click(RelationshipsSummary.submit()); + }); + + it("It should return them to the hub", async () => { + await verifyUrlPathIs(HubPage.url()); + }); + + it("When the user returns to the Hub, Then the Hub should be in a completed state", async () => { + await expect(await $(HubPage.submit()).getText()).toBe("Submit survey"); + await expect(await $(HubPage.heading()).getText()).toBe("Submit survey"); + }); + + it("When the user submits, it should show the thankyou page", async () => { + await click(HubPage.submit()); + await verifyUrlContains("thank-you"); + }); + }); + + describe("Given a user opens a schema with required sections", () => { + beforeEach("Load survey", async () => { + await browser.openQuestionnaire("test_hub_complete_sections.json"); + }); + + it("The hub should not show first of all", async () => { + await verifyUrlContains(EmploymentStatusBlockPage.url()); + }); + + it("The hub should only display when required sections are complete", async () => { + await $(EmploymentStatusBlockPage.exclusiveNoneOfTheseApply()).click(); + await click(EmploymentStatusBlockPage.submit()); + await $(EmploymentTypeBlockPage.studying()).click(); + await click(EmploymentTypeBlockPage.submit()); + await verifyUrlContains(HubPage.url()); + }); + }); + + describe("Given a user opens a schema with hub required sections based on a repeating section", () => { + beforeEach("Load survey", async () => { + await browser.openQuestionnaire("test_hub_section_required_with_repeat.json"); + }); + + it("When all the repeating sections are complete, Then the hub should be displayed", async () => { + await $(PrimaryPersonListCollectorPage.yes()).click(); + await $(PrimaryPersonListCollectorPage.submit()).click(); + await $(PrimaryPersonListCollectorAddPage.firstName()).setValue("Marcus"); + await $(PrimaryPersonListCollectorAddPage.lastName()).setValue("Twin"); + await click(PrimaryPersonListCollectorAddPage.submit()); + await $(ListCollectorPage.yes()).click(); + await $(ListCollectorPage.submit()).click(); + await $(ListCollectorAddPage.firstName()).setValue("John"); + await $(ListCollectorAddPage.lastName()).setValue("Doe"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.no()).click(); + await $(ListCollectorPage.submit()).click(); + await click(ListCollectorSectionSummaryPage.submit()); + + // Try to access the hub + await browser.url(HubPage.url()); + + // Redirected to the repeating sections to be completed + await $(ProxyRepeatPage.yes()).click(); + await $(ProxyRepeatPage.submit()).click(); + await $(DateOfBirthPage.day()).setValue(12); + await $(DateOfBirthPage.month()).setValue(4); + await $(DateOfBirthPage.year()).setValue(2021); + await click(DateOfBirthPage.submit()); + await $(RepeatingSummaryPage.submit()).click(); + await $(ProxyRepeatPage.yes()).click(); + await $(ProxyRepeatPage.submit()).click(); + await $(DateOfBirthPage.day()).setValue(1); + await $(DateOfBirthPage.month()).setValue(1); + await $(DateOfBirthPage.year()).setValue(2000); + await $(RepeatingSummaryPage.submit()).click(); + await verifyUrlContains(HubPage.url()); + }); + + it("When the repeating sections are incomplete, Then the hub should not be displayed", async () => { + await $(PrimaryPersonListCollectorPage.yes()).click(); + await $(PrimaryPersonListCollectorPage.submit()).click(); + await $(PrimaryPersonListCollectorAddPage.firstName()).setValue("Marcus"); + await $(PrimaryPersonListCollectorAddPage.lastName()).setValue("Twin"); + await click(PrimaryPersonListCollectorAddPage.submit()); + await $(ListCollectorPage.no()).click(); + await $(ListCollectorPage.submit()).click(); + await click(ListCollectorSectionSummaryPage.submit()); + + // Don't complete all the repeating questions + await $(ProxyRepeatPage.yes()).click(); + await $(ProxyRepeatPage.submit()).click(); + + await browser.url(HubPage.url()); + await verifyUrlContains("date-of-birth"); + }); + }); + + describe("Given a user opens a schema with hub required sections based on a repeating section using supplementary data", () => { + beforeEach("Load survey", async () => { + const responseId = getRandomString(16); + + await browser.openQuestionnaire("test_hub_section_required_with_repeat_supplementary.json.json", { + version: "v2", + sdsDatasetId: "203b2f9d-c500-8175-98db-86ffcfdccfa3", + responseId, + }); + }); + + it("When all the repeating sections are complete, Then the hub should be displayed", async () => { + await click(LoadedSuccessfullyBlockPage.submit()); + await click(IntroductionBlockPage.submit()); + + // Complete the repeating sections using supplementary data + await click(ListCollectorEmployeesPage.submit()); + await $(LengthOfEmploymentPage.day()).setValue(1); + await $(LengthOfEmploymentPage.month()).setValue(1); + await $(LengthOfEmploymentPage.year()).setValue(1930); + await click(LengthOfEmploymentPage.submit()); + await click(Section3Page.submit()); + await $(LengthOfEmploymentPage.day()).setValue(1); + await $(LengthOfEmploymentPage.month()).setValue(1); + await $(LengthOfEmploymentPage.year()).setValue(1930); + await click(LengthOfEmploymentPage.submit()); + await click(Section3Page.submit()); + await verifyUrlContains(HubPage.url()); + }); + + it("When the repeating sections are incomplete. Then the hub should not be displayed", async () => { + await click(LoadedSuccessfullyBlockPage.submit()); + await click(IntroductionBlockPage.submit()); + + // Don't complete the repeating sections that use supplementary data + await click(ListCollectorEmployeesPage.submit()); + await $(LengthOfEmploymentPage.day()).setValue(1); + await $(LengthOfEmploymentPage.month()).setValue(1); + await $(LengthOfEmploymentPage.year()).setValue(1930); + await click(LengthOfEmploymentPage.submit()); + await click(Section3Page.submit()); + + await browser.url(HubPage.url()); + await verifyUrlContains("length-of-employment"); + }); + }); + + describe("Given a section is complete and the user has been returned to a section summary by clicking the 'View answers' link ", () => { + beforeEach("Complete section", async () => { + await browser.openQuestionnaire(hubAndSpokeSchema); + await $(HubPage.summaryRowLink("household-section")).click(); + await $(DoesAnyoneLiveHere.no()).click(); + await click(DoesAnyoneLiveHere.submit()); + await click(HouseholdSummary.submit()); + await expect(await $(HubPage.summaryRowLink("household-section")).getHTML()).toContain("View answers: Household residents"); + }); + + it("When there are no changes, continue returns directly to the hub", async () => { + await $(HubPage.summaryRowLink("household-section")).click(); + await click(HouseholdSummary.submit()); + await verifyUrlPathIs(HubPage.url()); + await expect(await $(HubPage.summaryRowLink("household-section")).getHTML()).toContain("View answers: Household residents"); + }); + + it("When there are changes, which would set the section to in_progress it routes accordingly", async () => { + await $(HubPage.summaryRowLink("household-section")).click(); + await $(HouseholdSummary.doesAnyoneLiveHereAnswerEdit()).click(); + await $(DoesAnyoneLiveHere.yes()).click(); + await click(DoesAnyoneLiveHere.submit()); + await click(HouseholdSummary.submit()); + await verifyUrlContains(HowManyPeopleLiveHere.url()); + }); + }); +}); + +async function verifyUrlPathIs(expectedUrlPath) { + // Hub and Spoke URLs are "/questionnaire/", so we need strict checking of the URL path + const actualUrlPath = new URL(await browser.getUrl()).pathname; + await expect(actualUrlPath).toBe(expectedUrlPath); +} diff --git a/tests/functional/spec/interstitial_definition.spec.js b/tests/functional/spec/interstitial_definition.spec.js index 2e1d97855b..2f40919e90 100644 --- a/tests/functional/spec/interstitial_definition.spec.js +++ b/tests/functional/spec/interstitial_definition.spec.js @@ -2,42 +2,20 @@ import InterstitialDefinitionPage from "../generated_pages/interstitial_definiti describe("Component: Interstitial Definition", () => { describe("Given I launch the interstitial definition questionnaire", () => { - before(() => { - browser.openQuestionnaire("test_interstitial_definition.json"); + before(async () => { + await browser.openQuestionnaire("test_interstitial_definition.json"); }); - it("When there is a definition on an interstitial, then the page is displayed correctly", () => { - expect($(InterstitialDefinitionPage.definitionTitle(1)).isDisplayed()).to.be.true; - expect($(InterstitialDefinitionPage.definitionContent(1)).isDisplayed()).to.be.false; - expect($(InterstitialDefinitionPage.definitionButton(1)).isDisplayed()).to.be.false; - - expect($(InterstitialDefinitionPage.definitionTitle(2)).isDisplayed()).to.be.true; - expect($(InterstitialDefinitionPage.definitionContent(2)).isDisplayed()).to.be.false; - expect($(InterstitialDefinitionPage.definitionButton(2)).isDisplayed()).to.be.false; - }); - - it("When I click on a definition title, the content and button is display for just that definition", () => { - $(InterstitialDefinitionPage.definitionTitle(1)).click(); - - expect($(InterstitialDefinitionPage.definitionTitle(1)).isDisplayed()).to.be.true; - expect($(InterstitialDefinitionPage.definitionContent(1)).isDisplayed()).to.be.true; - expect($(InterstitialDefinitionPage.definitionButton(1)).isDisplayed()).to.be.true; - - expect($(InterstitialDefinitionPage.definitionTitle(2)).isDisplayed()).to.be.true; - expect($(InterstitialDefinitionPage.definitionContent(2)).isDisplayed()).to.be.false; - expect($(InterstitialDefinitionPage.definitionButton(2)).isDisplayed()).to.be.false; + it("When there is a definition on an interstitial, then the page is displayed correctly", async () => { + await expect(await $(InterstitialDefinitionPage.definitionTitle()).isDisplayed()).toBe(true); + await expect(await $(InterstitialDefinitionPage.definitionContent()).getText()).toBe(""); }); - it("When I click on the hide content button, then the page is displayed correctly", () => { - $(InterstitialDefinitionPage.definitionButton(1)).click(); - - expect($(InterstitialDefinitionPage.definitionTitle(1)).isDisplayed()).to.be.true; - expect($(InterstitialDefinitionPage.definitionContent(1)).isDisplayed()).to.be.false; - expect($(InterstitialDefinitionPage.definitionButton(1)).isDisplayed()).to.be.false; + it("When I click on a definition title, the content is displayed for just that definition", async () => { + await $(InterstitialDefinitionPage.definitionTitle()).click(); - expect($(InterstitialDefinitionPage.definitionTitle(2)).isDisplayed()).to.be.true; - expect($(InterstitialDefinitionPage.definitionContent(2)).isDisplayed()).to.be.false; - expect($(InterstitialDefinitionPage.definitionButton(2)).isDisplayed()).to.be.false; + await expect(await $(InterstitialDefinitionPage.definitionTitle()).isDisplayed()).toBe(true); + await expect(await $(InterstitialDefinitionPage.definitionContent()).getText()).toBe("In a way that accomplishes a desired aim or result"); }); }); }); diff --git a/tests/functional/spec/interviewer_note.spec.js b/tests/functional/spec/interviewer_note.spec.js index 0130e9a3b0..ecb36a8520 100644 --- a/tests/functional/spec/interviewer_note.spec.js +++ b/tests/functional/spec/interviewer_note.spec.js @@ -2,27 +2,28 @@ import ConfirmPage from "../generated_pages/interviewer_note/confirm-block.page. import FavouriteTeamPage from "../generated_pages/interviewer_note/favourite-team-block.page.js"; import FinalInterstitialPage from "../generated_pages/interviewer_note/final-interstitial-block.page.js"; import InitialInterstitialPage from "../generated_pages/interviewer_note/initial-interstitial-block.page.js"; +import { click } from "../helpers"; describe("Given I start a survey", () => { - before(() => { - browser.openQuestionnaire("test_interviewer_note.json"); + before(async () => { + await browser.openQuestionnaire("test_interviewer_note.json"); }); - it("When I view interstitial page and the interviewer_note is set to true then I should be able to see interviewer note", () => { - expect($(InitialInterstitialPage.questionText()).getText()).to.contain("Interviewer note"); + it("When I view interstitial page and the interviewer_note is set to true then I should be able to see interviewer note", async () => { + await expect(await $(InitialInterstitialPage.questionText()).getText()).toContain("Interviewer note"); }); - it("When I view question page and the interviewer_note is set to true then I should be able to see interviewer note", () => { - $(InitialInterstitialPage.submit()).click(); - expect($(FavouriteTeamPage.questionText()).getText()).to.contain("Interviewer note"); + it("When I view question page and the interviewer_note is set to true then I should be able to see interviewer note", async () => { + await click(InitialInterstitialPage.submit()); + await expect(await $(FavouriteTeamPage.questionText()).getText()).toContain("Interviewer note"); }); - it("When I view question page and the interviewer_note is set to false then I should not be able to see interviewer note", () => { - $(FavouriteTeamPage.favouriteTeam()).setValue("TNS"); - $(FavouriteTeamPage.submit()).click(); - expect($(ConfirmPage.questionText()).getText()).to.not.contain("Interviewer note"); + it("When I view question page and the interviewer_note is set to false then I should not be able to see interviewer note", async () => { + await $(FavouriteTeamPage.favouriteTeam()).setValue("TNS"); + await click(FavouriteTeamPage.submit()); + await expect(await $(ConfirmPage.questionText()).getText()).not.toBe("Interviewer note"); }); - it("When I view interstitial page and the interviewer_note is not set then I should not be able to see interviewer note", () => { - $(ConfirmPage.yes()).click(); - $(ConfirmPage.submit()).click(); - expect($(FinalInterstitialPage.questionText()).getText()).to.not.contain("Interviewer note"); + it("When I view interstitial page and the interviewer_note is not set then I should not be able to see interviewer note", async () => { + await $(ConfirmPage.yes()).click(); + await click(ConfirmPage.submit()); + await expect(await $(FinalInterstitialPage.questionText()).getText()).not.toBe("Interviewer note"); }); }); diff --git a/tests/functional/spec/introduction.spec.js b/tests/functional/spec/introduction.spec.js index 54fbcbb713..b00bef77bd 100644 --- a/tests/functional/spec/introduction.spec.js +++ b/tests/functional/spec/introduction.spec.js @@ -2,28 +2,34 @@ import IntroductionPage from "../generated_pages/introduction/introduction.page" describe("Introduction page", () => { const introductionSchema = "test_introduction.json"; - beforeEach(() => { - browser.openQuestionnaire(introductionSchema); + beforeEach(async () => { + await browser.openQuestionnaire(introductionSchema); }); - it("Given I start a survey, When I view the introduction page, Then I should be able to see introduction information", () => { - browser.openQuestionnaire(introductionSchema); - expect($(IntroductionPage.useOfData()).getText()).to.contain("How we use your data"); - expect($(IntroductionPage.useOfInformation()).getText()).to.contain( - "Data should relate to all sites in England, Scotland and Wales unless otherwise stated." + it("Given I start a survey, When I view the introduction page, Then I should be able to see introduction information", async () => { + await browser.openQuestionnaire(introductionSchema); + await expect(await $(IntroductionPage.useOfData()).getText()).toContain("How we use your data"); + await expect(await $(IntroductionPage.useOfInformation()).getText()).toContain( + "Data should relate to all sites in England, Scotland and Wales unless otherwise stated.", ); - expect($(IntroductionPage.legalResponse()).getText()).to.contain("Your response is legally required"); - expect($(IntroductionPage.legalBasis()).getText()).to.contain("Notice is given under section 999 of the Test Act 2000"); - expect($(IntroductionPage.introDescription()).getText()).to.contain( - "To take part, all you need to do is check that you have the information you need to answer the survey questions." + await expect(await $(IntroductionPage.legalResponse()).getText()).toBe("Your response is legally required"); + await expect(await $(IntroductionPage.legalBasis()).getText()).toBe("Notice is given under section 999 of the Test Act 2000"); + await expect(await $(IntroductionPage.introDescription()).getText()).toBe( + "To take part, all you need to do is check that you have the information you need to answer the survey questions.", ); }); - it("Given I start a survey with introduction guidance set, When I view the introduction page, Then I should be able to see introduction guidance", () => { - browser.openQuestionnaire(introductionSchema); - expect($(IntroductionPage.guidancePanel(1)).isDisplayed()).to.be.true; - expect($(IntroductionPage.guidancePanel(1)).getText()).to.contain("Coronavirus (COVID-19) guidance"); - expect($(IntroductionPage.guidancePanel(1)).getText()).to.contain( - "Explain your figures in the comment section to minimise us contacting you and to help us tell an industry story" + it("Given I start a survey, When preview content is set on the introduction page, Then the content headings should be displayed at the correct level", async () => { + await browser.openQuestionnaire(introductionSchema); + const introQuestionH3Selector = `${IntroductionPage.introQuestion()} h3`; + const h3Exists = await $(introQuestionH3Selector).isExisting(); + await expect(h3Exists).toBe(true); + }); + it("Given I start a survey with introduction guidance set, When I view the introduction page, Then I should be able to see introduction guidance", async () => { + await browser.openQuestionnaire(introductionSchema); + await expect(await $(IntroductionPage.guidancePanel(1)).isDisplayed()).toBe(true); + await expect(await $(IntroductionPage.guidancePanel(1)).getText()).toContain("Coronavirus (COVID-19) guidance"); + await expect(await $(IntroductionPage.guidancePanel(1)).getText()).toContain( + "Explain your figures in the comment section to minimise us contacting you and to help us tell an industry story", ); }); }); diff --git a/tests/functional/spec/journeys/enabled-sections/enabled_section_checkbox.spec.js b/tests/functional/spec/journeys/enabled-sections/enabled_section_checkbox.spec.js new file mode 100644 index 0000000000..aaddfb2588 --- /dev/null +++ b/tests/functional/spec/journeys/enabled-sections/enabled_section_checkbox.spec.js @@ -0,0 +1,42 @@ +import sectionOne from "../../../generated_pages/section_enabled_checkbox/section-1-block.page"; +import sectionTwo from "../../../generated_pages/section_enabled_checkbox/section-2-block.page"; +import SubmitPage from "../../../generated_pages/section_enabled_checkbox/submit.page"; +import { click, verifyUrlContains } from "../../../helpers"; +describe("Feature: Section Enabled Based On Checkbox Answers", () => { + beforeEach("Open survey", async () => { + await browser.openQuestionnaire("test_section_enabled_checkbox.json"); + }); + + it("When the user selects `Section 2` and submits, Then section 2 should be displayed", async () => { + await $(sectionOne.section1Section2()).click(); + await click(sectionOne.submit()); + + await verifyUrlContains("section-2-block"); + }); + + it("When the user selects `Section 3` and submits, Then section 2 should not be displayed and section 3 should be displayed", async () => { + await $(sectionOne.section1Section3()).click(); + await click(sectionOne.submit()); + + await verifyUrlContains("section-3-block"); + }); + + it("When the user selects `Section 2` and `Section 3` and submits, Then section 2 and section 3 should be displayed", async () => { + await $(sectionOne.section1Section2()).click(); + await $(sectionOne.section1Section3()).click(); + await click(sectionOne.submit()); + + await verifyUrlContains("section-2-block"); + await click(sectionTwo.submit()); + await verifyUrlContains("section-3-block"); + }); + + it("When the user selects `Neither` and submits, Then they should be taken straight to the summary", async () => { + await $(sectionOne.section1ExclusiveNeither()).click(); + await click(sectionOne.submit()); + + await verifyUrlContains(SubmitPage.url()); + await expect(await $(SubmitPage.section2Question()).isExisting()).toBe(false); + await expect(await $(SubmitPage.section3Question()).isExisting()).toBe(false); + }); +}); diff --git a/tests/functional/spec/journeys/enabled-sections/enabled_section_hub.spec.js b/tests/functional/spec/journeys/enabled-sections/enabled_section_hub.spec.js new file mode 100644 index 0000000000..07ba9d7f92 --- /dev/null +++ b/tests/functional/spec/journeys/enabled-sections/enabled_section_hub.spec.js @@ -0,0 +1,51 @@ +import sectionOne from "../../../generated_pages/section_enabled_hub/section-1-block.page"; +import hubPage from "../../../base_pages/hub.page"; +import { click } from "../../../helpers"; +describe("Feature: Section Enabled With Hub", () => { + beforeEach("Open survey", async () => { + await browser.openQuestionnaire("test_section_enabled_hub.json"); + }); + + it("When the user selects `Section 2` and submits, Then only section 2 should be displayed on the hub", async () => { + await $(sectionOne.section1Section2()).click(); + await click(sectionOne.submit()); + + await expect(await $(hubPage.summaryRowState("section-2")).isDisplayed()).toBe(true); + await expect(await $(hubPage.summaryRowTitle("section-2")).getText()).toBe("Section 2"); + + await expect(await $(hubPage.summaryRowState("section-3")).isDisplayed()).toBe(false); + }); + + it("When the user selects `Section 3` and submits, Then section 2 should not be displayed and section 3 should be displayed", async () => { + await $(sectionOne.section1Section3()).click(); + await click(sectionOne.submit()); + + await expect(await $(hubPage.summaryRowState("section-3")).isDisplayed()).toBe(true); + await expect(await $(hubPage.summaryRowTitle("section-3")).getText()).toBe("Section 3"); + + await expect(await $(hubPage.summaryRowState("section-2")).isDisplayed()).toBe(false); + }); + + it("When the user selects `Section 2` and `Section 3` and submits, Then section 2 and section 3 should be displayed", async () => { + await $(sectionOne.section1Section2()).click(); + await $(sectionOne.section1Section3()).click(); + await click(sectionOne.submit()); + + await expect(await $(hubPage.summaryRowState("section-2")).isDisplayed()).toBe(true); + await expect(await $(hubPage.summaryRowTitle("section-2")).getText()).toBe("Section 2"); + + await expect(await $(hubPage.summaryRowState("section-3")).isDisplayed()).toBe(true); + await expect(await $(hubPage.summaryRowTitle("section-3")).getText()).toBe("Section 3"); + }); + + it("When the user selects `Neither` and submits, Then hub should not display any other section and should be in the `Completed` state.", async () => { + await $(sectionOne.section1ExclusiveNeither()).click(); + await click(sectionOne.submit()); + + await expect(await $(hubPage.summaryRowState("section-2")).isDisplayed()).toBe(false); + await expect(await $(hubPage.summaryRowState("section-3")).isDisplayed()).toBe(false); + + await expect(await $(hubPage.submit()).getText()).toBe("Submit survey"); + await expect(await $(hubPage.heading()).getText()).toBe("Submit survey"); + }); +}); diff --git a/tests/functional/spec/journeys/enabled-sections/enabled_section_radio.spec.js b/tests/functional/spec/journeys/enabled-sections/enabled_section_radio.spec.js new file mode 100644 index 0000000000..cd18bcaf63 --- /dev/null +++ b/tests/functional/spec/journeys/enabled-sections/enabled_section_radio.spec.js @@ -0,0 +1,41 @@ +import sectionOne from "../../../generated_pages/section_enabled_radio/section-1-block.page"; +import SubmitPage from "../../../generated_pages/section_enabled_radio/submit.page"; +import { click, verifyUrlContains } from "../../../helpers"; +describe("Feature: Section Enabled Based On Radio Answers", () => { + beforeEach("Open survey", async () => { + await browser.openQuestionnaire("test_section_enabled_radio.json"); + }); + + it("When the user answers `Yes, enable section 2` and submits, Then section 2 should be displayed", async () => { + await $(sectionOne.yesEnableSection2()).click(); + await click(sectionOne.submit()); + + await verifyUrlContains("section-2-block"); + }); + + it("When the user answers `No, disable section 2` and submits, Then they should be taking straight to the summary", async () => { + await $(sectionOne.noDisableSection2()).click(); + await click(sectionOne.submit()); + + await verifyUrlContains(SubmitPage.url()); + await expect(await $(SubmitPage.section2Question()).isExisting()).toBe(false); + }); + + describe("Given that section 2 is enabled", () => { + beforeEach("Enable section 2", async () => { + await $(sectionOne.yesEnableSection2()).click(); + await click(sectionOne.submit()); + + await verifyUrlContains("section-2-block"); + }); + + it("When the user changes the answers and disables section 2, Then they should be taken straight to the summary", async () => { + await browser.back(); + await verifyUrlContains("section-1-block"); + + await $(sectionOne.noDisableSection2()).click(); + await click(sectionOne.submit()); + await verifyUrlContains(SubmitPage.url()); + }); + }); +}); diff --git a/tests/functional/spec/journeys/hub_and_spoke/choose_another_section.spec.js b/tests/functional/spec/journeys/hub_and_spoke/choose_another_section.spec.js new file mode 100644 index 0000000000..83e92c5230 --- /dev/null +++ b/tests/functional/spec/journeys/hub_and_spoke/choose_another_section.spec.js @@ -0,0 +1,33 @@ +import EmploymentStatusBlockPage from "../../../generated_pages/hub_and_spoke/employment-status.page.js"; +import ProxyPage from "../../../generated_pages/hub_and_spoke/proxy.page.js"; +import HubPage from "../../../base_pages/hub.page.js"; +import { click } from "../../../helpers"; +describe("Choose another section link", () => { + it("When a user first views the Hub, then the link should not be displayed", async () => { + await browser.openQuestionnaire("test_hub_and_spoke.json"); + await expect(await $("body").getText()).not.toBe("Choose another section and return to this later"); + }); + + it("When a user views the first question and the hub is not available, then the link should not be displayed", async () => { + await browser.openQuestionnaire("test_hub_complete_sections.json"); + await expect(await $("body").getText()).not.toBe("Choose another section and return to this later"); + }); + + it("When a user starts a new section and the hub is available, then the link should be displayed", async () => { + await browser.openQuestionnaire("test_hub_complete_sections.json"); + await $(EmploymentStatusBlockPage.workingAsAnEmployee()).click(); + await click(EmploymentStatusBlockPage.submit()); + await $(HubPage.summaryRowLink("accommodation-section")).click(); + await expect(await $("body").getText()).toContain("Choose another section and return to this later"); + }); + + it("When a user gets to a section summary and the hub is available, then the link should not be displayed", async () => { + await browser.openQuestionnaire("test_hub_complete_sections.json"); + await $(EmploymentStatusBlockPage.workingAsAnEmployee()).click(); + await click(EmploymentStatusBlockPage.submit()); + await $(HubPage.summaryRowLink("accommodation-section")).click(); + await $(ProxyPage.noIMAnsweringForMyself()).click(); + await click(ProxyPage.submit()); + await expect(await $("body").getText()).not.toBe("Choose another section and return to this later"); + }); +}); diff --git a/tests/functional/spec/journeys/hub_and_spoke/hub_and_spoke_custom_content.spec.js b/tests/functional/spec/journeys/hub_and_spoke/hub_and_spoke_custom_content.spec.js new file mode 100644 index 0000000000..ad1509d6ef --- /dev/null +++ b/tests/functional/spec/journeys/hub_and_spoke/hub_and_spoke_custom_content.spec.js @@ -0,0 +1,32 @@ +import HouseholdSummary from "../../../generated_pages/hub_and_spoke_custom_content/household-section-summary.page.js"; +import HowManyPeopleLiveHere from "../../../generated_pages/hub_and_spoke_custom_content/how-many-people-live-here.page.js"; +import DoesAnyoneLiveHere from "../../../generated_pages/hub_and_spoke_custom_content/does-anyone-live-here.page.js"; +import HubPage from "../../../base_pages/hub.page.js"; +import { click } from "../../../helpers"; +describe("Feature: Hub and Spoke with custom content", () => { + const hubAndSpokeSchema = "test_hub_and_spoke_custom_content.json"; + + it("When the questionnaire is incomplete, then custom content should be displayed correctly", async () => { + await browser.openQuestionnaire(hubAndSpokeSchema); + await expect(await $(HubPage.heading()).getText()).toBe("Choose another section to complete"); + await expect(await $(HubPage.guidance()).isExisting()).toBe(false); + await expect(await $(HubPage.summaryRowLink("household-section")).getHTML()).toContain("Start section: Household residents"); + await expect(await $(HubPage.submit()).getText()).toBe("Continue"); + await expect(await $(HubPage.warning()).isExisting()).toBe(false); + }); + + it("When the questionnaire is complete, then custom content should be displayed correctly", async () => { + await browser.openQuestionnaire(hubAndSpokeSchema); + await $(HubPage.summaryRowLink("household-section")).click(); + await $(DoesAnyoneLiveHere.yes()).click(); + await click(DoesAnyoneLiveHere.submit()); + await $(HowManyPeopleLiveHere.answer1()).click(); + await click(HowManyPeopleLiveHere.submit()); + await click(HouseholdSummary.submit()); + await expect(await $(HubPage.summaryRowLink("household-section")).getHTML()).toContain("View answers: Household residents"); + await expect(await $(HubPage.heading()).getText()).toBe("Submission title"); + await expect(await $(HubPage.guidance()).getText()).toBe("Submission guidance"); + await expect(await $(HubPage.submit()).getText()).toBe("Submission button"); + await expect(await $(HubPage.warning()).getText()).toBe("Submission warning"); + }); +}); diff --git a/tests/functional/spec/journeys/hub_and_spoke/hub_and_spoke_required_enable.spec.js b/tests/functional/spec/journeys/hub_and_spoke/hub_and_spoke_required_enable.spec.js new file mode 100644 index 0000000000..253ea4edb5 --- /dev/null +++ b/tests/functional/spec/journeys/hub_and_spoke/hub_and_spoke_required_enable.spec.js @@ -0,0 +1,21 @@ +import HouseholdRelationshipsBlockPage from "../../../generated_pages/hub_section_required_and_enabled/household-relationships-block.page"; +import RelationshipsCountPage from "../../../generated_pages/hub_section_required_and_enabled/relationships-count.page"; +import { SubmitPage } from "../../../base_pages/submit.page"; +import { click, verifyUrlContains } from "../../../helpers"; +describe("Hub and spoke section required and enabled", () => { + beforeEach("Load the survey", async () => { + await browser.openQuestionnaire("test_hub_section_required_and_enabled.json"); + }); + it("Given a relationship question in household, When I answer 'Yes', meaning the second section is enabled, Then I am routed to second section", async () => { + await $(HouseholdRelationshipsBlockPage.yes()).click(); + await click(HouseholdRelationshipsBlockPage.submit()); + await expect(await $(RelationshipsCountPage.legend()).getText()).toBe("How many people are related?"); + }); + it("Given a relationship question in household, When I answer 'No', Then I am redirected to the hub and can submit my answers without completing the other section", async () => { + await $(HouseholdRelationshipsBlockPage.no()).click(); + await click(HouseholdRelationshipsBlockPage.submit()); + await expect(await $("body").getText()).toContain("Submit survey"); + await click(SubmitPage.submit()); + await verifyUrlContains("thank-you"); + }); +}); diff --git a/tests/functional/spec/journeys/hub_and_spoke/previous.spec.js b/tests/functional/spec/journeys/hub_and_spoke/previous.spec.js new file mode 100644 index 0000000000..d3c3e66961 --- /dev/null +++ b/tests/functional/spec/journeys/hub_and_spoke/previous.spec.js @@ -0,0 +1,34 @@ +import EmploymentStatusBlockPage from "../../../generated_pages/hub_and_spoke/employment-status.page.js"; +import EmploymentTypePage from "../../../generated_pages/hub_and_spoke/employment-type.page.js"; +import HubPage from "../../../base_pages/hub.page.js"; +import ProxyPage from "../../../generated_pages/hub_and_spoke/proxy.page.js"; +import { click } from "../../../helpers"; +const schema = "test_hub_complete_sections.json"; +describe("Choose another section link", () => { + beforeEach(async () => { + await browser.openQuestionnaire(schema); + }); + + it("When a user gets to initial question, then the previous location link should not be displayed", async () => { + await expect(await $(EmploymentStatusBlockPage.previous()).isExisting()).toBe(false); + }); + + it("When a user gets to the hub, then the previous location link should not be displayed", async () => { + await $(EmploymentStatusBlockPage.workingAsAnEmployee()).click(); + await click(EmploymentStatusBlockPage.submit()); + await expect(await $(HubPage.previous()).isExisting()).toBe(false); + }); + + it("When a user gets to subsequent question, then the previous location link should be displayed", async () => { + await $(EmploymentStatusBlockPage.exclusiveNoneOfTheseApply()).click(); + await click(EmploymentStatusBlockPage.submit()); + await expect(await $(EmploymentTypePage.previous()).isExisting()).toBe(true); + }); + + it("When a user gets to subsequent questions past the hub, then the previous location link should be displayed", async () => { + await $(EmploymentStatusBlockPage.workingAsAnEmployee()).click(); + await click(EmploymentStatusBlockPage.submit()); + await $(HubPage.summaryRowLink("accommodation-section")).click(); + await expect(await $(ProxyPage.previous()).isExisting()).toBe(true); + }); +}); diff --git a/tests/functional/spec/journeys/progress/progress_value_source_blocks.js b/tests/functional/spec/journeys/progress/progress_value_source_blocks.js new file mode 100644 index 0000000000..a810d5e34b --- /dev/null +++ b/tests/functional/spec/journeys/progress/progress_value_source_blocks.js @@ -0,0 +1,193 @@ +import FirstQuestionPage from "../../../generated_pages/progress_value_source_blocks/s1-b1.page"; +import SecondQuestionPage from "../../../generated_pages/progress_value_source_blocks/s1-b2.page"; +import ThirdQuestionPage from "../../../generated_pages/progress_value_source_blocks/s1-b3.page"; +import ThirdQuestionSectionTwoPage from "../../../generated_pages/progress_value_source_section_enabled_no_hub/s2-b1.page"; +import FourthQuestionPage from "../../../generated_pages/progress_value_source_blocks/s1-b4.page"; +import FifthQuestionPage from "../../../generated_pages/progress_value_source_blocks/s1-b5.page"; +import SixthQuestionPage from "../../../generated_pages/progress_value_source_blocks/s1-b6.page"; +import SeventhQuestionPage from "../../../generated_pages/progress_value_source_blocks/s1-b7.page"; +import SubmitPage from "../../../generated_pages/progress_value_source_blocks/submit.page"; +import HubPage from "../../../base_pages/hub.page"; +import { click, verifyUrlContains } from "../../../helpers"; +describe("Feature: Routing based on progress value sources using block identifiers", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_progress_value_source_blocks.json"); + }); + + describe("Given I have routing based on the completeness of a block", () => { + it("When the block being evaluated is incomplete (Q2), Then the dependent question (Q4) should not be on the path or displayed on the summary", async () => { + await $(FirstQuestionPage.q1A1()).setValue("0"); + await click(FirstQuestionPage.submit()); + + await verifyUrlContains(ThirdQuestionPage.pageName); + await $(ThirdQuestionPage.q1A1()).setValue("1"); + await click(ThirdQuestionPage.submit()); + + await $(FifthQuestionPage.q1A1()).setValue("2"); + await click(FifthQuestionPage.submit()); + + await $(SeventhQuestionPage.q1A1()).setValue("3"); + await click(SeventhQuestionPage.submit()); + + await verifyUrlContains(SubmitPage.pageName); + await expect(await $("body").getText()).not.toBe("Section 1 Question 2"); + await expect(await $("body").getText()).not.toBe("Section 1 Question 4"); + }); + }); + + describe("Given I have routing based on the completeness of a block", () => { + it("When the blocks being evaluated are complete (Q2 + Q5), Then the dependent questions (Q4 + Q6) should be on the path and displayed on the summary", async () => { + await $(FirstQuestionPage.q1A1()).setValue("1"); + await click(FirstQuestionPage.submit()); + + await verifyUrlContains(SecondQuestionPage.pageName); + await $(SecondQuestionPage.q1A1()).setValue("1"); + await click(SecondQuestionPage.submit()); + + await $(ThirdQuestionPage.q1A1()).setValue("2"); + await click(ThirdQuestionPage.submit()); + + await verifyUrlContains(FourthQuestionPage.pageName); + await $(FourthQuestionPage.q1A1()).setValue("3"); + await click(FourthQuestionPage.submit()); + + await $(FifthQuestionPage.q1A1()).setValue("4"); + await click(FifthQuestionPage.submit()); + + await verifyUrlContains(SixthQuestionPage.pageName); + await $(SixthQuestionPage.q1A1()).setValue("5"); + await click(SixthQuestionPage.submit()); + + await $(SeventhQuestionPage.q1A1()).setValue("6"); + await click(SeventhQuestionPage.submit()); + + await verifyUrlContains(SubmitPage.pageName); + await expect(await $("body").getText()).toContain("Section 1 Question 4"); + await expect(await $("body").getText()).toContain("Section 1 Question 6"); + }); + }); + + describe("Given I have routing based on the completeness of a block", () => { + it("When an answer is changed so that the block being evaluated is completed, Then the dependent questions (Q4 + Q6) should be on the path and displayed on the summary", async () => { + await $(FirstQuestionPage.q1A1()).setValue("0"); + await click(FirstQuestionPage.submit()); + + await verifyUrlContains(ThirdQuestionPage.pageName); + await $(ThirdQuestionPage.q1A1()).setValue("1"); + await click(ThirdQuestionPage.submit()); + + await $(FifthQuestionPage.q1A1()).setValue("2"); + await click(FifthQuestionPage.submit()); + + await $(SeventhQuestionPage.q1A1()).setValue("3"); + await click(SeventhQuestionPage.submit()); + + await $(SubmitPage.s1B1Q1A1Edit()).click(); + await verifyUrlContains(FirstQuestionPage.pageName); + await $(FirstQuestionPage.q1A1()).setValue("1"); + await click(FirstQuestionPage.submit()); + + await verifyUrlContains(SecondQuestionPage.pageName); + await $(SecondQuestionPage.q1A1()).setValue("1"); + await click(SecondQuestionPage.submit()); + + await click(ThirdQuestionPage.submit()); + + await verifyUrlContains(FourthQuestionPage.pageName); + await $(FourthQuestionPage.q1A1()).setValue("3"); + await click(FourthQuestionPage.submit()); + + await click(FifthQuestionPage.submit()); + + await verifyUrlContains(SixthQuestionPage.pageName); + await $(SixthQuestionPage.q1A1()).setValue("3"); + await click(SixthQuestionPage.submit()); + + await verifyUrlContains(SubmitPage.pageName); + await expect(await $("body").getText()).toContain("Section 1 Question 4"); + await expect(await $("body").getText()).toContain("Section 1 Question 6"); + }); + }); + + describe("Given I have routing based on the completeness of a block", () => { + it("When an answer is removed form the path block being evaluated is no longer completed, Then the dependent questions (Q4 + Q6) should not be on the path and not be displayed on the summary", async () => { + await $(FirstQuestionPage.q1A1()).setValue("1"); + await click(FirstQuestionPage.submit()); + + await verifyUrlContains(SecondQuestionPage.pageName); + await $(SecondQuestionPage.q1A1()).setValue("1"); + await click(SecondQuestionPage.submit()); + + await $(ThirdQuestionPage.q1A1()).setValue("2"); + await click(ThirdQuestionPage.submit()); + + await verifyUrlContains(FourthQuestionPage.pageName); + await $(FourthQuestionPage.q1A1()).setValue("3"); + await click(FourthQuestionPage.submit()); + + await $(FifthQuestionPage.q1A1()).setValue("4"); + await click(FifthQuestionPage.submit()); + + await verifyUrlContains(SixthQuestionPage.pageName); + await $(SixthQuestionPage.q1A1()).setValue("5"); + await click(SixthQuestionPage.submit()); + + await $(SeventhQuestionPage.q1A1()).setValue("6"); + await click(SeventhQuestionPage.submit()); + + await verifyUrlContains(SubmitPage.pageName); + await $(SubmitPage.s1B1Q1A1Edit()).click(); + await verifyUrlContains(FirstQuestionPage.pageName); + await $(FirstQuestionPage.q1A1()).setValue("0"); + await click(FirstQuestionPage.submit()); + + await expect(await $("body").getText()).not.toBe("Section 1 Question 4"); + await expect(await $("body").getText()).not.toBe("Section 1 Question 6"); + }); + }); +}); + +describe("Feature: Section enabled based on progress value sources using block identifiers (no hub)", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_progress_value_source_section_enabled_no_hub.json"); + }); + + describe("Given I have a section enabled based on the completeness of a block", () => { + it("When the block being evaluated is complete, Then the dependent section should be enabled", async () => { + await $(FirstQuestionPage.q1A1()).setValue("0"); + await click(FirstQuestionPage.submit()); + await $(SecondQuestionPage.q1A1()).setValue("1"); + await click(SecondQuestionPage.submit()); + await verifyUrlContains(ThirdQuestionSectionTwoPage.pageName); + }); + }); +}); + +describe("Feature: Section enabled based on progress value sources using section identifiers", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_progress_value_source_section_enabled_hub.json"); + }); + + describe("Given I have a section enabled based on the completeness of another section", () => { + it("When the section being evaluated is complete, Then the dependent section should be enabled", async () => { + await click(HubPage.submit()); + await $(FirstQuestionPage.q1A1()).setValue("0"); + await click(FirstQuestionPage.submit()); + await $(SecondQuestionPage.q1A1()).setValue("1"); + await click(SecondQuestionPage.submit()); + await expect(await $(HubPage.summaryRowState("section-2")).getText()).toBe("Not started"); + }); + }); + + describe("Given I have a section enabled based on the completeness of another section", () => { + it("When the section being evaluated is incomplete, Then the dependent section should not be enabled", async () => { + await click(HubPage.submit()); + await $(FirstQuestionPage.q1A1()).setValue("0"); + await click(FirstQuestionPage.submit()); + await browser.url(HubPage.url()); + + await expect(await $(HubPage.summaryRowState("section-1")).getText()).toBe("Partially completed"); + await expect(await $("body").getText()).not.toBe("Section 2"); + }); + }); +}); diff --git a/tests/functional/spec/journeys/progress/progress_value_source_repeating.js b/tests/functional/spec/journeys/progress/progress_value_source_repeating.js new file mode 100644 index 0000000000..d46ecf8596 --- /dev/null +++ b/tests/functional/spec/journeys/progress/progress_value_source_repeating.js @@ -0,0 +1,199 @@ +import HubPage from "../../../base_pages/hub.page"; +import ListCollectorPage from "../../../generated_pages/new_calculated_summary_repeating_section/list-collector.page"; +import ListCollectorAddPage from "../../../generated_pages/new_calculated_summary_repeating_section/list-collector-add.page"; +import QuestionBlockPage from "../../../generated_pages/progress_block_value_source_repeating_sections/question-block.page"; +import DOBQuestionBlockPage from "../../../generated_pages/progress_block_value_source_repeating_sections/dob-block.page"; +import RandomQuestionEnablerBlockPage from "../../../generated_pages/progress_block_value_source_repeating_sections/random-question-enabler-block.page"; +import SectionTwoSummaryPage from "../../../generated_pages/progress_block_value_source_repeating_sections/section-2-summary.page"; +import SectionThreeSummaryPage from "../../../generated_pages/progress_value_source_calculated_summary/section-3-summary.page"; +import OtherQuestionBlockPage from "../../../generated_pages/progress_block_value_source_repeating_sections/other-question-block.page"; +import FirstNumberBlockPage from "../../../generated_pages/progress_value_source_calculated_summary/first-number-block.page"; +import SecondNumberBlockPage from "../../../generated_pages/progress_value_source_calculated_summary/second-number-block.page"; +import SectionTwoQuestionBlockPage from "../../../generated_pages/progress_value_source_calculated_summary/s2-b1.page"; +import CalculatedSummaryBlockPage from "../../../generated_pages/progress_value_source_calculated_summary/calculated-summary-block.page"; +import { click, verifyUrlContains } from "../../../helpers"; +describe("Feature: Routing rules based on progress value sources in repeating sections", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_progress_block_value_source_repeating_sections.json"); + }); + + describe("Given I have routing in a repeating section based on the completeness of a block", () => { + it("When the block is incomplete, then I should not see the dependent question in the repeating section", async () => { + await click(HubPage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("John"); + await $(ListCollectorAddPage.lastName()).setValue("Doe"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await verifyUrlContains(QuestionBlockPage.pageName); + + await browser.url(HubPage.url()); + await expect(await $(HubPage.summaryRowState("section-1")).getText()).toBe("Partially completed"); + + await $(HubPage.summaryRowLink("section-2-1")).click(); + await click(DOBQuestionBlockPage.submit()); + await verifyUrlContains(SectionTwoSummaryPage.pageName); + }); + }); + + describe("Given I have routing in a repeating section based on the completeness of a block", () => { + it("When the block is complete, then I should see the dependent question in the repeating section", async () => { + await click(HubPage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("John"); + await $(ListCollectorAddPage.lastName()).setValue("Doe"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await click(QuestionBlockPage.submit()); + await $(RandomQuestionEnablerBlockPage.randomQuestionEnabler()).setValue(1); + await click(RandomQuestionEnablerBlockPage.submit()); + + await browser.url(HubPage.url()); + await expect(await $(HubPage.summaryRowState("section-1")).getText()).toBe("Completed"); + + await $(HubPage.summaryRowLink("section-2-1")).click(); + await click(DOBQuestionBlockPage.submit()); + await verifyUrlContains(OtherQuestionBlockPage.pageName); + }); + }); + + describe("Given I have routing in a repeating section based on the completeness of a block", () => { + it("When the status of the block changes from incomplete to complete, then the dependent question should be on the path in the repeating sections", async () => { + await click(HubPage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("John"); + await $(ListCollectorAddPage.lastName()).setValue("Doe"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Joe"); + await $(ListCollectorAddPage.lastName()).setValue("Bloggs"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await browser.url(HubPage.url()); + await expect(await $(HubPage.summaryRowState("section-2-1")).getText()).toBe("Not started"); + await expect(await $(HubPage.summaryRowState("section-2-2")).getText()).toBe("Not started"); + + await $(HubPage.summaryRowLink("section-2-1")).click(); + await click(DOBQuestionBlockPage.submit()); + await click(SectionTwoSummaryPage.submit()); + await expect(await $(HubPage.summaryRowState("section-2-1")).getText()).toBe("Completed"); + await expect(await $(HubPage.summaryRowState("section-2-2")).getText()).toBe("Not started"); + + await click(HubPage.submit()); + await click(QuestionBlockPage.submit()); + await $(RandomQuestionEnablerBlockPage.randomQuestionEnabler()).setValue(1); + await click(RandomQuestionEnablerBlockPage.submit()); + + await expect(await $(HubPage.summaryRowState("section-2-1")).getText()).toBe("Partially completed"); + await expect(await $(HubPage.summaryRowState("section-2-2")).getText()).toBe("Not started"); + }); + }); +}); + +describe("Feature: Routing rules based on progress value sources in repeating sections", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_progress_value_source_calculated_summary.json"); + }); + + describe("Given I have routing in a repeating section based on the completeness of a calculated summary", () => { + it("When the calculated summary block is incomplete, then I should not see the dependent question in the repeating section", async () => { + await click(HubPage.submit()); + await $(FirstNumberBlockPage.firstNumber()).setValue(1); + await click(FirstNumberBlockPage.submit()); + await browser.url(HubPage.url()); + + await $(HubPage.summaryRowLink("section-2")).click(); + + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("John"); + await $(ListCollectorAddPage.lastName()).setValue("Doe"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await verifyUrlContains(HubPage.pageName); + + await $(HubPage.summaryRowLink("section-3-1")).click(); + await click(DOBQuestionBlockPage.submit()); + await click(SectionThreeSummaryPage.submit()); + + await expect(await $(HubPage.summaryRowState("section-1")).getText()).toBe("Partially completed"); + await expect(await $(HubPage.summaryRowState("section-2")).getText()).toBe("Completed"); + await expect(await $(HubPage.summaryRowState("section-3-1")).getText()).toBe("Completed"); + }); + }); + + describe("Given I have routing in a repeating section based on the completeness of a calculated summary", () => { + it("When the calculated summary block is incomplete but is updated so that it is completed, then I should see the dependency should be updated in the repeating section", async () => { + await click(HubPage.submit()); + await $(FirstNumberBlockPage.firstNumber()).setValue(1); + await click(FirstNumberBlockPage.submit()); + await browser.url(HubPage.url()); + + await $(HubPage.summaryRowLink("section-2")).click(); + + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("John"); + await $(ListCollectorAddPage.lastName()).setValue("Doe"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await verifyUrlContains(HubPage.pageName); + + await $(HubPage.summaryRowLink("section-3-1")).click(); + await click(DOBQuestionBlockPage.submit()); + await click(SectionThreeSummaryPage.submit()); + + await expect(await $(HubPage.summaryRowState("section-1")).getText()).toBe("Partially completed"); + await expect(await $(HubPage.summaryRowState("section-2")).getText()).toBe("Completed"); + await expect(await $(HubPage.summaryRowState("section-3-1")).getText()).toBe("Completed"); + + await $(HubPage.summaryRowLink("section-1")).click(); + await $(SecondNumberBlockPage.secondNumber()).setValue(2); + await click(SecondNumberBlockPage.submit()); + await click(CalculatedSummaryBlockPage.submit()); + await browser.url(HubPage.url()); + + await expect(await $(HubPage.summaryRowState("section-1")).getText()).toBe("Completed"); + await expect(await $(HubPage.summaryRowState("section-2")).getText()).toBe("Partially completed"); + await expect(await $(HubPage.summaryRowState("section-3-1")).getText()).toBe("Partially completed"); + }); + }); + + describe("Given I have routing in a repeating section based on the completeness of a calculated summary", () => { + it("When the calculated summary block is complete, then I should see the dependent question in the repeating section", async () => { + await click(HubPage.submit()); + await $(FirstNumberBlockPage.firstNumber()).setValue(1); + await click(FirstNumberBlockPage.submit()); + await $(SecondNumberBlockPage.secondNumber()).setValue(2); + await click(SecondNumberBlockPage.submit()); + await click(CalculatedSummaryBlockPage.submit()); + await browser.url(HubPage.url()); + + await $(HubPage.summaryRowLink("section-2")).click(); + + await $(SectionTwoQuestionBlockPage.q1A1()).setValue(1); + await click(SectionTwoQuestionBlockPage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("John"); + await $(ListCollectorAddPage.lastName()).setValue("Doe"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await verifyUrlContains(HubPage.pageName); + + await $(HubPage.summaryRowLink("section-3-1")).click(); + await click(DOBQuestionBlockPage.submit()); + await verifyUrlContains(OtherQuestionBlockPage.pageName); + }); + }); +}); diff --git a/tests/functional/spec/journeys/repeating_blocks/list_collector_repeating_blocks.spec.js b/tests/functional/spec/journeys/repeating_blocks/list_collector_repeating_blocks.spec.js new file mode 100644 index 0000000000..733fc607df --- /dev/null +++ b/tests/functional/spec/journeys/repeating_blocks/list_collector_repeating_blocks.spec.js @@ -0,0 +1,365 @@ +import ResponsiblePartyPage from "../../../generated_pages/list_collector_repeating_blocks_section_summary/responsible-party.page"; +import AnyCompaniesOrBranchesPage from "../../../generated_pages/list_collector_repeating_blocks_section_summary/any-companies-or-branches.page"; +import AddCompanyPage from "../../../generated_pages/list_collector_repeating_blocks_section_summary/any-other-companies-or-branches-add.page"; +import EditCompanyPage from "../../../generated_pages/list_collector_repeating_blocks_section_summary/any-other-companies-or-branches-edit.page"; +import RemoveCompanyPage from "../../../generated_pages/list_collector_repeating_blocks_section_summary/any-other-companies-or-branches-remove.page"; +import CompaniesRepeatingBlock1Page from "../../../generated_pages/list_collector_repeating_blocks_section_summary/companies-repeating-block-1-repeating-block.page"; +import CompaniesRepeatingBlock2Page from "../../../generated_pages/list_collector_repeating_blocks_section_summary/companies-repeating-block-2-repeating-block.page"; +import AnyOtherCompaniesOrBranchesPage from "../../../generated_pages/list_collector_repeating_blocks_section_summary/any-other-companies-or-branches.page"; +import SectionCompaniesPage from "../../../generated_pages/list_collector_repeating_blocks_section_summary/section-companies-summary.page"; +import AnyOtherTradingDetailsPage from "../../../generated_pages/list_collector_repeating_blocks_section_summary/any-other-trading-details.page"; +import SubmitPage from "../../../generated_pages/list_collector_repeating_blocks_section_summary/submit.page"; +import { repeatingAnswerChangeLink, checkItemsInList, summaryItemComplete, click, verifyUrlContains } from "../../../helpers"; +import HubPage from "../../../base_pages/hub.page"; +import ResponsiblePartyHubPage from "../../../generated_pages/list_collector_repeating_blocks_with_hub/responsible-party-business.page"; +import { expect } from "@wdio/globals"; +import ThankYouPage from "../../../base_pages/thank-you.page"; + +const summaryValues = 'dd[class="ons-summary__values"]'; +async function proceedToListCollector() { + await $(ResponsiblePartyPage.yes()).click(); + await click(AnyCompaniesOrBranchesPage.submit()); + await $(AnyCompaniesOrBranchesPage.yes()).click(); + await click(AnyCompaniesOrBranchesPage.submit()); +} + +async function addCompany( + companyOrBranchName, + registrationNumber, + registrationDateDay, + registrationDateMonth, + registrationDateYear, + authorisedTraderUk, + authorisedTraderEu, +) { + await $(AddCompanyPage.companyOrBranchName()).setValue(companyOrBranchName); + await click(AddCompanyPage.submit()); + await $(CompaniesRepeatingBlock1Page.registrationNumber()).setValue(registrationNumber); + await $(CompaniesRepeatingBlock1Page.registrationDateday()).setValue(registrationDateDay); + await $(CompaniesRepeatingBlock1Page.registrationDatemonth()).setValue(registrationDateMonth); + await $(CompaniesRepeatingBlock1Page.registrationDateyear()).setValue(registrationDateYear); + await click(CompaniesRepeatingBlock1Page.submit()); + if (authorisedTraderUk) { + await $(CompaniesRepeatingBlock2Page.authorisedTraderUkRadioYes()).click(); + } else { + await $(CompaniesRepeatingBlock2Page.authorisedTraderUkRadioNo()).click(); + } + if (authorisedTraderEu) { + await $(CompaniesRepeatingBlock2Page.authorisedTraderEuRadioYes()).click(); + } else if (authorisedTraderEu !== undefined) { + await $(CompaniesRepeatingBlock2Page.authorisedTraderEuRadioNo()).click(); + } + await click(CompaniesRepeatingBlock2Page.submit()); +} + +describe("List Collector Repeating Blocks", () => { + describe("Given a normal journey through the list collector with repeating blocks", () => { + before("Load the survey", async () => { + await browser.openQuestionnaire("test_list_collector_repeating_blocks_section_summary.json"); + // These tests sometimes fail when a button is on the screen, but right on the very edge, accept cookies to increase screen space + await $(ResponsiblePartyPage.acceptCookies()).click(); + }); + it("When the user adds items and completes all of the repeating blocks, Then they are able to successfully submit the questionnaire.", async () => { + await proceedToListCollector(); + await addCompany("ONS", "123", "1", "1", "2023", true, true); + await $(AnyOtherCompaniesOrBranchesPage.yes()).click(); + await click(AnyOtherCompaniesOrBranchesPage.submit()); + await addCompany("GOV", "456", "2", "2", "2023", false, false); + + await $(AnyOtherCompaniesOrBranchesPage.no()).click(); + await click(AnyOtherCompaniesOrBranchesPage.submit()); + + await click(AnyOtherTradingDetailsPage.submit()); + await click(SectionCompaniesPage.submit()); + await click(SubmitPage.submit()); + await verifyUrlContains(ThankYouPage.pageName); + }); + }); + + describe("Given a journey through the list collector with repeating blocks where items need to be updated", () => { + before("Load the survey", async () => { + await browser.openQuestionnaire("test_list_collector_repeating_blocks_section_summary.json"); + }); + it("When the user adds items to the list and completes the repeating blocks, Then the completed items are displayed on the list collector page.", async () => { + await proceedToListCollector(); + await addCompany("ONS", "123", "1", "1", "2023", true, true); + await $(AnyOtherCompaniesOrBranchesPage.yes()).click(); + await click(AnyOtherCompaniesOrBranchesPage.submit()); + await addCompany("GOV", "456", "2", "2", "2023", false, false); + await $(AnyOtherCompaniesOrBranchesPage.yes()).click(); + await click(AnyOtherCompaniesOrBranchesPage.submit()); + await addCompany("MOD", "789", "3", "3", "2023", true); + await checkItemsInList(["ONS", "GOV", "MOD"], AnyOtherCompaniesOrBranchesPage.listLabel); + }); + + it("When the user edits an item, Then the name of the item is able to be changed", async () => { + await $(AnyOtherCompaniesOrBranchesPage.listEditLink(2)).click(); + await $(EditCompanyPage.companyOrBranchName()).setValue("Government"); + await click(EditCompanyPage.submit()); + await checkItemsInList(["ONS", "Government", "MOD"], AnyOtherCompaniesOrBranchesPage.listLabel); + }); + + it("When the user clicks the remove link, Then the item selected is removed", async () => { + await $(AnyOtherCompaniesOrBranchesPage.listRemoveLink(2)).click(); + await $(RemoveCompanyPage.yes()).click(); + await click(RemoveCompanyPage.submit()); + await checkItemsInList(["ONS", "MOD"], AnyOtherCompaniesOrBranchesPage.listLabel); + await expect(await $(AnyOtherCompaniesOrBranchesPage.listLabel(2)).getText()).not.toContain("Government"); + }); + + it("When a user has finished editing or removing from the list, Then they are still able to add additional companies", async () => { + await $(AnyOtherCompaniesOrBranchesPage.yes()).click(); + await click(AnyOtherCompaniesOrBranchesPage.submit()); + await addCompany("Council", "101", "4", "4", "2023", false, true); + await checkItemsInList(["ONS", "MOD", "Council"], AnyOtherCompaniesOrBranchesPage.listLabel); + }); + + it("When a user has finished making changes to the list, Then section can be completed and the questionnaire submitted", async () => { + await $(AnyOtherCompaniesOrBranchesPage.no()).click(); + await click(AnyOtherCompaniesOrBranchesPage.submit()); + + await click(AnyOtherTradingDetailsPage.submit()); + await click(SectionCompaniesPage.submit()); + await click(SubmitPage.submit()); + await verifyUrlContains(ThankYouPage.pageName); + }); + }); + + describe("Given a journey that test routes through the list collector with repeating blocks.", () => { + before("Load the survey", async () => { + await browser.openQuestionnaire("test_list_collector_repeating_blocks_section_summary.json"); + }); + it("When the user only completes some of the repeating blocks and leaves others incomplete, Then on the list collector page only completed items should display the completed checkmark icon.", async () => { + await proceedToListCollector(); + + await addCompany("ONS", "123", "1", "1", "2023", true, true); + await $(AnyOtherCompaniesOrBranchesPage.yes()).click(); + await click(AnyOtherCompaniesOrBranchesPage.submit()); + await $(AddCompanyPage.companyOrBranchName()).setValue("GOV"); + await click(AddCompanyPage.submit()); + await $(CompaniesRepeatingBlock1Page.cancelAndReturn()).click(); + await $(EditCompanyPage.cancelAndReturn()).click(); + + await $(AnyOtherCompaniesOrBranchesPage.yes()).click(); + await click(AnyOtherCompaniesOrBranchesPage.submit()); + await $(AddCompanyPage.companyOrBranchName()).setValue("MOD"); + await click(AddCompanyPage.submit()); + await $(CompaniesRepeatingBlock1Page.registrationNumber()).setValue("789"); + await $(CompaniesRepeatingBlock1Page.registrationDateday()).setValue("3"); + await $(CompaniesRepeatingBlock1Page.registrationDatemonth()).setValue("3"); + await $(CompaniesRepeatingBlock1Page.registrationDateyear()).setValue("2023"); + await click(CompaniesRepeatingBlock1Page.submit()); + await $(CompaniesRepeatingBlock2Page.cancelAndReturn()).click(); + await $(CompaniesRepeatingBlock1Page.cancelAndReturn()).click(); + await $(EditCompanyPage.cancelAndReturn()).click(); + + await $(AnyOtherCompaniesOrBranchesPage.yes()).click(); + await click(AnyOtherCompaniesOrBranchesPage.submit()); + await addCompany("NAV", "101", "4", "4", "2023", true, true); + + // Only the ONS and NAV items should be complete + await checkItemsInList(["ONS", "GOV", "MOD", "NAV"], AnyOtherCompaniesOrBranchesPage.listLabel); + await summaryItemComplete(`dt[data-qa="list-item-1-label"]`, true); + await summaryItemComplete(`dt[data-qa="list-item-2-label"]`, false); + await summaryItemComplete(`dt[data-qa="list-item-3-label"]`, false); + await summaryItemComplete(`dt[data-qa="list-item-1-label"]`, true); + }); + + it("When an item has incomplete repeating blocks, Then using submit on the list collector page will navigate the user to the first incomplete repeating block.", async () => { + await $(AnyOtherCompaniesOrBranchesPage.no()).click(); + await click(AnyOtherCompaniesOrBranchesPage.submit()); + await verifyUrlContains(CompaniesRepeatingBlock1Page.pageName); + }); + + it("When there are multiple incomplete items and only the first incomplete item is completed, Then attempting using Submit on the list collector page will navigate the user to the next incomplete item.", async () => { + // Complete the first incomplete list item + await $(CompaniesRepeatingBlock1Page.registrationNumber()).setValue("456"); + await $(CompaniesRepeatingBlock1Page.registrationDateday()).setValue("2"); + await $(CompaniesRepeatingBlock1Page.registrationDatemonth()).setValue("2"); + await $(CompaniesRepeatingBlock1Page.registrationDateyear()).setValue("2023"); + await click(CompaniesRepeatingBlock1Page.submit()); + await $(CompaniesRepeatingBlock2Page.authorisedTraderUkRadioNo()).click(); + await $(CompaniesRepeatingBlock2Page.authorisedTraderEuRadioNo()).click(); + await click(CompaniesRepeatingBlock2Page.submit()); + + await $(AnyOtherCompaniesOrBranchesPage.no()).click(); + await click(AnyOtherCompaniesOrBranchesPage.submit()); + + // The user is taken to the next incomplete repeating block + await verifyUrlContains(CompaniesRepeatingBlock2Page.pageName); + }); + + it("When the last remaining incomplete repeating block is completed, Then all items are marked as completed with the checkmark icon.", async () => { + await $(CompaniesRepeatingBlock2Page.authorisedTraderUkRadioNo()).click(); + await click(CompaniesRepeatingBlock2Page.submit()); + await summaryItemComplete(`dt[data-qa="list-item-1-label"]`, true); + await summaryItemComplete(`dt[data-qa="list-item-2-label"]`, true); + await summaryItemComplete(`dt[data-qa="list-item-3-label"]`, true); + await summaryItemComplete(`dt[data-qa="list-item-4-label"]`, true); + }); + + it("When the user clicks a change link from the section summary and submits without changing an answer, Then the user is returned to the section summary anchored to the answer they clicked on", async () => { + await $(AnyOtherCompaniesOrBranchesPage.no()).click(); + await click(AnyOtherCompaniesOrBranchesPage.submit()); + await click(AnyOtherTradingDetailsPage.submit()); + + await $(SectionCompaniesPage.anyOtherTradingDetailsAnswerEdit()).click(); + await click(AnyOtherTradingDetailsPage.submit()); + await verifyUrlContains("section-companies/#any-other-trading-details-answer"); + + await $(SectionCompaniesPage.anyOtherTradingDetailsAnswerEdit()).click(); + await $(AnyOtherTradingDetailsPage.previous()).click(); + await verifyUrlContains("section-companies/#any-other-trading-details-answer"); + }); + + it("When an answer is edited from the section summary which does not affect progress, Then pressing continue returns the user to the section summary anchored to the answer they edited", async () => { + await $(SectionCompaniesPage.anyOtherTradingDetailsAnswerEdit()).click(); + await $(AnyOtherTradingDetailsPage.answer()).setValue("No"); + await click(AnyOtherTradingDetailsPage.submit()); + await verifyUrlContains("section-companies/#any-other-trading-details-answer"); + }); + + it("When a user clicks a change link from the final summary and submits without changing an answer, Then the user is returned to the final summary anchored to the answer they clicked on", async () => { + await click(SectionCompaniesPage.submit()); + + await $(SubmitPage.anyOtherTradingDetailsAnswerEdit()).click(); + await click(AnyOtherTradingDetailsPage.submit()); + await verifyUrlContains("submit/#any-other-trading-details-answer"); + + await $(SubmitPage.anyOtherTradingDetailsAnswerEdit()).click(); + await $(AnyOtherTradingDetailsPage.previous()).click(); + await verifyUrlContains("submit/#any-other-trading-details-answer"); + }); + + it("When an an answer is edited from the final summary which does not affect progress, Then pressing continue returns the user to the final summary anchored to the answer they edited", async () => { + await $(SectionCompaniesPage.anyOtherTradingDetailsAnswerEdit()).click(); + await $(AnyOtherTradingDetailsPage.answer()).setValue("Yes"); + await click(AnyOtherTradingDetailsPage.submit()); + await verifyUrlContains("submit/#any-other-trading-details-answer"); + }); + + it("When all items are completed by the user, Then the questionnaire is able to be submitted.", async () => { + await click(SubmitPage.submit()); + await verifyUrlContains(ThankYouPage.pageName); + }); + }); + + describe("Given a journey through the list collector with repeating blocks", () => { + before("Load the survey", async () => { + await browser.openQuestionnaire("test_list_collector_repeating_blocks_section_summary.json"); + }); + it("When the user adds and completes items, Then they are able to see the items on the section summary page.", async () => { + await proceedToListCollector(); + await addCompany("ONS", "123", "1", "1", "2023", true, true); + await $(AnyOtherCompaniesOrBranchesPage.yes()).click(); + await click(AnyOtherCompaniesOrBranchesPage.submit()); + await addCompany("GOV", "456", "2", "2", "2023", false); + await $(AnyOtherCompaniesOrBranchesPage.no()).click(); + await click(AnyOtherCompaniesOrBranchesPage.submit()); + await click(AnyOtherTradingDetailsPage.submit()); + await click(SectionCompaniesPage.submit()); + await expect(await $$(summaryValues)[2].getText()).toContain("ONS"); + await expect(await $$(summaryValues)[4].getText()).toContain("1 January 2023"); + await expect(await $$(summaryValues)[5].getText()).toContain("Yes"); + await expect(await $$(summaryValues)[7].getText()).toContain("GOV"); + await expect(await $$(summaryValues)[8].getText()).toContain("456"); + await expect(await $$(summaryValues)[11].getText()).toContain("No answer provided"); + }); + + it("When an item is edited from the section summary page, Then the correct value is displayed when the user returns to the summary.", async () => { + await expect(await $$(summaryValues)[8].getText()).toContain("456"); + await repeatingAnswerChangeLink(8).click(); + await $(CompaniesRepeatingBlock1Page.registrationNumber()).setValue("789"); + await click(CompaniesRepeatingBlock1Page.submit()); + await expect(await $$(summaryValues)[8].getText()).toContain("789"); + }); + }); + + describe("Given the user is completing a list collector with repeating blocks in a mandatory section of a hub based questionnaire.", () => { + before("Load the survey", async () => { + await browser.openQuestionnaire("test_list_collector_repeating_blocks_with_hub.json"); + }); + it("When the user adds complete and incomplete items and returns to the hub, Then the user should be taken to first incomplete repeating block when pressing Continue.", async () => { + await proceedToListCollector(); + + await addCompany("ONS", "123", "1", "1", "2023", true, true); + await $(AnyOtherCompaniesOrBranchesPage.yes()).click(); + await click(AnyOtherCompaniesOrBranchesPage.submit()); + await $(AddCompanyPage.companyOrBranchName()).setValue("GOV"); + await click(AddCompanyPage.submit()); + await $(CompaniesRepeatingBlock1Page.cancelAndReturn()).click(); + await browser.url("questionnaire/"); + await click(HubPage.submit()); + await $(AnyOtherCompaniesOrBranchesPage.no()).click(); + await click(AnyOtherCompaniesOrBranchesPage.submit()); + await verifyUrlContains(CompaniesRepeatingBlock1Page.pageName); + }); + + it("When the user completes the incomplete blocks and returns to the list collector Page, Then the completed items should display the checkmark icon", async () => { + await $(CompaniesRepeatingBlock1Page.registrationNumber()).setValue("456"); + await $(CompaniesRepeatingBlock1Page.registrationDateday()).setValue("2"); + await $(CompaniesRepeatingBlock1Page.registrationDatemonth()).setValue("2"); + await $(CompaniesRepeatingBlock1Page.registrationDateyear()).setValue("2023"); + await click(CompaniesRepeatingBlock1Page.submit()); + await $(CompaniesRepeatingBlock2Page.authorisedTraderUkRadioNo()).click(); + await click(CompaniesRepeatingBlock2Page.submit()); + await verifyUrlContains(AnyOtherCompaniesOrBranchesPage.pageName); + await summaryItemComplete(`dt[data-qa="list-item-1-label"]`, true); + await summaryItemComplete(`dt[data-qa="list-item-2-label"]`, true); + }); + + it("When another incomplete item is added via the section summary, Then navigating to the submit page of the section will redirect to the list collector page.", async () => { + // Add another item and partially complete + await $(AnyOtherCompaniesOrBranchesPage.no()).click(); + await click(AnyOtherCompaniesOrBranchesPage.submit()); + await click(AnyOtherTradingDetailsPage.submit()); + await $(SectionCompaniesPage.companiesListAddLink()).click(); + await $(AddCompanyPage.companyOrBranchName()).setValue("MOD"); + await click(AddCompanyPage.submit()); + await $(CompaniesRepeatingBlock1Page.cancelAndReturn()).click(); + + // Navigating to the section summary will redirect to the list collector page + await browser.url("questionnaire/sections/section-companies/"); + await verifyUrlContains(AnyOtherCompaniesOrBranchesPage.pageName); + }); + + it("When the incomplete repeating blocks are completed, Then the user is able to complete the section and is taken to the hub page.", async () => { + await $(AnyOtherCompaniesOrBranchesPage.no()).click(); + await click(AnyOtherCompaniesOrBranchesPage.submit()); + await $(CompaniesRepeatingBlock1Page.registrationNumber()).setValue("789"); + await $(CompaniesRepeatingBlock1Page.registrationDateday()).setValue("3"); + await $(CompaniesRepeatingBlock1Page.registrationDatemonth()).setValue("3"); + await $(CompaniesRepeatingBlock1Page.registrationDateyear()).setValue("2023"); + await click(CompaniesRepeatingBlock1Page.submit()); + await $(CompaniesRepeatingBlock2Page.authorisedTraderUkRadioYes()).click(); + await click(CompaniesRepeatingBlock2Page.submit()); + await $(AnyOtherCompaniesOrBranchesPage.no()).click(); + await click(AnyOtherCompaniesOrBranchesPage.submit()); + await click(SectionCompaniesPage.submit()); + await verifyUrlContains(HubPage.pageName); + }); + + it("When the user is on the Hub page and has completed the section, Then they are able to add additional companies using the Add link", async () => { + await $(HubPage.summaryRowLink("section-companies")).click(); + await $(SectionCompaniesPage.companiesListAddLink()).click(); + await $(AddCompanyPage.companyOrBranchName()).setValue("MOJ"); + await click(AddCompanyPage.submit()); + await $(CompaniesRepeatingBlock1Page.registrationNumber()).setValue("789"); + await $(CompaniesRepeatingBlock1Page.registrationDateday()).setValue("3"); + await $(CompaniesRepeatingBlock1Page.registrationDatemonth()).setValue("3"); + await $(CompaniesRepeatingBlock1Page.registrationDateyear()).setValue("2023"); + await click(CompaniesRepeatingBlock1Page.submit()); + await $(CompaniesRepeatingBlock2Page.authorisedTraderUkRadioYes()).click(); + await click(CompaniesRepeatingBlock2Page.submit()); + await $(AnyOtherCompaniesOrBranchesPage.no()).click(); + await click(AnyOtherCompaniesOrBranchesPage.submit()); + await click(SectionCompaniesPage.submit()); + await verifyUrlContains(HubPage.pageName); + }); + + it("When the user has completed the list collector section and uses Submit on the hub page, Then the user will be redirected to the next section.", async () => { + await click(HubPage.submit()); + await verifyUrlContains(ResponsiblePartyHubPage.pageName); + }); + }); +}); diff --git a/tests/functional/spec/journeys/repeating_sections/repeating_sections_with_hub_and_spoke.spec.js b/tests/functional/spec/journeys/repeating_sections/repeating_sections_with_hub_and_spoke.spec.js new file mode 100644 index 0000000000..5046b0b235 --- /dev/null +++ b/tests/functional/spec/journeys/repeating_sections/repeating_sections_with_hub_and_spoke.spec.js @@ -0,0 +1,306 @@ +import ConfirmDateOfBirthPage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/confirm-dob.page"; +import DateOfBirthPage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/date-of-birth.page"; +import FirstListCollectorAddPage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/list-collector-add.page"; +import FirstListCollectorPage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/list-collector.page"; +import HubPage from "../../../base_pages/hub.page.js"; +import PersonalDetailsSummaryPage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/personal-details-section-summary.page"; +import PrimaryPersonAddPage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/primary-person-list-collector-add.page"; +import PrimaryPersonPage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/primary-person-list-collector.page"; +import ProxyPage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/proxy.page"; +import SecondListCollectorAddPage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/another-list-collector-block-add.page"; +import SecondListCollectorInterstitialPage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/next-interstitial.page"; +import SecondListCollectorPage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/another-list-collector-block.page"; +import SectionSummaryPage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/section-summary.page.js"; +import SexPage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/sex.page"; +import VisitorsDateOfBirthPage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/visitors-date-of-birth.page"; +import VisitorsListCollectorAddPage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/visitors-block-add.page"; +import VisitorsListCollectorPage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/visitors-block.page"; +import VisitorsListCollectorRemovePage from "../../../generated_pages/repeating_sections_with_hub_and_spoke/visitors-block-remove.page"; +import { click, verifyUrlContains } from "../../../helpers"; +describe("Feature: Repeating Sections with Hub and Spoke", () => { + describe("Given the user has added some members to the household and is on the Hub", () => { + before("Open survey and add household members", async () => { + await browser.openQuestionnaire("test_repeating_sections_with_hub_and_spoke.json"); + // Accept cookies, this is done due to headless window size where cookie banner + // is pushing the submit button outside window + await $(HubPage.acceptCookies()).click(); + // Ensure we are on the Hub + await verifyUrlContains(HubPage.url()); + // Ensure the first section is not started + await expect(await $(HubPage.summaryRowState("section")).getText()).toBe("Not started"); + // Start first section to add household members + await $(HubPage.summaryRowLink("section")).click(); + + // Add a primary person + await $(PrimaryPersonPage.yes()).click(); + await click(PrimaryPersonPage.submit()); + await $(PrimaryPersonAddPage.firstName()).setValue("Marcus"); + await $(PrimaryPersonAddPage.lastName()).setValue("Twin"); + await click(PrimaryPersonPage.submit()); + + // Add other household members (First list collector) + await $(FirstListCollectorPage.yes()).click(); + await click(FirstListCollectorPage.submit()); + await $(FirstListCollectorAddPage.firstName()).setValue("Jean"); + await $(FirstListCollectorAddPage.lastName()).setValue("Clemens"); + await click(FirstListCollectorAddPage.submit()); + + await $(FirstListCollectorPage.yes()).click(); + await click(FirstListCollectorPage.submit()); + await $(FirstListCollectorAddPage.firstName()).setValue("Samuel"); + await $(FirstListCollectorAddPage.lastName()).setValue("Clemens"); + await click(FirstListCollectorAddPage.submit()); + + // Go to second list collector + await $(FirstListCollectorPage.no()).click(); + await click(FirstListCollectorPage.submit()); + await click(SecondListCollectorInterstitialPage.submit()); + + // Add other household members (Second list collector) + await $(SecondListCollectorPage.yes()).click(); + await click(SecondListCollectorPage.submit()); + await $(SecondListCollectorAddPage.firstName()).setValue("John"); + await $(SecondListCollectorAddPage.lastName()).setValue("Doe"); + await click(SecondListCollectorAddPage.submit()); + + // Go back to the Hub + await $(SecondListCollectorPage.no()).click(); + await click(SecondListCollectorPage.submit()); + await $(VisitorsListCollectorPage.no()).click(); + await click(VisitorsListCollectorPage.submit()); + }); + + beforeEach("Navigate to the Hub", async () => await browser.url(HubPage.url())); + + it("Then a section for each household member should be displayed", async () => { + await verifyUrlContains(HubPage.url()); + + await expect(await $(HubPage.summaryRowState("section")).getText()).toBe("Completed"); + await expect(await $(HubPage.summaryRowTitle("personal-details-section-1")).getText()).toBe("Marcus Twin"); + await expect(await $(HubPage.summaryRowState("personal-details-section-1")).getText()).toBe("Not started"); + await expect(await $(HubPage.summaryRowState("personal-details-section-2")).getText()).toBe("Not started"); + await expect(await $(HubPage.summaryRowTitle("personal-details-section-2")).getText()).toBe("Jean Clemens"); + await expect(await $(HubPage.summaryRowState("personal-details-section-3")).getText()).toBe("Not started"); + await expect(await $(HubPage.summaryRowTitle("personal-details-section-3")).getText()).toBe("Samuel Clemens"); + await expect(await $(HubPage.summaryRowState("personal-details-section-4")).getText()).toBe("Not started"); + await expect(await $(HubPage.summaryRowTitle("personal-details-section-4")).getText()).toBe("John Doe"); + + await expect(await $(HubPage.summaryRowState("section-5")).isExisting()).toBe(false); + }); + + it("When the user starts a repeating section and clicks the Previous link on the first question, Then they should be taken back to the Hub", async () => { + await $(HubPage.summaryRowLink("personal-details-section-2")).click(); + await $(ProxyPage.previous()).click(); + + await verifyUrlContains(HubPage.url()); + }); + + it("When the user partially completes a repeating section, Then that section should be marked as 'Partially completed' on the Hub", async () => { + await $(HubPage.summaryRowLink("personal-details-section-1")).click(); + await $(ProxyPage.yes()).click(); + await click(ProxyPage.submit()); + + await $(DateOfBirthPage.day()).setValue("01"); + await $(DateOfBirthPage.month()).setValue("03"); + await $(DateOfBirthPage.year()).setValue("2000"); + await click(DateOfBirthPage.submit()); + + await $(ConfirmDateOfBirthPage.confirmDateOfBirthYesPersonNameIsAgeOld()).click(); + await click(ConfirmDateOfBirthPage.submit()); + + await browser.url(HubPage.url()); + + await verifyUrlContains(HubPage.url()); + await expect(await $(HubPage.summaryRowState("personal-details-section-1")).getText()).toBe("Partially completed"); + }); + + it("When the user continues with a partially completed repeating section, Then they are taken to the first incomplete block", async () => { + await $(HubPage.summaryRowLink("personal-details-section-1")).click(); + + await expect(await $(SexPage.questionText()).getText()).toBe("What is Marcus Twin’s sex?"); + }); + + it("When the user completes a repeating section, Then that section should be marked as 'Completed' on the Hub", async () => { + await $(HubPage.summaryRowLink("personal-details-section-2")).click(); + await $(ProxyPage.yes()).click(); + await click(ProxyPage.submit()); + + await $(DateOfBirthPage.day()).setValue("09"); + await $(DateOfBirthPage.month()).setValue("09"); + await $(DateOfBirthPage.year()).setValue("1995"); + await click(DateOfBirthPage.submit()); + + await $(ConfirmDateOfBirthPage.confirmDateOfBirthYesPersonNameIsAgeOld()).click(); + await click(ConfirmDateOfBirthPage.submit()); + + await $(SexPage.female()).click(); + await click(SexPage.submit()); + + await click(PersonalDetailsSummaryPage.submit()); + + await verifyUrlContains(HubPage.url()); + await expect(await $(HubPage.summaryRowState("personal-details-section-2")).getText()).toBe("Completed"); + }); + + it("When the user clicks 'View answers' for a completed repeating section, Then they are taken to the summary", async () => { + await $(HubPage.summaryRowLink("personal-details-section-2")).click(); + await verifyUrlContains("/sections/personal-details-section"); + }); + + it("When the user views the summary for a repeating section, Then the page title is shown", async () => { + await $(HubPage.summaryRowLink("personal-details-section-2")).click(); + await expect(await browser.getTitle()).toBe("â€Ļ - Hub & Spoke"); + }); + + it("When the user adds 2 visitors to the household then a section for each visitor should be display on the hub", async () => { + // Ensure no other sections exist + await expect(await $(HubPage.summaryRowState("personal-details-section-5")).isExisting()).toBe(false); + await expect(await $(HubPage.summaryRowState("visitors-section-1")).isExisting()).toBe(false); + + // Start section for first visitor + await $(HubPage.summaryRowLink("section")).click(); + + // Add first visitor + await $(SectionSummaryPage.visitorListAddLink()).click(); + await $(VisitorsListCollectorAddPage.visitorFirstName()).setValue("Joe"); + await $(VisitorsListCollectorAddPage.visitorLastName()).setValue("Public"); + await click(VisitorsListCollectorAddPage.submit()); + await verifyUrlContains("/questionnaire/visitors-block"); + + // Add second visitor + await $(VisitorsListCollectorPage.yes()).click(); + await click(VisitorsListCollectorPage.submit()); + await $(VisitorsListCollectorAddPage.visitorFirstName()).setValue("Yvonne"); + await $(VisitorsListCollectorAddPage.visitorLastName()).setValue("Yoe"); + await click(VisitorsListCollectorAddPage.submit()); + + // Exit the visitors list collector + await $(VisitorsListCollectorPage.no()).click(); + await click(VisitorsListCollectorPage.submit()); + + await click(SectionSummaryPage.submit()); + + await expect(await $(HubPage.summaryRowState("visitors-section-1")).getText()).toBe("Not started"); + await expect(await $(HubPage.summaryRowTitle("visitors-section-1")).getText()).toBe("Joe Public"); + await expect(await $(HubPage.summaryRowState("visitors-section-2")).getText()).toBe("Not started"); + await expect(await $(HubPage.summaryRowTitle("visitors-section-2")).getText()).toBe("Yvonne Yoe"); + + await expect(await $(HubPage.summaryRowState("visitors-section-3")).isExisting()).toBe(false); + }); + + it("When the user clicks 'Continue' from the Hub, Then they should progress to the first incomplete section", async () => { + await click(HubPage.submit()); + await expect(await $(ConfirmDateOfBirthPage.questionText()).getText()).toBe("What is Marcus Twin’s sex?"); + }); + + it("When the user answers on their behalf, Then they are shown the non proxy question variant", async () => { + await $(HubPage.summaryRowLink("personal-details-section-4")).click(); + await $(ProxyPage.noIMAnsweringForMyself()).click(); + await click(ProxyPage.submit()); + + await $(DateOfBirthPage.day()).setValue("07"); + await $(DateOfBirthPage.month()).setValue("07"); + await $(DateOfBirthPage.year()).setValue("1970"); + await click(DateOfBirthPage.submit()); + + await $(ConfirmDateOfBirthPage.confirmDateOfBirthYesIAmAgeOld()).click(); + await click(ConfirmDateOfBirthPage.submit()); + + await expect(await $(SexPage.questionText()).getText()).toBe("What is your sex?"); + }); + + it("When the user answers on on behalf of someone else, Then they are shown the proxy question variant for the relevant repeating section", async () => { + await $(HubPage.summaryRowLink("personal-details-section-3")).click(); + await $(ProxyPage.yes()).click(); + await click(ProxyPage.submit()); + + await $(DateOfBirthPage.day()).setValue("11"); + await $(DateOfBirthPage.month()).setValue("11"); + await $(DateOfBirthPage.year()).setValue("1990"); + await click(DateOfBirthPage.submit()); + + await $(ConfirmDateOfBirthPage.confirmDateOfBirthYesPersonNameIsAgeOld()).click(); + await click(ConfirmDateOfBirthPage.submit()); + await expect(await $(SexPage.questionText()).getText()).toBe("What is Samuel Clemens’ sex?"); + }); + + it("When the user completes all sections, Then the Hub should be in the completed state", async () => { + // Complete remaining sections + await click(HubPage.submit()); + await $(SexPage.male()).click(); + await click(SexPage.submit()); + await click(PersonalDetailsSummaryPage.submit()); + + await click(HubPage.submit()); + await click(SexPage.submit()); + await click(PersonalDetailsSummaryPage.submit()); + + await click(HubPage.submit()); + await $(SexPage.female()).click(); + await click(SexPage.submit()); + await click(PersonalDetailsSummaryPage.submit()); + + await click(HubPage.submit()); + await $(VisitorsDateOfBirthPage.day()).setValue("03"); + await $(VisitorsDateOfBirthPage.month()).setValue("09"); + await $(VisitorsDateOfBirthPage.year()).setValue("1975"); + await click(VisitorsDateOfBirthPage.submit()); + + await click(HubPage.submit()); + await $(VisitorsDateOfBirthPage.day()).setValue("31"); + await $(VisitorsDateOfBirthPage.month()).setValue("07"); + await $(VisitorsDateOfBirthPage.year()).setValue("1999"); + await click(VisitorsDateOfBirthPage.submit()); + + await expect(await $(HubPage.submit()).getText()).toBe("Submit survey"); + await expect(await $(HubPage.heading()).getText()).toBe("Submit survey"); + }); + + it("When the user adds a new visitor, Then the Hub should not be in the completed state", async () => { + await $(HubPage.summaryRowLink("section")).click(); + + // Add another visitor + await $(SectionSummaryPage.visitorListAddLink()).click(); + await $(VisitorsListCollectorAddPage.visitorFirstName()).setValue("Anna"); + await $(VisitorsListCollectorAddPage.visitorLastName()).setValue("Doe"); + await click(VisitorsListCollectorAddPage.submit()); + + await $(VisitorsListCollectorPage.no()).click(); + await click(VisitorsListCollectorPage.submit()); + + await click(SectionSummaryPage.submit()); + + // New visitor added to hub + await expect(await $(HubPage.summaryRowState("visitors-section-3")).getText()).toBe("Not started"); + await expect(await $(HubPage.summaryRowState("visitors-section-3")).isExisting()).toBe(true); + + await expect(await $(HubPage.submit()).getText()).not.toBe("Submit survey"); + await expect(await $(HubPage.submit()).getText()).toBe("Continue"); + + await expect(await $(HubPage.heading()).getText()).not.toBe("Submit survey"); + await expect(await $(HubPage.heading()).getText()).toBe("Choose another section to complete"); + }); + + it("When the user removes a visitor, Then their section is not longer displayed on he Hub", async () => { + // Ensure final householder exists + await expect(await $(HubPage.summaryRowState("visitors-section-3")).isExisting()).toBe(true); + + await $(HubPage.summaryRowLink("section")).click(); + + // Remove final visitor + await $(SectionSummaryPage.visitorListRemoveLink(3)).click(); + + await $(VisitorsListCollectorRemovePage.yes()).click(); + await click(VisitorsListCollectorPage.submit()); + await click(SectionSummaryPage.submit()); + + // Ensure final householder no longer exists + await expect(await $(HubPage.summaryRowState("visitors-section-3")).isExisting()).toBe(false); + }); + + it("When the user submits, it should show the thank you page", async () => { + await click(HubPage.submit()); + await verifyUrlContains("thank-you"); + }); + }); +}); diff --git a/tests/functional/spec/journeys/routing/all_in.spec.js b/tests/functional/spec/journeys/routing/all_in.spec.js new file mode 100644 index 0000000000..c29c644094 --- /dev/null +++ b/tests/functional/spec/journeys/routing/all_in.spec.js @@ -0,0 +1,32 @@ +import CountryCheckboxPage from "../../../generated_pages/routing_checkbox_contains_all/country-checkbox.page"; +import CountryInterstitialPage from "../../../generated_pages/routing_checkbox_contains_all/country-interstitial-india-and-malta.page"; +import CountryInterstitialOtherPage from "../../../generated_pages/routing_checkbox_contains_all/country-interstitial-not-india-and-malta.page"; +import { click, verifyUrlContains } from "../../../helpers"; + +describe("Feature: Routing - ALL-IN Operator", () => { + describe("Equals", () => { + describe("Given I start the ALL-IN operator routing survey", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_routing_checkbox_contains_all.json"); + }); + + it("When I do select India and Malta, Then I should be routed to the correct answer interstitial page", async () => { + await $(CountryCheckboxPage.india()).click(); + await $(CountryCheckboxPage.malta()).click(); + await click(CountryCheckboxPage.submit()); + await verifyUrlContains(CountryInterstitialPage.pageName); + }); + it("When I do select India only, Then I should be routed to the correct answer interstitial page", async () => { + await $(CountryCheckboxPage.india()).click(); + await click(CountryCheckboxPage.submit()); + await verifyUrlContains(CountryInterstitialOtherPage.pageName); + }); + + it("When I do not select India or Malta, Then I should be routed to the incorrect answer interstitial page", async () => { + await $(CountryCheckboxPage.liechtenstein()).click(); + await click(CountryCheckboxPage.submit()); + await verifyUrlContains(CountryInterstitialOtherPage.pageName); + }); + }); + }); +}); diff --git a/tests/functional/spec/journeys/routing/and.spec.js b/tests/functional/spec/journeys/routing/and.spec.js new file mode 100644 index 0000000000..680ae1878c --- /dev/null +++ b/tests/functional/spec/journeys/routing/and.spec.js @@ -0,0 +1,47 @@ +import FirstNumberQuestionPage from "../../../generated_pages/routing_and/number-question-1.page"; +import SecondNumberQuestionPage from "../../../generated_pages/routing_and/number-question-2.page"; +import CorrectAnswerPage from "../../../generated_pages/routing_and/correct-answer.page"; +import IncorrectAnswerPage from "../../../generated_pages/routing_and/incorrect-answer.page"; +import { click, verifyUrlContains } from "../../../helpers"; + +describe("Feature: Routing - And Operator", () => { + describe("Equals", () => { + describe("Given I start the and operator routing survey", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_routing_and.json"); + }); + + it("When I enter both answers correctly with 123 and 321, Then I should be routed to the correct page", async () => { + await $(FirstNumberQuestionPage.answer1()).setValue(123); + await click(FirstNumberQuestionPage.submit()); + await $(SecondNumberQuestionPage.answer2()).setValue(321); + await click(SecondNumberQuestionPage.submit()); + await verifyUrlContains(CorrectAnswerPage.pageName); + }); + + it("When I only enter the second answer correctly with 555 and 321, Then I should be routed to the incorrect page", async () => { + await $(FirstNumberQuestionPage.answer1()).setValue(555); + await click(FirstNumberQuestionPage.submit()); + await $(SecondNumberQuestionPage.answer2()).setValue(321); + await click(SecondNumberQuestionPage.submit()); + await verifyUrlContains(IncorrectAnswerPage.pageName); + }); + + it("When I only enter the first answer correctly with 123 and 555, Then I should be routed to the incorrect page", async () => { + await $(FirstNumberQuestionPage.answer1()).setValue(123); + await click(FirstNumberQuestionPage.submit()); + await $(SecondNumberQuestionPage.answer2()).setValue(555); + await click(SecondNumberQuestionPage.submit()); + await verifyUrlContains(IncorrectAnswerPage.pageName); + }); + + it("When I answer both questions incorrectly with 555 and 444, Then I should be routed to the incorrect page", async () => { + await $(FirstNumberQuestionPage.answer1()).setValue(555); + await click(FirstNumberQuestionPage.submit()); + await $(SecondNumberQuestionPage.answer2()).setValue(444); + await click(SecondNumberQuestionPage.submit()); + await verifyUrlContains(IncorrectAnswerPage.pageName); + }); + }); + }); +}); diff --git a/tests/functional/spec/journeys/routing/answer_action_redirect_to_list_add_block_checkbox.spec.js b/tests/functional/spec/journeys/routing/answer_action_redirect_to_list_add_block_checkbox.spec.js new file mode 100644 index 0000000000..bc703450a7 --- /dev/null +++ b/tests/functional/spec/journeys/routing/answer_action_redirect_to_list_add_block_checkbox.spec.js @@ -0,0 +1,71 @@ +import { checkItemsInList, click, verifyUrlContains } from "../../../helpers"; +import AnyoneLiveAtListCollector from "../../../generated_pages/answer_action_redirect_to_list_add_block_checkbox/anyone-else-live-at.page"; +import AnyoneLiveAtListCollectorAddPage from "../../../generated_pages/answer_action_redirect_to_list_add_block_checkbox/anyone-else-live-at-add.page"; +import AnyoneLiveAtListCollectorRemovePage from "../../../generated_pages/answer_action_redirect_to_list_add_block_checkbox/anyone-else-live-at-remove.page"; +import AnyoneUsuallyLiveAt from "../../../generated_pages/answer_action_redirect_to_list_add_block_checkbox/anyone-usually-live-at.page"; + +describe("Answer Action: Redirect To List Add Question (Checkbox)", () => { + describe('Given the user is on a question with a "RedirectToListAddBlock" action enabled', () => { + before("Launch survey", async () => { + await browser.openQuestionnaire("test_answer_action_redirect_to_list_add_block_checkbox.json"); + }); + + it('When the user selects "No", Then, they should be taken to the list collector.', async () => { + await $(AnyoneUsuallyLiveAt.no()).click(); + await click(AnyoneUsuallyLiveAt.submit()); + await verifyUrlContains(AnyoneLiveAtListCollector.pageName); + }); + + it('When the user selects "Yes" then they should be taken to the list collector add question.', async () => { + await browser.url(AnyoneUsuallyLiveAt.url()); + await $(AnyoneUsuallyLiveAt.iThinkSo()).click(); + await click(AnyoneUsuallyLiveAt.submit()); + await verifyUrlContains(AnyoneLiveAtListCollectorAddPage.pageName); + await verifyUrlContains("?previous=anyone-usually-live-at"); + }); + + it('When the user clicks the "Previous" link from the add question then they should be taken to the block they came from, not the list collector', async () => { + await $(AnyoneLiveAtListCollectorAddPage.previous()).click(); + await verifyUrlContains(AnyoneUsuallyLiveAt.pageName); + }); + + it("When the user adds a household member, Then, they are taken to the list collector and the household members are displayed", async () => { + await click(AnyoneUsuallyLiveAt.submit()); + await $(AnyoneLiveAtListCollectorAddPage.firstName()).setValue("Marcus"); + await $(AnyoneLiveAtListCollectorAddPage.lastName()).setValue("Twin"); + await click(AnyoneLiveAtListCollectorAddPage.submit()); + await verifyUrlContains(AnyoneLiveAtListCollector.pageName); + + const peopleExpected = ["Marcus Twin"]; + await checkItemsInList(peopleExpected, AnyoneLiveAtListCollector.listLabel); + }); + + it('When the user click the "Previous" link from the list collector, Then, they are taken to the last complete block', async () => { + await $(AnyoneLiveAtListCollector.previous()).click(); + await verifyUrlContains(AnyoneUsuallyLiveAt.pageName); + }); + + it("When the user resubmits the first block and then list is not empty, Then they are taken to the list collector", async () => { + await click(AnyoneUsuallyLiveAt.submit()); + await verifyUrlContains(AnyoneLiveAtListCollector.pageName); + }); + + it("When the users removes the only person (Marcus Twain), Then, they are shown an empty list collector", async () => { + await $(AnyoneLiveAtListCollector.listRemoveLink(1)).click(); + await $(AnyoneLiveAtListCollectorRemovePage.yes()).click(); + await click(AnyoneLiveAtListCollectorRemovePage.submit()); + await verifyUrlContains(AnyoneLiveAtListCollector.pageName); + await expect(await $(AnyoneLiveAtListCollector.listLabel(1)).isExisting()).toBe(false); + }); + + it("When the user resubmits the first block and then list is empty, Then they are taken to the add question", async () => { + await verifyUrlContains(AnyoneLiveAtListCollector.pageName); + + await $(AnyoneLiveAtListCollector.previous()).click(); + await verifyUrlContains(AnyoneUsuallyLiveAt.pageName); + + await click(AnyoneUsuallyLiveAt.submit()); + await verifyUrlContains(AnyoneLiveAtListCollectorAddPage.pageName); + }); + }); +}); diff --git a/tests/functional/spec/journeys/routing/answer_action_redirect_to_list_add_block_radio.spec.js b/tests/functional/spec/journeys/routing/answer_action_redirect_to_list_add_block_radio.spec.js new file mode 100644 index 0000000000..b6c4f26d6d --- /dev/null +++ b/tests/functional/spec/journeys/routing/answer_action_redirect_to_list_add_block_radio.spec.js @@ -0,0 +1,71 @@ +import { checkItemsInList, click, verifyUrlContains } from "../../../helpers"; +import AnyoneLiveAtListCollector from "../../../generated_pages/answer_action_redirect_to_list_add_block_radio/anyone-else-live-at.page"; +import AnyoneLiveAtListCollectorAddPage from "../../../generated_pages/answer_action_redirect_to_list_add_block_radio/anyone-else-live-at-add.page"; +import AnyoneLiveAtListCollectorRemovePage from "../../../generated_pages/answer_action_redirect_to_list_add_block_radio/anyone-else-live-at-remove.page"; +import AnyoneUsuallyLiveAt from "../../../generated_pages/answer_action_redirect_to_list_add_block_radio/anyone-usually-live-at.page"; + +describe("Answer Action: Redirect To List Add Question (Radio)", () => { + describe('Given the user is on a question with a "RedirectToListAddBlock" action enabled', () => { + before("Launch survey", async () => { + await browser.openQuestionnaire("test_answer_action_redirect_to_list_add_block_radio.json"); + }); + + it('When the user answers "No", Then, they should be taken to straight the list collector.', async () => { + await $(AnyoneUsuallyLiveAt.no()).click(); + await click(AnyoneUsuallyLiveAt.submit()); + await verifyUrlContains(AnyoneLiveAtListCollector.pageName); + }); + + it('When the user answers "Yes" then they should be taken to the list collector add question.', async () => { + await browser.url(AnyoneUsuallyLiveAt.url()); + await $(AnyoneUsuallyLiveAt.yes()).click(); + await click(AnyoneUsuallyLiveAt.submit()); + await verifyUrlContains(AnyoneLiveAtListCollectorAddPage.pageName); + await verifyUrlContains("?previous=anyone-usually-live-at"); + }); + + it('When the user clicks the "Previous" link from the add question then they should be taken to the block they came from, not the list collector', async () => { + await $(AnyoneLiveAtListCollectorAddPage.previous()).click(); + await verifyUrlContains(AnyoneUsuallyLiveAt.pageName); + }); + + it("When the user adds a household member, Then, they are taken to the list collector and the household members are displayed", async () => { + await click(AnyoneUsuallyLiveAt.submit()); + await $(AnyoneLiveAtListCollectorAddPage.firstName()).setValue("Marcus"); + await $(AnyoneLiveAtListCollectorAddPage.lastName()).setValue("Twin"); + await click(AnyoneLiveAtListCollectorAddPage.submit()); + await verifyUrlContains(AnyoneLiveAtListCollector.pageName); + + const peopleExpected = ["Marcus Twin"]; + await checkItemsInList(peopleExpected, AnyoneLiveAtListCollector.listLabel); + }); + + it('When the user click the "Previous" link from the list collector, Then, they are taken to the last complete block', async () => { + await $(AnyoneLiveAtListCollector.previous()).click(); + await verifyUrlContains(AnyoneUsuallyLiveAt.pageName); + }); + + it("When the user resubmits the first block and then list is not empty, Then they are taken to the list collector", async () => { + await click(AnyoneUsuallyLiveAt.submit()); + await verifyUrlContains(AnyoneLiveAtListCollector.pageName); + }); + + it("When the users removes the only person (Marcus Twain), Then, they are shown an empty list collector", async () => { + await $(AnyoneLiveAtListCollector.listRemoveLink(1)).click(); + await $(AnyoneLiveAtListCollectorRemovePage.yes()).click(); + await click(AnyoneLiveAtListCollectorRemovePage.submit()); + await verifyUrlContains(AnyoneLiveAtListCollector.pageName); + await expect(await $(AnyoneLiveAtListCollector.listLabel(1)).isExisting()).toBe(false); + }); + + it("When the user resubmits the first block and then list is empty, Then they are taken to the add question", async () => { + await verifyUrlContains(AnyoneLiveAtListCollector.pageName); + + await $(AnyoneLiveAtListCollector.previous()).click(); + await verifyUrlContains(AnyoneUsuallyLiveAt.pageName); + + await click(AnyoneUsuallyLiveAt.submit()); + await verifyUrlContains(AnyoneLiveAtListCollectorAddPage.pageName); + }); + }); +}); diff --git a/tests/functional/spec/journeys/routing/answer_comparison_routing.spec.js b/tests/functional/spec/journeys/routing/answer_comparison_routing.spec.js new file mode 100644 index 0000000000..2a453e33c5 --- /dev/null +++ b/tests/functional/spec/journeys/routing/answer_comparison_routing.spec.js @@ -0,0 +1,33 @@ +import RouteComparison1Page from "../../../generated_pages/routing_answer_comparison/route-comparison-1.page.js"; +import RouteComparison2Page from "../../../generated_pages/routing_answer_comparison/route-comparison-2.page.js"; +import { click } from "../../../helpers"; + +describe("Test routing skip", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_routing_answer_comparison.json"); + }); + + it("Given we start the routing test survey, When we enter a low number then a high number, Then, we should be routed to the fourth page", async () => { + await $(RouteComparison1Page.answer()).setValue(1); + await click(RouteComparison1Page.submit()); + await $(RouteComparison2Page.answer()).setValue(2); + await click(RouteComparison2Page.submit()); + await expect(await $("#main-content > p").getText()).toBe("This page should never be skipped"); + }); + + it("Given we start the routing test survey, When we enter a high number then a low number, Then, we should be routed to the third page", async () => { + await $(RouteComparison1Page.answer()).setValue(1); + await click(RouteComparison1Page.submit()); + await $(RouteComparison2Page.answer()).setValue(0); + await click(RouteComparison2Page.submit()); + await expect(await $("#main-content > p").getText()).toBe("This page should be skipped if your second answer was higher than your first"); + }); + + it("Given we start the routing test survey, When we enter an equal number on both questions, Then, we should be routed to the third page", async () => { + await $(RouteComparison1Page.answer()).setValue(1); + await click(RouteComparison1Page.submit()); + await $(RouteComparison2Page.answer()).setValue(1); + await click(RouteComparison2Page.submit()); + await expect(await $("#main-content > p").getText()).toBe("This page should be skipped if your second answer was higher than your first"); + }); +}); diff --git a/tests/functional/spec/journeys/routing/answer_not_on_path.spec.js b/tests/functional/spec/journeys/routing/answer_not_on_path.spec.js new file mode 100644 index 0000000000..45c137c5e2 --- /dev/null +++ b/tests/functional/spec/journeys/routing/answer_not_on_path.spec.js @@ -0,0 +1,37 @@ +import InitialChoicePage from "../../../generated_pages/routing_not_affected_by_answers_not_on_path/initial-choice.page.js"; +import InvalidPathPage from "../../../generated_pages/routing_not_affected_by_answers_not_on_path/invalid-path.page.js"; +import InvalidPathInterstitialPage from "../../../generated_pages/routing_not_affected_by_answers_not_on_path/invalid-path-interstitial.page.js"; +import ValidPathPage from "../../../generated_pages/routing_not_affected_by_answers_not_on_path/valid-path.page.js"; +import ValidFinalInterstitialPage from "../../../generated_pages/routing_not_affected_by_answers_not_on_path/valid-final-interstitial.page.js"; +import { click, verifyUrlContains } from "../../../helpers"; +describe("Answers not on path are not considered when routing", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_routing_not_affected_by_answers_not_on_path.json"); + }); + + it("Given the user enters an answer on the first path, when they return to the second path, they should be routed to the valid path interstitial", async () => { + await $(InitialChoicePage.goHereFirst()).click(); + await click(InitialChoicePage.submit()); + + await verifyUrlContains(InvalidPathPage.pageName); + await $(InvalidPathPage.answer()).setValue(123); + await click(InvalidPathPage.submit()); + + // We now have an answer in the store on the 'invalid' path + + await verifyUrlContains(InvalidPathInterstitialPage.pageName); + await $(InvalidPathInterstitialPage.previous()).click(); + await $(InvalidPathPage.previous()).click(); + + // Take the second route + + await $(InitialChoicePage.goHereSecond()).click(); + await click(InitialChoicePage.submit()); + + await $(ValidPathPage.answer()).setValue(321); + await click(ValidPathPage.submit()); + + // We should be routed to the valid interstitial page since the invalid path answer should not be considered whilst routing. + await verifyUrlContains(ValidFinalInterstitialPage.pageName); + }); +}); diff --git a/tests/functional/spec/journeys/routing/answered_unanswered.spec.js b/tests/functional/spec/journeys/routing/answered_unanswered.spec.js new file mode 100644 index 0000000000..e54f06b62c --- /dev/null +++ b/tests/functional/spec/journeys/routing/answered_unanswered.spec.js @@ -0,0 +1,94 @@ +import QuestionOne from "../../../generated_pages/routing_answered_unanswered/block-1.page"; +import QuestionOneAnswered from "../../../generated_pages/routing_answered_unanswered/answered-question-1.page"; +import QuestionOneUnanswered from "../../../generated_pages/routing_answered_unanswered/unanswered-question-1.page"; + +import QuestionTwo from "../../../generated_pages/routing_answered_unanswered/block-2.page"; +import QuestionTwoAnswered from "../../../generated_pages/routing_answered_unanswered/answered-question-2.page"; +import QuestionTwoUnanswered from "../../../generated_pages/routing_answered_unanswered/unanswered-question-2.page"; + +import QuestionThree from "../../../generated_pages/routing_answered_unanswered/block-3.page"; +import QuestionThreeAnsweredOrNotZero from "../../../generated_pages/routing_answered_unanswered/answered-question-3.page"; +import QuestionThreeUnansweredOrAnswerZero from "../../../generated_pages/routing_answered_unanswered/unanswered-or-zero-question-3.page"; +import { click, verifyUrlContains } from "../../../helpers"; +describe("Test routing question answered/unanswered", () => { + describe("Given I am on the first question", () => { + beforeEach("Load the questionnaire", async () => { + await browser.openQuestionnaire("test_routing_answered_unanswered.json"); + }); + + it("When I select any answer and submit, Then I should see a page saying I have answered the first question", async () => { + await $(QuestionOne.ham()).click(); + await click(QuestionOne.submit()); + await expect(await $(QuestionOneAnswered.heading()).getText()).toBe("You answered the first question!"); + await verifyUrlContains(QuestionOneAnswered.pageName); + + await $(QuestionOneAnswered.previous()).click(); + await $(QuestionOne.cheese()).click(); + await click(QuestionOne.submit()); + await expect(await $(QuestionOneAnswered.heading()).getText()).toBe("You answered the first question!"); + await verifyUrlContains(QuestionOneAnswered.pageName); + }); + + it("When I don't select an answer and submit, Then I should see a page saying I have not answered the first question", async () => { + await click(QuestionOne.submit()); + await expect(await $(QuestionOneAnswered.heading()).getText()).toBe("You did not answer the first question!"); + await verifyUrlContains(QuestionOneAnswered.pageName); + }); + }); + + describe("Given I am on the second question", () => { + beforeEach("Load the questionnaire and get to the second question", async () => { + await browser.openQuestionnaire("test_routing_answered_unanswered.json"); + await click(QuestionOne.submit()); + await click(QuestionOneUnanswered.submit()); + }); + + it("When I select any answer and submit, Then I should see a page saying I have answered the second question", async () => { + await $(QuestionTwo.pizzaHut()).click(); + await click(QuestionTwo.submit()); + await expect(await $(QuestionTwoAnswered.heading()).getText()).toBe("You answered the second question!"); + await verifyUrlContains(QuestionTwoAnswered.pageName); + + await $(QuestionOneAnswered.previous()).click(); + await $(QuestionTwo.dominoS()).click(); + await click(QuestionTwo.submit()); + await expect(await $(QuestionTwoAnswered.heading()).getText()).toBe("You answered the second question!"); + await verifyUrlContains(QuestionTwoAnswered.pageName); + }); + + it("When I don't select an answer and submit, Then I should see a page saying I have not answered the second question", async () => { + await click(QuestionTwo.submit()); + await expect(await $(QuestionTwoUnanswered.heading()).getText()).toBe("You did not answer the second question!"); + await verifyUrlContains(QuestionTwoAnswered.pageName); + }); + }); + + describe("Given I am on the third question", () => { + beforeEach("Load the questionnaire and get to the third question", async () => { + await browser.openQuestionnaire("test_routing_answered_unanswered.json"); + await click(QuestionOne.submit()); + await click(QuestionOneUnanswered.submit()); + await click(QuestionTwo.submit()); + await click(QuestionTwoUnanswered.submit()); + }); + + it("When I do not answer the question or answer `0` and submit, Then I should see a page saying I did not answer the question or that I chose `0`", async () => { + await click(QuestionThree.submit()); + await expect(await $(QuestionThreeUnansweredOrAnswerZero.heading()).getText()).toBe("You did not answer the question or chose 0 slices"); + await verifyUrlContains(QuestionThreeUnansweredOrAnswerZero.pageName); + + await $(QuestionThreeUnansweredOrAnswerZero.previous()).click(); + await $(QuestionThree.answer3()).setValue("0"); + await click(QuestionThree.submit()); + await expect(await $(QuestionThreeUnansweredOrAnswerZero.heading()).getText()).toBe("You did not answer the question or chose 0 slices"); + await verifyUrlContains(QuestionThreeUnansweredOrAnswerZero.pageName); + }); + + it("When I enter an answer greater than 0 and submit, Then I should see a page saying I chose at least one", async () => { + await $(QuestionThree.answer3()).setValue("2"); + await click(QuestionThree.submit()); + await expect(await $(QuestionTwoAnswered.heading()).getText()).toBe("You chose at least 1 slice"); + await verifyUrlContains(QuestionThreeAnsweredOrNotZero.pageName); + }); + }); +}); diff --git a/tests/functional/spec/journeys/routing/any_in.spec.js b/tests/functional/spec/journeys/routing/any_in.spec.js new file mode 100644 index 0000000000..129a5fa4d3 --- /dev/null +++ b/tests/functional/spec/journeys/routing/any_in.spec.js @@ -0,0 +1,31 @@ +import CountryCheckboxPage from "../../../generated_pages/routing_checkbox_contains_any/country-checkbox.page"; +import CountryInterstitialPage from "../../../generated_pages/routing_checkbox_contains_any/country-interstitial-india-or-malta-or-both.page"; +import CountryInterstitialOtherPage from "../../../generated_pages/routing_checkbox_contains_any/country-interstitial-not-india-or-malta-or-both.page"; +import { click, verifyUrlContains } from "../../../helpers"; +describe("Feature: Routing - ANY-IN Operator", () => { + describe("Equals", () => { + describe("Given I start the ANY-IN operator routing survey", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_routing_checkbox_contains_any.json"); + }); + + it("When I do select India and Malta, Then I should be routed to the correct answer interstitial page", async () => { + await $(CountryCheckboxPage.india()).click(); + await $(CountryCheckboxPage.malta()).click(); + await click(CountryCheckboxPage.submit()); + await verifyUrlContains(CountryInterstitialPage.pageName); + }); + + it("When I do select India or Malta, Then I should be routed to the correct answer interstitial page", async () => { + await $(CountryCheckboxPage.india()).click(); + await click(CountryCheckboxPage.submit()); + await verifyUrlContains(CountryInterstitialPage.pageName); + }); + it("When I do not select India or Malta, Then I should be routed to the incorrect answer interstitial page", async () => { + await $(CountryCheckboxPage.liechtenstein()).click(); + await click(CountryCheckboxPage.submit()); + await verifyUrlContains(CountryInterstitialOtherPage.pageName); + }); + }); + }); +}); diff --git a/tests/functional/spec/journeys/routing/boolean.spec.js b/tests/functional/spec/journeys/routing/boolean.spec.js new file mode 100644 index 0000000000..1b2a4ca021 --- /dev/null +++ b/tests/functional/spec/journeys/routing/boolean.spec.js @@ -0,0 +1,22 @@ +import Block1Page from "../../../generated_pages/metadata_routing/block1.page"; +import Block2Page from "../../../generated_pages/metadata_routing/block2.page"; +import Block3Page from "../../../generated_pages/metadata_routing/block3.page"; +import { click, verifyUrlContains } from "../../../helpers"; + +describe("Feature: Routing - Boolean Flag", () => { + it("Given I have a routing rule that uses a boolean flag and it is False, When I press continue, Then I should be routed to the correct page", async () => { + await browser.openQuestionnaire("test_metadata_routing.json", { + booleanFlag: false, + }); + await click(Block1Page.submit()); + await verifyUrlContains(Block2Page.pageName); + }); + + it("Given I have a routing rule that uses a boolean flag and it is True, When I press continue, Then I should be routed to the correct page ", async () => { + await browser.openQuestionnaire("test_metadata_routing.json", { + booleanFlag: true, + }); + await click(Block1Page.submit()); + await verifyUrlContains(Block3Page.pageName); + }); +}); diff --git a/tests/functional/spec/journeys/routing/checkbox_count.spec.js b/tests/functional/spec/journeys/routing/checkbox_count.spec.js new file mode 100644 index 0000000000..7e4f5c75b8 --- /dev/null +++ b/tests/functional/spec/journeys/routing/checkbox_count.spec.js @@ -0,0 +1,43 @@ +import ToppingCheckboxPage from "../../../generated_pages/routing_checkbox_count/topping-checkbox.page.js"; +import CorrectAnswerPage from "../../../generated_pages/routing_checkbox_count/correct-answer.page"; +import IncorrectAnswerPage from "../../../generated_pages/routing_checkbox_count/incorrect-answer.page"; +import { click, verifyUrlContains } from "../../../helpers"; +describe("Test routing using count of checkboxes checked", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_routing_checkbox_count.json"); + }); + + it("Given a user selects 2 checkboxes, When they submit, Then they should be routed to the correct page", async () => { + await $(ToppingCheckboxPage.cheese()).click(); + await $(ToppingCheckboxPage.ham()).click(); + await click(ToppingCheckboxPage.submit()); + + await verifyUrlContains(CorrectAnswerPage.pageName); + await expect(await $(CorrectAnswerPage.questionText()).getText()).toBe("You selected 2 or more toppings"); + }); + + it("Given a user selects no checkboxes, When they submit, Then they should be routed to the incorrect page", async () => { + await click(ToppingCheckboxPage.submit()); + + await verifyUrlContains(IncorrectAnswerPage.pageName); + await expect(await $(IncorrectAnswerPage.questionText()).getText()).toBe("You did not select 2 or more toppings"); + }); + + it("Given a user selects 1 checkbox, When they submit, Then they should be routed to the incorrect page", async () => { + await $(ToppingCheckboxPage.cheese()).click(); + await click(ToppingCheckboxPage.submit()); + + await verifyUrlContains(IncorrectAnswerPage.pageName); + await expect(await $(IncorrectAnswerPage.questionText()).getText()).toBe("You did not select 2 or more toppings"); + }); + + it("Given a user selects 3 checkbox, When they submit, Then they should be routed to the correct page", async () => { + await $(ToppingCheckboxPage.cheese()).click(); + await $(ToppingCheckboxPage.ham()).click(); + await $(ToppingCheckboxPage.pineapple()).click(); + await click(ToppingCheckboxPage.submit()); + + await verifyUrlContains(CorrectAnswerPage.pageName); + await expect(await $(CorrectAnswerPage.questionText()).getText()).toBe("You selected 2 or more toppings"); + }); +}); diff --git a/tests/functional/spec/journeys/routing/conditional_combined_routing.spec.js b/tests/functional/spec/journeys/routing/conditional_combined_routing.spec.js new file mode 100644 index 0000000000..bd32466634 --- /dev/null +++ b/tests/functional/spec/journeys/routing/conditional_combined_routing.spec.js @@ -0,0 +1,45 @@ +import ConditionalCombinedRoutingPage from "../../../generated_pages/conditional_combined_routing/conditional-routing-block.page"; +import ResponseAny from "../../../generated_pages/conditional_combined_routing/response-any.page"; +import ResponseNotAny from "../../../generated_pages/conditional_combined_routing/response-not-any.page"; +import SubmitPage from "../../../generated_pages/conditional_combined_routing/submit.page"; +import { click, verifyUrlContains } from "../../../helpers"; + +describe("Conditional combined routing.", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_conditional_combined_routing.json"); + }); + + it('Given a list of radio options, when I choose the option "Yes" or the option "Sometimes" then I should be routed to the relevant page', async () => { + // When + await $(ConditionalCombinedRoutingPage.yes()).click(); + await click(ConditionalCombinedRoutingPage.submit()); + // Then + await verifyUrlContains(ResponseAny.pageName); + + // Or + await $(ResponseAny.previous()).click(); + + // When + await $(ConditionalCombinedRoutingPage.sometimes()).click(); + await click(ConditionalCombinedRoutingPage.submit()); + + // Then + await verifyUrlContains(ResponseAny.pageName); + }); + + it('Given a list of radio options, when I choose the option "No, I prefer tea" then I should be routed to the relevant page', async () => { + // When + await $(ConditionalCombinedRoutingPage.noIPreferTea()).click(); + await click(ConditionalCombinedRoutingPage.submit()); + // Then + await verifyUrlContains(ResponseNotAny.pageName); + }); + + it('Given a list of radio options, when I choose the option "No, I don\'t drink any hot drinks" then I should be routed to the submit page', async () => { + // When + await $(ConditionalCombinedRoutingPage.noIDonTDrinkAnyHotDrinks()).click(); + await click(ConditionalCombinedRoutingPage.submit()); + // Then + await verifyUrlContains(SubmitPage.pageName); + }); +}); diff --git a/tests/functional/spec/journeys/routing/date.spec.js b/tests/functional/spec/journeys/routing/date.spec.js new file mode 100644 index 0000000000..d9418b57da --- /dev/null +++ b/tests/functional/spec/journeys/routing/date.spec.js @@ -0,0 +1,250 @@ +import IncorrectAnswerPage from "../../../generated_pages/routing_date_equals/incorrect-answer.page.js"; +import CorrectAnswerPage from "../../../generated_pages/routing_date_equals/correct-answer.page.js"; + +import DateEqualsComparisonQuestionPage from "../../../generated_pages/routing_date_equals/comparison-date-block.page"; +import DateEqualsQuestionPage from "../../../generated_pages/routing_date_equals/date-question.page"; +import DateNotEqualsQuestionPage from "../../../generated_pages/routing_date_not_equals/date-question.page"; +import DateGreaterThanQuestionPage from "../../../generated_pages/routing_date_greater_than/date-question.page"; +import DateGreaterThanOrEqualsQuestionPage from "../../../generated_pages/routing_date_greater_than_or_equals/date-question.page"; +import DateLessThanQuestionPage from "../../../generated_pages/routing_date_less_than/date-question.page"; +import DateLessThanOrEqualsQuestionPage from "../../../generated_pages/routing_date_less_than_or_equals/date-question.page"; +import { click, verifyUrlContains } from "../../../helpers"; +const today = new Date(); +const dayToday = today.getDate(); +const monthToday = today.getMonth() + 1; // January is 0! +const yearToday = today.getFullYear(); + +const yesterday = new Date(); +yesterday.setDate(today.getDate() - 1); +const dayYesterday = yesterday.getDate(); +const monthYesterday = yesterday.getMonth() + 1; +const yearYesterday = yesterday.getFullYear(); + +const tomorrow = new Date(); +tomorrow.setDate(today.getDate() + 1); +const dayTomorrow = tomorrow.getDate(); +const monthTomorrow = tomorrow.getMonth() + 1; +const yearTomorrow = tomorrow.getFullYear(); + +describe("Feature: Routing on a Date", () => { + describe("Equals", () => { + describe("Given I start date routing equals survey", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_routing_date_equals.json"); + await $(DateEqualsComparisonQuestionPage.day()).setValue(31); + await $(DateEqualsComparisonQuestionPage.month()).setValue(3); + await $(DateEqualsComparisonQuestionPage.year()).setValue(2020); + await click(DateEqualsComparisonQuestionPage.submit()); + }); + + it("When I enter the same date, Then I should be routed to the correct page", async () => { + await $(DateEqualsQuestionPage.day()).setValue(31); + await $(DateEqualsQuestionPage.month()).setValue(3); + await $(DateEqualsQuestionPage.year()).setValue(2020); + await click(DateEqualsQuestionPage.submit()); + await verifyUrlContains(CorrectAnswerPage.pageName); + }); + + it("When I enter the yesterday date, Then I should be routed to the correct page", async () => { + await $(DateEqualsQuestionPage.day()).setValue(30); + await $(DateEqualsQuestionPage.month()).setValue(3); + await $(DateEqualsQuestionPage.year()).setValue(2020); + await click(DateEqualsQuestionPage.submit()); + await verifyUrlContains(CorrectAnswerPage.pageName); + }); + + it("When I enter the tomorrow date, Then I should be routed to the correct page", async () => { + await $(DateEqualsQuestionPage.day()).setValue(1); + await $(DateEqualsQuestionPage.month()).setValue(4); + await $(DateEqualsQuestionPage.year()).setValue(2020); + await click(DateEqualsQuestionPage.submit()); + await verifyUrlContains(CorrectAnswerPage.pageName); + }); + + it("When I enter the last month date, Then I should be routed to the correct page", async () => { + await $(DateEqualsQuestionPage.day()).setValue(29); + await $(DateEqualsQuestionPage.month()).setValue(2); + await $(DateEqualsQuestionPage.year()).setValue(2020); + await click(DateEqualsQuestionPage.submit()); + await verifyUrlContains(CorrectAnswerPage.pageName); + }); + + it("When I enter the next month date, Then I should be routed to the correct page", async () => { + await $(DateEqualsQuestionPage.day()).setValue(30); + await $(DateEqualsQuestionPage.month()).setValue(4); + await $(DateEqualsQuestionPage.year()).setValue(2020); + await click(DateEqualsQuestionPage.submit()); + await verifyUrlContains(CorrectAnswerPage.pageName); + }); + + it("When I enter the last year date, Then I should be routed to the correct page", async () => { + await $(DateEqualsQuestionPage.day()).setValue(31); + await $(DateEqualsQuestionPage.month()).setValue(3); + await $(DateEqualsQuestionPage.year()).setValue(2019); + await click(DateEqualsQuestionPage.submit()); + await verifyUrlContains(CorrectAnswerPage.pageName); + }); + + it("When I enter the next year date, Then I should be routed to the correct page", async () => { + await $(DateEqualsQuestionPage.day()).setValue(31); + await $(DateEqualsQuestionPage.month()).setValue(3); + await $(DateEqualsQuestionPage.year()).setValue(2021); + await click(DateEqualsQuestionPage.submit()); + await verifyUrlContains(CorrectAnswerPage.pageName); + }); + + it("When I enter an incorrect date, Then I should be routed to the incorrect page", async () => { + await $(DateEqualsQuestionPage.day()).setValue(1); + await $(DateEqualsQuestionPage.month()).setValue(3); + await $(DateEqualsQuestionPage.year()).setValue(2020); + await click(DateEqualsComparisonQuestionPage.submit()); + await verifyUrlContains(CorrectAnswerPage.pageName); + }); + }); + }); + + describe("Not Equals", () => { + describe("Given I start date routing not equals survey", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_routing_date_not_equals.json"); + }); + + it("When I enter a different date to February 2018, Then I should be routed to the correct page", async () => { + await $(DateNotEqualsQuestionPage.Month()).setValue(3); + await $(DateNotEqualsQuestionPage.Year()).setValue(2018); + await click(DateNotEqualsQuestionPage.submit()); + + await verifyUrlContains(CorrectAnswerPage.pageName); + }); + + it("When I enter February 2018, Then I should be routed to the incorrect page", async () => { + await $(DateNotEqualsQuestionPage.Month()).setValue(2); + await $(DateNotEqualsQuestionPage.Year()).setValue(2018); + await click(DateNotEqualsQuestionPage.submit()); + await verifyUrlContains(IncorrectAnswerPage.pageName); + }); + }); + }); + + describe("Greater Than", () => { + describe("Given I start date routing greater than survey", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_routing_date_greater_than.json"); + }); + + it("When I enter a date greater than the 1st March 2017, Then I should be routed to the correct page", async () => { + await $(DateGreaterThanQuestionPage.day()).setValue(2); + await $(DateGreaterThanQuestionPage.month()).setValue(3); + await $(DateGreaterThanQuestionPage.year()).setValue(2017); + await click(DateGreaterThanQuestionPage.submit()); + await verifyUrlContains(CorrectAnswerPage.pageName); + }); + + it("When I enter the 1st March 2017, Then I should be routed to the incorrect page", async () => { + await $(DateGreaterThanQuestionPage.day()).setValue(1); + await $(DateGreaterThanQuestionPage.month()).setValue(3); + await $(DateGreaterThanQuestionPage.year()).setValue(2017); + await click(DateGreaterThanQuestionPage.submit()); + await verifyUrlContains(CorrectAnswerPage.pageName); + }); + + it("When I enter a date less than the 1st March 2017, Then I should be routed to the incorrect page", async () => { + await $(DateGreaterThanQuestionPage.day()).setValue(28); + await $(DateGreaterThanQuestionPage.month()).setValue(2); + await $(DateGreaterThanQuestionPage.year()).setValue(2017); + await click(DateGreaterThanQuestionPage.submit()); + await verifyUrlContains(IncorrectAnswerPage.pageName); + }); + }); + }); + + describe("Greater Than Or Equals", () => { + describe("Given I start date routing greater than or equals survey", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_routing_date_greater_than_or_equals.json"); + }); + + it("When I enter a date greater than 2017, Then I should be routed to the correct page", async () => { + await $(DateGreaterThanOrEqualsQuestionPage.Year()).setValue(2018); + await click(DateGreaterThanOrEqualsQuestionPage.submit()); + await verifyUrlContains(CorrectAnswerPage.pageName); + }); + + it("When I enter 2017, Then I should be routed to the correct page", async () => { + await $(DateGreaterThanOrEqualsQuestionPage.Year()).setValue(2017); + await click(DateGreaterThanOrEqualsQuestionPage.submit()); + await verifyUrlContains(CorrectAnswerPage.pageName); + }); + + it("When I enter a date less than March 2017, Then I should be routed to the incorrect page", async () => { + await $(DateGreaterThanOrEqualsQuestionPage.Year()).setValue(2016); + await click(DateGreaterThanOrEqualsQuestionPage.submit()); + await verifyUrlContains(IncorrectAnswerPage.pageName); + }); + }); + }); + + describe("Less Than", () => { + describe("Given I start date routing less than survey", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_routing_date_less_than.json"); + }); + + it("When I enter a date less than today, Then I should be routed to the correct page", async () => { + await $(DateLessThanQuestionPage.day()).setValue(dayYesterday); + await $(DateLessThanQuestionPage.month()).setValue(monthYesterday); + await $(DateLessThanQuestionPage.year()).setValue(yearYesterday); + await click(DateLessThanQuestionPage.submit()); + await verifyUrlContains(CorrectAnswerPage.pageName); + }); + + it("When I enter a date equal to today, Then I should be routed to the incorrect page", async () => { + await $(DateLessThanQuestionPage.day()).setValue(dayToday); + await $(DateLessThanQuestionPage.month()).setValue(monthToday); + await $(DateLessThanQuestionPage.year()).setValue(yearToday); + await click(DateLessThanQuestionPage.submit()); + await verifyUrlContains(IncorrectAnswerPage.pageName); + }); + + it("When I enter a date greater than today, Then I should be routed to the incorrect page", async () => { + await $(DateLessThanQuestionPage.day()).setValue(dayTomorrow); + await $(DateLessThanQuestionPage.month()).setValue(monthTomorrow); + await $(DateLessThanQuestionPage.year()).setValue(yearTomorrow); + await click(DateLessThanQuestionPage.submit()); + await verifyUrlContains(IncorrectAnswerPage.pageName); + }); + }); + }); + + describe("Less Than Or Equals", () => { + describe("Given I start date routing less than or equals survey", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_routing_date_less_than_or_equals.json"); + }); + + it("When I enter a date less than today, Then I should be routed to the correct page", async () => { + await $(DateLessThanOrEqualsQuestionPage.day()).setValue(dayYesterday); + await $(DateLessThanOrEqualsQuestionPage.month()).setValue(monthYesterday); + await $(DateLessThanOrEqualsQuestionPage.year()).setValue(yearYesterday); + await click(DateLessThanOrEqualsQuestionPage.submit()); + await verifyUrlContains(CorrectAnswerPage.pageName); + }); + + it("When I enter a date equal to today, Then I should be routed to the correct page", async () => { + await $(DateLessThanOrEqualsQuestionPage.day()).setValue(dayToday); + await $(DateLessThanOrEqualsQuestionPage.month()).setValue(monthToday); + await $(DateLessThanOrEqualsQuestionPage.year()).setValue(yearToday); + await click(DateLessThanOrEqualsQuestionPage.submit()); + await verifyUrlContains(CorrectAnswerPage.pageName); + }); + + it("When I enter a date greater than today, Then I should be routed to the incorrect page", async () => { + await $(DateLessThanOrEqualsQuestionPage.day()).setValue(dayTomorrow); + await $(DateLessThanOrEqualsQuestionPage.month()).setValue(monthTomorrow); + await $(DateLessThanOrEqualsQuestionPage.year()).setValue(yearTomorrow); + await click(DateLessThanOrEqualsQuestionPage.submit()); + await verifyUrlContains(IncorrectAnswerPage.pageName); + }); + }); + }); +}); diff --git a/tests/functional/spec/journeys/routing/in.spec.js b/tests/functional/spec/journeys/routing/in.spec.js new file mode 100644 index 0000000000..ee913c98ab --- /dev/null +++ b/tests/functional/spec/journeys/routing/in.spec.js @@ -0,0 +1,25 @@ +import CountryCheckboxPage from "../../../generated_pages/routing_checkbox_contains_in/country-checkbox.page"; +import CountryInterstitialPage from "../../../generated_pages/routing_checkbox_contains_in/country-interstitial-india.page"; +import CountryInterstitialOtherPage from "../../../generated_pages/routing_checkbox_contains_in/country-interstitial-not-india.page"; +import { click, verifyUrlContains } from "../../../helpers"; +describe("Feature: Routing - IN Operator", () => { + describe("Equals", () => { + describe("Given I start the IN operator routing survey", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_routing_checkbox_contains_in.json"); + }); + + it("When I do select India, Then I should be routed to the the correct answer interstitial page", async () => { + await $(CountryCheckboxPage.india()).click(); + await click(CountryCheckboxPage.submit()); + await verifyUrlContains(CountryInterstitialPage.pageName); + }); + + it("When I do not select India, Then I should be routed to the the incorrect answer interstitial page", async () => { + await $(CountryCheckboxPage.liechtenstein()).click(); + await click(CountryCheckboxPage.submit()); + await verifyUrlContains(CountryInterstitialOtherPage.pageName); + }); + }); + }); +}); diff --git a/tests/functional/spec/journeys/routing/not.spec.js b/tests/functional/spec/journeys/routing/not.spec.js new file mode 100644 index 0000000000..d5e4c2898b --- /dev/null +++ b/tests/functional/spec/journeys/routing/not.spec.js @@ -0,0 +1,25 @@ +import CountryCheckboxPage from "../../../generated_pages/routing_not/country-checkbox.page"; +import CountryInterstitialPage from "../../../generated_pages/routing_not/country-interstitial-not-india.page"; +import IndiaInterstitialPage from "../../../generated_pages/routing_not/country-interstitial-india.page"; +import { click, verifyUrlContains } from "../../../helpers"; +describe("Feature: Routing - Not Operator", () => { + describe("Equals", () => { + describe("Given I start the not operator routing survey", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_routing_not.json"); + }); + + it("When I do not select India, Then I should be routed to the not India interstitial page", async () => { + await $(CountryCheckboxPage.azerbaijan()).click(); + await click(CountryCheckboxPage.submit()); + await verifyUrlContains(CountryInterstitialPage.pageName); + }); + + it("When I select India, Then I should be routed to the India interstitial page", async () => { + await $(CountryCheckboxPage.india()).click(); + await click(CountryCheckboxPage.submit()); + await verifyUrlContains(IndiaInterstitialPage.pageName); + }); + }); + }); +}); diff --git a/tests/functional/spec/journeys/routing/number.spec.js b/tests/functional/spec/journeys/routing/number.spec.js new file mode 100644 index 0000000000..d4b60a1bf3 --- /dev/null +++ b/tests/functional/spec/journeys/routing/number.spec.js @@ -0,0 +1,159 @@ +import NumberQuestionPage from "../../../generated_pages/routing_number_equals/number-question.page"; +import CorrectAnswerPage from "../../../generated_pages/routing_number_equals/correct-answer.page"; +import IncorrectAnswerPage from "../../../generated_pages/routing_number_equals/incorrect-answer.page"; +import { click, verifyUrlContains } from "../../../helpers"; +describe("Feature: Routing on a Number", () => { + describe("Equals", () => { + describe("Given I start number routing equals survey", () => { + before(async () => { + await browser.openQuestionnaire("test_routing_number_equals.json"); + }); + + it("When I enter 123, Then I should be routed to the correct page", async () => { + await $(NumberQuestionPage.answer()).setValue(123); + await click(NumberQuestionPage.submit()); + await verifyUrlContains(CorrectAnswerPage.pageName); + }); + + it("When I enter a number that isn't 123, Then I should be routed to the incorrect page", async () => { + await $(CorrectAnswerPage.previous()).click(); + await $(NumberQuestionPage.answer()).setValue(555); + await click(NumberQuestionPage.submit()); + await verifyUrlContains(IncorrectAnswerPage.pageName); + }); + }); + }); + + describe("Not Equals", () => { + describe("Given I start number routing not equals survey", () => { + before(async () => { + await browser.openQuestionnaire("test_routing_number_not_equals.json"); + }); + + it("When I enter a number that isn't 123, Then I should be routed to the correct page", async () => { + await $(NumberQuestionPage.answer()).setValue(987); + await click(NumberQuestionPage.submit()); + await verifyUrlContains(CorrectAnswerPage.pageName); + }); + + it("When I enter 123, Then I should be routed to the incorrect page", async () => { + await $(CorrectAnswerPage.previous()).click(); + await $(NumberQuestionPage.answer()).setValue(123); + await click(NumberQuestionPage.submit()); + await verifyUrlContains(IncorrectAnswerPage.pageName); + }); + }); + }); + + describe("Greater Than", () => { + describe("Given I start number routing greater than survey", () => { + before(async () => { + await browser.openQuestionnaire("test_routing_number_greater_than.json"); + }); + + it("When I enter a number greater than 123, Then I should be routed to the correct page", async () => { + await $(NumberQuestionPage.answer()).setValue(555); + await click(NumberQuestionPage.submit()); + await verifyUrlContains(CorrectAnswerPage.pageName); + }); + + it("When I enter 123, Then I should be routed to the incorrect page", async () => { + await $(CorrectAnswerPage.previous()).click(); + await $(NumberQuestionPage.answer()).setValue(123); + await click(NumberQuestionPage.submit()); + await verifyUrlContains(IncorrectAnswerPage.pageName); + }); + + it("When I enter a number less than 123, Then I should be routed to the incorrect page", async () => { + await $(IncorrectAnswerPage.previous()).click(); + await $(NumberQuestionPage.answer()).setValue(2); + await click(NumberQuestionPage.submit()); + await verifyUrlContains(IncorrectAnswerPage.pageName); + }); + }); + }); + + describe("Less Than", () => { + describe("Given I start number routing less than survey", () => { + before(async () => { + await browser.openQuestionnaire("test_routing_number_less_than.json"); + }); + + it("When I enter a number less than 123, Then I should be routed to the correct page", async () => { + await $(NumberQuestionPage.answer()).setValue(77); + await click(NumberQuestionPage.submit()); + await verifyUrlContains(CorrectAnswerPage.pageName); + }); + + it("When I enter 123, Then I should be routed to the incorrect page", async () => { + await $(CorrectAnswerPage.previous()).click(); + await $(NumberQuestionPage.answer()).setValue(123); + await click(NumberQuestionPage.submit()); + await verifyUrlContains(IncorrectAnswerPage.pageName); + }); + + it("When I enter a number greater than 123, Then I should be routed to the incorrect page", async () => { + await $(IncorrectAnswerPage.previous()).click(); + await $(NumberQuestionPage.answer()).setValue(765); + await click(NumberQuestionPage.submit()); + await verifyUrlContains(IncorrectAnswerPage.pageName); + }); + }); + }); + + describe("Greater Than or Equal", () => { + describe("Given I have number routing with a greater than or equal", () => { + before(async () => { + await browser.openQuestionnaire("test_routing_number_greater_than_or_equal.json"); + }); + + it("When I enter a number greater than 123, Then I should be routed to the correct page", async () => { + await $(NumberQuestionPage.answer()).setValue(555); + await click(NumberQuestionPage.submit()); + await verifyUrlContains(CorrectAnswerPage.pageName); + }); + + it("When I enter 123, Then I should be routed to the correct page", async () => { + await $(CorrectAnswerPage.previous()).click(); + await $(NumberQuestionPage.answer()).setValue(123); + await click(NumberQuestionPage.submit()); + await verifyUrlContains(CorrectAnswerPage.pageName); + }); + + it("When I enter a number less than 123, Then I should be routed to the incorrect page", async () => { + await $(CorrectAnswerPage.previous()).click(); + await $(NumberQuestionPage.answer()).setValue(2); + await click(NumberQuestionPage.submit()); + await verifyUrlContains(IncorrectAnswerPage.pageName); + }); + }); + }); + + describe("Less Than or Equal", () => { + describe("Given I have number routing with a less than or equal", () => { + before(async () => { + await browser.openQuestionnaire("test_routing_number_less_than_or_equal.json"); + }); + + it("When I enter a number less than 123, Then I should be routed to the correct page", async () => { + await $(NumberQuestionPage.answer()).setValue(23); + await click(NumberQuestionPage.submit()); + await verifyUrlContains(CorrectAnswerPage.pageName); + }); + + it("When I enter 123, Then I should be routed to the correct page", async () => { + await $(CorrectAnswerPage.previous()).click(); + await $(NumberQuestionPage.answer()).setValue(123); + await click(NumberQuestionPage.submit()); + await verifyUrlContains(CorrectAnswerPage.pageName); + }); + + it("When I enter a number larger than 123, Then I should be routed to the incorrect page", async () => { + await $(CorrectAnswerPage.previous()).click(); + await $(NumberQuestionPage.answer()).setValue(546); + await click(NumberQuestionPage.submit()); + await verifyUrlContains(IncorrectAnswerPage.pageName); + }); + }); + }); +}); diff --git a/tests/functional/spec/journeys/routing/or.spec.js b/tests/functional/spec/journeys/routing/or.spec.js new file mode 100644 index 0000000000..1529ff9996 --- /dev/null +++ b/tests/functional/spec/journeys/routing/or.spec.js @@ -0,0 +1,46 @@ +import FirstNumberQuestionPage from "../../../generated_pages/routing_or/number-question-1.page"; +import SecondNumberQuestionPage from "../../../generated_pages/routing_or/number-question-2.page"; +import CorrectAnswerPage from "../../../generated_pages/routing_or/correct-answer.page"; +import IncorrectAnswerPage from "../../../generated_pages/routing_or/incorrect-answer.page"; +import { click, verifyUrlContains } from "../../../helpers"; +describe("Feature: Routing - OR Operator", () => { + describe("Equals", () => { + describe("Given I start the or operator routing survey", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_routing_or.json"); + }); + + it("When I enter both answers correctly with 123 and 321, Then I should be routed to the correct page", async () => { + await $(FirstNumberQuestionPage.answer1()).setValue(123); + await click(FirstNumberQuestionPage.submit()); + await $(SecondNumberQuestionPage.answer2()).setValue(321); + await click(SecondNumberQuestionPage.submit()); + await verifyUrlContains(CorrectAnswerPage.pageName); + }); + + it("When I only enter the second answer correctly with 555 and 321, Then I should be routed to the correct page", async () => { + await $(FirstNumberQuestionPage.answer1()).setValue(555); + await click(FirstNumberQuestionPage.submit()); + await $(SecondNumberQuestionPage.answer2()).setValue(321); + await click(SecondNumberQuestionPage.submit()); + await verifyUrlContains(CorrectAnswerPage.pageName); + }); + + it("When I only enter the first answer correctly with 123 and 555, Then I should be routed to the correct page", async () => { + await $(FirstNumberQuestionPage.answer1()).setValue(123); + await click(FirstNumberQuestionPage.submit()); + await $(SecondNumberQuestionPage.answer2()).setValue(555); + await click(SecondNumberQuestionPage.submit()); + await verifyUrlContains(CorrectAnswerPage.pageName); + }); + + it("When I answer both questions incorrectly with 555 and 444, Then I should be routed to the incorrect page", async () => { + await $(FirstNumberQuestionPage.answer1()).setValue(555); + await click(FirstNumberQuestionPage.submit()); + await $(SecondNumberQuestionPage.answer2()).setValue(444); + await click(SecondNumberQuestionPage.submit()); + await verifyUrlContains(IncorrectAnswerPage.pageName); + }); + }); + }); +}); diff --git a/tests/functional/spec/features/routing/removes_completed_block.spec.js b/tests/functional/spec/journeys/routing/removes_completed_block.spec.js similarity index 50% rename from tests/functional/spec/features/routing/removes_completed_block.spec.js rename to tests/functional/spec/journeys/routing/removes_completed_block.spec.js index 899e3dfd57..d93963fa04 100644 --- a/tests/functional/spec/features/routing/removes_completed_block.spec.js +++ b/tests/functional/spec/journeys/routing/removes_completed_block.spec.js @@ -1,16 +1,16 @@ import NumberOfEmployeesTotalBlockPage from "../../../generated_pages/confirmation_question/number-of-employees-total-block.page.js"; import ConfirmZeroEmployeesBlockPage from "../../../generated_pages/confirmation_question/confirm-zero-employees-block.page.js"; import SubmitPage from "../../../generated_pages/confirmation_question/submit.page.js"; - +import { click, verifyUrlContains } from "../../../helpers"; describe("Feature: Routing incompletes block if routing backwards", () => { describe("Given I have a confirmation Question", () => { - before("Get to summary", () => { - browser.openQuestionnaire("test_confirmation_question.json"); - $(NumberOfEmployeesTotalBlockPage.numberOfEmployeesTotal()).setValue(0); - $(NumberOfEmployeesTotalBlockPage.submit()).click(); - $(ConfirmZeroEmployeesBlockPage.yes()).click(); - $(ConfirmZeroEmployeesBlockPage.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); + before("Get to summary", async () => { + await browser.openQuestionnaire("test_confirmation_question.json"); + await $(NumberOfEmployeesTotalBlockPage.numberOfEmployeesTotal()).setValue(0); + await click(NumberOfEmployeesTotalBlockPage.submit()); + await $(ConfirmZeroEmployeesBlockPage.yes()).click(); + await click(ConfirmZeroEmployeesBlockPage.submit()); + await verifyUrlContains(SubmitPage.pageName); }); }); }); diff --git a/tests/functional/spec/journeys/skipping/answer_comparison_skip_conditions.spec.js b/tests/functional/spec/journeys/skipping/answer_comparison_skip_conditions.spec.js new file mode 100644 index 0000000000..952388e5fe --- /dev/null +++ b/tests/functional/spec/journeys/skipping/answer_comparison_skip_conditions.spec.js @@ -0,0 +1,31 @@ +import Comparison1Page from "../../../generated_pages/skip_condition_answer_comparison/comparison-1.page.js"; +import Comparison2Page from "../../../generated_pages/skip_condition_answer_comparison/comparison-2.page.js"; +import { click } from "../../../helpers"; + +describe("Test skip condition answer comparisons", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_skip_condition_answer_comparison.json"); + }); + + it("Given we start the skip condition survey, when we enter the same answers, then the interstitial should show that the answers are the same", async () => { + await $(Comparison1Page.answer()).setValue(1); + await click(Comparison1Page.submit()); + await $(Comparison2Page.answer()).setValue(1); + await click(Comparison2Page.submit()); + await expect(await $("#main-content > p").getText()).toBe("Your second number was equal to your first number"); + }); + it("Given we start the skip condition survey, when we enter a high number then a low number, then the interstitial should show that the answers are low then high", async () => { + await $(Comparison1Page.answer()).setValue(3); + await click(Comparison1Page.submit()); + await $(Comparison2Page.answer()).setValue(2); + await click(Comparison2Page.submit()); + await expect(await $("#main-content > p").getText()).toBe("Your first answer was greater than your second number"); + }); + it("Given we start the skip condition survey, when we enter a low number then a high number, then the interstitial should show that the answers are high then low", async () => { + await $(Comparison1Page.answer()).setValue(1); + await click(Comparison1Page.submit()); + await $(Comparison2Page.answer()).setValue(2); + await click(Comparison2Page.submit()); + await expect(await $("#main-content > p").getText()).toBe("Your first answer was less than your second number"); + }); +}); diff --git a/tests/functional/spec/journeys/skipping/routing_and_skipping_section_dependencies.spec.js b/tests/functional/spec/journeys/skipping/routing_and_skipping_section_dependencies.spec.js new file mode 100644 index 0000000000..773637e2a5 --- /dev/null +++ b/tests/functional/spec/journeys/skipping/routing_and_skipping_section_dependencies.spec.js @@ -0,0 +1,490 @@ +import AgePage from "../../../generated_pages/routing_and_skipping_section_dependencies/age.page"; +import HouseHoldPersonalDetailsSectionSummaryPage from "../../../generated_pages/routing_and_skipping_section_dependencies/household-personal-details-section-summary.page"; +import HouseholdSectionSummaryPage from "../../../generated_pages/routing_and_skipping_section_dependencies/household-section-summary.page"; +import ListCollectorAddPage from "../../../generated_pages/routing_and_skipping_section_dependencies/list-collector-add.page"; +import ListCollectorPage from "../../../generated_pages/routing_and_skipping_section_dependencies/list-collector.page"; +import NamePage from "../../../generated_pages/routing_and_skipping_section_dependencies/name-block.page"; +import PrimaryPersonSummaryPage from "../../../generated_pages/routing_and_skipping_section_dependencies/primary-person-summary.page"; +import ReasonNoConfirmationPage from "../../../generated_pages/routing_and_skipping_section_dependencies/reason-no-confirmation.page"; +import RepeatingAgePage from "../../../generated_pages/routing_and_skipping_section_dependencies/repeating-age.page"; +import RepeatingSexPage from "../../../generated_pages/routing_and_skipping_section_dependencies/repeating-sex.page"; +import SecurityPage from "../../../generated_pages/routing_and_skipping_section_dependencies/security.page"; +import SkipAgePage from "../../../generated_pages/routing_and_skipping_section_dependencies/skip-age.page"; +import SkipEnableSectionPage from "../../../generated_pages/routing_and_skipping_section_dependencies/skip-household-section.page"; +import EnableSectionPage from "../../../generated_pages/routing_and_skipping_section_dependencies/enable-section.page"; +import SkipConfirmationPage from "../../../generated_pages/routing_and_skipping_section_dependencies/skip-confirmation.page"; +import SkipConfirmationSectionSummaryPage from "../../../generated_pages/routing_and_skipping_section_dependencies/skip-confirmation-section-summary.page"; +import SkipSectionSummaryPage from "../../../generated_pages/routing_and_skipping_section_dependencies/skip-section-summary.page"; +import RepeatingIsDependentPage from "../../../generated_pages/routing_and_skipping_section_dependencies/repeating-is-dependent.page"; +import RepeatingIsSmokerPage from "../../../generated_pages/routing_and_skipping_section_dependencies/repeating-is-smoker.page"; + +import HubPage from "../../../base_pages/hub.page"; +import { click } from "../../../helpers"; + +describe("Routing and skipping section dependencies", () => { + describe("Given the routing and skipping section dependencies questionnaire", () => { + beforeEach("Load the survey", async () => { + await browser.openQuestionnaire("test_routing_and_skipping_section_dependencies.json"); + }); + + it("When I answer 'No' to skipping the age question, Then in the Primary Person section I am asked my name, age and why I didn't confirm skipping", async () => { + await answerNoToSkipAgeQuestion(); + + await selectPrimaryPerson(); + await answerAndSubmitNameQuestion(); + await answerAndSubmitAgeQuestion(); + await answerAndSubmitReasonForNoConfirmationQuestion(); + + await expectPersonalDetailsName(); + await expectPersonalDetailsAge(); + await expectReasonNoConfirmationAnswer(); + }); + + it("When I answer 'Yes' to skipping the age question, Then in the Primary Person section I am only asked my name and why I didn't confirm skipping", async () => { + await answerYesToSkipAgeQuestion(); + + await selectPrimaryPerson(); + await answerAndSubmitNameQuestion(); + await answerAndSubmitReasonForNoConfirmationQuestion(); + + await expectPersonalDetailsName(); + await expectReasonNoConfirmationAnswer(); + await expectPersonalDetailsAgeExistingFalse(); + }); + + it("When I answer 'Yes' to skipping the age question and 'Yes' to are you sure in skip question confirmation section, Then in the Primary Person section I am just asked my name", async () => { + await answerYesToSkipAgeQuestion(); + + await selectConfirmationSectionAndAnswerSecurityQuestion(); + await answerYesToSkipConfirmationQuestion(); + + await selectPrimaryPerson(); + await answerAndSubmitNameQuestion(); + + await expectPersonalDetailsName(); + await expectPersonalDetailsAgeExistingFalse(); + await expectReasonNoConfirmationExistingFalse(); + }); + + it("When I answer 'Yes' to skipping the age question but 'No' to are you sure in skip question confirmation section, Then in the Primary Person section I am only asked my name and age", async () => { + await answerYesToSkipAgeQuestion(); + + await selectConfirmationSectionAndAnswerSecurityQuestion(); + await answerNoToSkipConfirmationQuestion(); + + await selectPrimaryPerson(); + await answerAndSubmitNameQuestion(); + await answerAndSubmitAgeQuestion(); + + await expectPersonalDetailsName(); + await expectPersonalDetailsAge(); + await expectReasonNoConfirmationExistingFalse(); + }); + + it("When I answer 'No' to skipping the age question and populate the household, Then in each repeating section I am not asked their age", async () => { + await answerNoToSkipAgeQuestion(); + + await addHouseholdMembers(); + + await $(HubPage.summaryRowLink("household-personal-details-section-1")).click(); + await $(RepeatingSexPage.female()).click(); + await click(RepeatingSexPage.submit()); + await $(RepeatingAgePage.answer()).setValue("45"); + await click(RepeatingAgePage.submit()); + await $(RepeatingIsDependentPage.no()).click(); + await click(RepeatingIsDependentPage.submit()); + await $(RepeatingIsSmokerPage.no()).click(); + await click(RepeatingIsSmokerPage.submit()); + + await expect(await $(HouseHoldPersonalDetailsSectionSummaryPage.repeatingSexAnswer()).getText()).toBe("Female"); + await expect(await $(HouseHoldPersonalDetailsSectionSummaryPage.repeatingAgeAnswer()).getText()).toBe("45"); + + await click(HouseHoldPersonalDetailsSectionSummaryPage.submit()); + await $(HubPage.summaryRowLink("household-personal-details-section-2")).click(); + await $(RepeatingSexPage.male()).click(); + await click(RepeatingSexPage.submit()); + await $(RepeatingAgePage.answer()).setValue("10"); + await click(RepeatingAgePage.submit()); + await $(RepeatingIsDependentPage.yes()).click(); + await click(RepeatingIsDependentPage.submit()); + + await expect(await $(HouseHoldPersonalDetailsSectionSummaryPage.repeatingSexAnswer()).getText()).toBe("Male"); + await expect(await $(HouseHoldPersonalDetailsSectionSummaryPage.repeatingAgeAnswer()).getText()).toBe("10"); + }); + + it("When I answer 'Yes' to skipping the age question and populate the household, Then in each repeating section I am not asked their age", async () => { + await answerYesToSkipAgeQuestion(); + + await addHouseholdMembers(); + + await $(HubPage.summaryRowLink("household-personal-details-section-1")).click(); + await $(RepeatingSexPage.female()).click(); + await click(RepeatingSexPage.submit()); + await $(RepeatingIsDependentPage.no()).click(); + await click(RepeatingIsDependentPage.submit()); + await expect(await $(HouseHoldPersonalDetailsSectionSummaryPage.repeatingSexAnswer()).getText()).toBe("Female"); + await expect(await $(HouseHoldPersonalDetailsSectionSummaryPage.repeatingAgeAnswer()).isExisting()).toBe(false); + + await click(HouseHoldPersonalDetailsSectionSummaryPage.submit()); + await $(HubPage.summaryRowLink("household-personal-details-section-2")).click(); + await $(RepeatingSexPage.male()).click(); + await click(RepeatingSexPage.submit()); + await $(RepeatingIsDependentPage.yes()).click(); + await click(RepeatingIsDependentPage.submit()); + + await expect(await $(HouseHoldPersonalDetailsSectionSummaryPage.repeatingSexAnswer()).getText()).toBe("Male"); + await expect(await $(HouseHoldPersonalDetailsSectionSummaryPage.repeatingAgeAnswer()).isExisting()).toBe(false); + }); + }); + + describe("Given the routing and skipping section dependencies questionnaire", () => { + beforeEach("Load the survey", async () => { + await browser.openQuestionnaire("test_routing_and_skipping_section_dependencies.json"); + }); + it("When I answer 'No' to skipping the section question and 'Yes' to enable the section question, Then the household summary will be visible on the hub", async () => { + await answerNoToSkipEnableQuestionAndYesToEnableSection(); + + await expect(await $(HubPage.summaryRowLink("household-section")).isExisting()).toBe(true); + }); + it("When I answer 'No' to skipping the section question and 'No' to enable the section question, Then the household summary will not be visible on the hub", async () => { + await answerNoToSkipEnableQuestionAndNoToEnableSection(); + + await expect(await $(HubPage.summaryRowLink("household-section")).isExisting()).toBe(false); + }); + }); + + describe("Given the routing and skipping section dependencies questionnaire and I answered 'No' to skipping the section question and 'Yes' to enable the section question", () => { + before("Load the survey", async () => { + await browser.openQuestionnaire("test_routing_and_skipping_section_dependencies.json"); + }); + it("When I change my answer to skipping the section question to 'No', Then the household summary will not be visible on the hub", async () => { + await answerNoToSkipEnableQuestionAndYesToEnableSection(); + await changeSkipEnableQuestionToYes(); + + await expect(await $(HubPage.summaryRowLink("household-section")).isExisting()).toBe(false); + }); + }); + + describe("Given the routing and skipping section dependencies questionnaire and I answered 'Yes' to skipping the age question but 'No' to are you sure in skip question confirmation section", () => { + before("Load the survey", async () => { + await browser.openQuestionnaire("test_routing_and_skipping_section_dependencies.json"); + }); + + it("When I change my answer to skipping age to 'No', removing the 'are you sure' question from the path, Then in the Primary Person section I am asked my name, age and why I didn't confirm skipping", async () => { + await answerYesToSkipAgeQuestion(); + + await selectConfirmationSectionAndAnswerSecurityQuestion(); + await answerNoToSkipConfirmationQuestion(); + + await editNoToSkipAgeQuestion(); + + await selectPrimaryPerson(); + await answerAndSubmitNameQuestion(); + await answerAndSubmitAgeQuestion(); + + await $(ReasonNoConfirmationPage.iDidButItWasRemovedFromThePathAsIChangedMyAnswerToNoOnTheSkipQuestion()).click(); + await click(ReasonNoConfirmationPage.submit()); + + await expectPersonalDetailsName(); + await expectPersonalDetailsAge(); + await expect(await $(PrimaryPersonSummaryPage.reasonNoConfirmationAnswer()).getText()).toBe( + "I did, but it was removed from the path as I changed my answer to No on the skip question", + ); + }); + }); + + describe("Given the routing and skipping section dependencies questionnaire and I answered 'Yes' to skipping the age question and complete the Primary Person section", () => { + before("Load the survey", async () => { + await browser.openQuestionnaire("test_routing_and_skipping_section_dependencies.json"); + }); + + it("When I change my answer to skipping age to 'No', Then the Primary Person section status is changed to Partially completed", async () => { + await answerYesToSkipAgeQuestion(); + await selectPrimaryPerson(); + await answerAndSubmitNameQuestion(); + await answerAndSubmitReasonForNoConfirmationQuestion(); + await click(PrimaryPersonSummaryPage.submit()); + + await expect(await $(HubPage.summaryRowState("primary-person")).getText()).toBe("Completed"); + + await editNoToSkipAgeQuestion(); + + await expect(await $(HubPage.summaryRowState("primary-person")).getText()).toBe("Partially completed"); + }); + + it("When I change my answer back to skipping age to 'Yes', Then the Primary Person section status is changed back to Completed", async () => { + await editYesToSkipAgeQuestion(); + + await expect(await $(HubPage.summaryRowState("primary-person")).getText()).toBe("Completed"); + }); + }); + + describe("Given the routing and skipping section dependencies questionnaire and I answered 'Yes' to skipping the age question and add 2 household members but complete only one", () => { + before("Load the survey", async () => { + await browser.openQuestionnaire("test_routing_and_skipping_section_dependencies.json"); + }); + + it("When I change my answer to skipping age to 'No', Then the completed household member status is changed to Partially completed and the other stays as not started", async () => { + await answerYesToSkipAgeQuestion(); + await addHouseholdMembers(); + await $(HubPage.summaryRowLink("household-personal-details-section-1")).click(); + await $(RepeatingSexPage.female()).click(); + await click(RepeatingSexPage.submit()); + await $(RepeatingIsDependentPage.no()).click(); + await click(RepeatingIsDependentPage.submit()); + await click(HouseHoldPersonalDetailsSectionSummaryPage.submit()); + + await editNoToSkipAgeQuestion(); + + await expect(await $(HubPage.summaryRowState("household-personal-details-section-1")).getText()).toBe("Partially completed"); + await expect(await $(HubPage.summaryRowState("household-personal-details-section-2")).getText()).toBe("Not started"); + }); + + it("When I change my answer back to skipping age to 'Yes', Then the Partially completed household member status is changed back to Completed and the other stays as not started", async () => { + await editYesToSkipAgeQuestion(); + + await expect(await $(HubPage.summaryRowState("household-personal-details-section-1")).getText()).toBe("Completed"); + await expect(await $(HubPage.summaryRowState("household-personal-details-section-2")).getText()).toBe("Not started"); + }); + }); + + describe("Given the routing and skipping section dependencies questionnaire", () => { + beforeEach("Load the survey", async () => { + await browser.openQuestionnaire("test_routing_and_skipping_section_dependencies.json"); + }); + + it("When I answer 'No' to skipping the age question and populate the household with Repeating Age > 18, Then in each repeating section I am asked if they are smoker", async () => { + await answerNoToSkipAgeQuestion(); + + await addHouseholdMembers(); + + await $(HubPage.summaryRowLink("household-personal-details-section-1")).click(); + await $(RepeatingSexPage.female()).click(); + await click(RepeatingSexPage.submit()); + await $(RepeatingAgePage.answer()).setValue("45"); + await click(RepeatingAgePage.submit()); + await $(RepeatingIsDependentPage.no()).click(); + await click(RepeatingIsDependentPage.submit()); + await $(RepeatingIsSmokerPage.no()).click(); + await click(RepeatingIsSmokerPage.submit()); + + await expect(await $(HouseHoldPersonalDetailsSectionSummaryPage.repeatingSexAnswer()).getText()).toBe("Female"); + await expect(await $(HouseHoldPersonalDetailsSectionSummaryPage.repeatingAgeAnswer()).getText()).toBe("45"); + await expect(await $(HouseHoldPersonalDetailsSectionSummaryPage.repeatingIsSmokerAnswer()).getText()).toBe("No"); + + await click(HouseHoldPersonalDetailsSectionSummaryPage.submit()); + await $(HubPage.summaryRowLink("household-personal-details-section-2")).click(); + await $(RepeatingSexPage.male()).click(); + await click(RepeatingSexPage.submit()); + await $(RepeatingAgePage.answer()).setValue("19"); + await click(RepeatingAgePage.submit()); + await $(RepeatingIsDependentPage.yes()).click(); + await click(RepeatingIsDependentPage.submit()); + await $(RepeatingIsSmokerPage.no()).click(); + await click(RepeatingIsSmokerPage.submit()); + + await expect(await $(HouseHoldPersonalDetailsSectionSummaryPage.repeatingSexAnswer()).getText()).toBe("Male"); + await expect(await $(HouseHoldPersonalDetailsSectionSummaryPage.repeatingAgeAnswer()).getText()).toBe("19"); + await expect(await $(HouseHoldPersonalDetailsSectionSummaryPage.repeatingIsSmokerAnswer()).getText()).toBe("No"); + }); + + it("When I answer 'No' to skipping the age question and populate the household with Repeating Age < 18, Then in each repeating section I am not asked if they are smoker", async () => { + await answerNoToSkipAgeQuestion(); + + await addHouseholdMembers(); + + await $(HubPage.summaryRowLink("household-personal-details-section-1")).click(); + await $(RepeatingSexPage.female()).click(); + await click(RepeatingSexPage.submit()); + await $(RepeatingAgePage.answer()).setValue("15"); + await click(RepeatingAgePage.submit()); + await $(RepeatingIsDependentPage.yes()).click(); + await click(RepeatingIsDependentPage.submit()); + + await expect(await $(HouseHoldPersonalDetailsSectionSummaryPage.repeatingSexAnswer()).getText()).toBe("Female"); + await expect(await $(HouseHoldPersonalDetailsSectionSummaryPage.repeatingAgeAnswer()).getText()).toBe("15"); + await expect(await $(HouseHoldPersonalDetailsSectionSummaryPage.repeatingIsSmokerAnswer()).isExisting()).toBe(false); + + await click(HouseHoldPersonalDetailsSectionSummaryPage.submit()); + await $(HubPage.summaryRowLink("household-personal-details-section-2")).click(); + await $(RepeatingSexPage.male()).click(); + await click(RepeatingSexPage.submit()); + await $(RepeatingAgePage.answer()).setValue("10"); + await click(RepeatingAgePage.submit()); + await $(RepeatingIsDependentPage.yes()).click(); + await click(RepeatingIsDependentPage.submit()); + + await expect(await $(HouseHoldPersonalDetailsSectionSummaryPage.repeatingSexAnswer()).getText()).toBe("Male"); + await expect(await $(HouseHoldPersonalDetailsSectionSummaryPage.repeatingAgeAnswer()).getText()).toBe("10"); + await expect(await $(HouseHoldPersonalDetailsSectionSummaryPage.repeatingIsSmokerAnswer()).isExisting()).toBe(false); + }); + + it("When I answer 'Yes' to skipping the age question and populate the household, Then in each repeating section I am not asked if they are smoker", async () => { + await answerYesToSkipAgeQuestion(); + + await addHouseholdMembers(); + + await $(HubPage.summaryRowLink("household-personal-details-section-1")).click(); + await $(RepeatingSexPage.female()).click(); + await click(RepeatingSexPage.submit()); + await $(RepeatingIsDependentPage.no()).click(); + await click(RepeatingIsDependentPage.submit()); + await expect(await $(HouseHoldPersonalDetailsSectionSummaryPage.repeatingSexAnswer()).getText()).toBe("Female"); + await expect(await $(HouseHoldPersonalDetailsSectionSummaryPage.repeatingAgeAnswer()).isExisting()).toBe(false); + await expect(await $(HouseHoldPersonalDetailsSectionSummaryPage.repeatingIsSmokerAnswer()).isExisting()).toBe(false); + + await click(HouseHoldPersonalDetailsSectionSummaryPage.submit()); + await $(HubPage.summaryRowLink("household-personal-details-section-2")).click(); + await $(RepeatingSexPage.male()).click(); + await click(RepeatingSexPage.submit()); + await $(RepeatingIsDependentPage.yes()).click(); + await click(RepeatingIsDependentPage.submit()); + + await expect(await $(HouseHoldPersonalDetailsSectionSummaryPage.repeatingSexAnswer()).getText()).toBe("Male"); + await expect(await $(HouseHoldPersonalDetailsSectionSummaryPage.repeatingAgeAnswer()).isExisting()).toBe(false); + await expect(await $(HouseHoldPersonalDetailsSectionSummaryPage.repeatingIsSmokerAnswer()).isExisting()).toBe(false); + }); + }); +}); + +const addHouseholdMembers = async () => { + await $(HubPage.summaryRowLink("household-section")).click(); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Sarah"); + await $(ListCollectorAddPage.lastName()).setValue("Smith"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Marcus"); + await $(ListCollectorAddPage.lastName()).setValue("Smith"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await click(HouseholdSectionSummaryPage.submit()); +}; + +const selectPrimaryPerson = async () => { + await $(HubPage.summaryRowLink("primary-person")).click(); +}; + +const selectConfirmationSectionAndAnswerSecurityQuestion = async () => { + await $(HubPage.summaryRowLink("skip-confirmation-section")).click(); + await $(SecurityPage.yes()).click(); + await click(SecurityPage.submit()); +}; + +const answerYesToSkipAgeQuestion = async () => { + await $(HubPage.summaryRowLink("skip-section")).click(); + await $(SkipAgePage.yes()).click(); + await click(SkipAgePage.submit()); + await $(SkipEnableSectionPage.no()).click(); + await click(SkipEnableSectionPage.submit()); + await $(EnableSectionPage.yes()).click(); + await click(EnableSectionPage.submit()); + await click(SkipSectionSummaryPage.submit()); +}; + +const editNoToSkipAgeQuestion = async () => { + await $(HubPage.summaryRowLink("skip-section")).click(); + await $(SkipSectionSummaryPage.skipAgeAnswerEdit()).click(); + await $(SkipAgePage.no()).click(); + await click(SkipAgePage.submit()); + await click(SkipSectionSummaryPage.submit()); +}; + +const editYesToSkipAgeQuestion = async () => { + await $(HubPage.summaryRowLink("skip-section")).click(); + await $(SkipSectionSummaryPage.skipAgeAnswerEdit()).click(); + await $(SkipAgePage.yes()).click(); + await click(SkipAgePage.submit()); + await click(SkipSectionSummaryPage.submit()); +}; + +const answerNoToSkipAgeQuestion = async () => { + await $(HubPage.summaryRowLink("skip-section")).click(); + await $(SkipAgePage.no()).click(); + await click(SkipAgePage.submit()); + await $(SkipEnableSectionPage.no()).click(); + await click(SkipEnableSectionPage.submit()); + await $(EnableSectionPage.yes()).click(); + await click(EnableSectionPage.submit()); + await click(SkipSectionSummaryPage.submit()); +}; + +const answerNoToSkipConfirmationQuestion = async () => { + await $(SkipConfirmationPage.no()).click(); + await click(SkipConfirmationPage.submit()); + await click(SkipConfirmationSectionSummaryPage.submit()); +}; + +const answerYesToSkipConfirmationQuestion = async () => { + await $(SkipConfirmationPage.yes()).click(); + await click(SkipConfirmationPage.submit()); + await click(SkipConfirmationSectionSummaryPage.submit()); +}; + +const answerNoToSkipEnableQuestionAndYesToEnableSection = async () => { + await $(HubPage.summaryRowLink("skip-section")).click(); + await $(SkipAgePage.no()).click(); + await click(SkipAgePage.submit()); + await $(SkipEnableSectionPage.no()).click(); + await click(SkipEnableSectionPage.submit()); + await $(EnableSectionPage.yes()).click(); + await click(EnableSectionPage.submit()); + await click(SkipSectionSummaryPage.submit()); +}; + +const answerNoToSkipEnableQuestionAndNoToEnableSection = async () => { + await $(HubPage.summaryRowLink("skip-section")).click(); + await $(SkipAgePage.no()).click(); + await click(SkipAgePage.submit()); + await $(SkipEnableSectionPage.no()).click(); + await click(SkipEnableSectionPage.submit()); + await $(EnableSectionPage.no()).click(); + await click(EnableSectionPage.submit()); + await click(SkipSectionSummaryPage.submit()); +}; + +const changeSkipEnableQuestionToYes = async () => { + await $(HubPage.summaryRowLink("skip-section")).click(); + await $(SkipSectionSummaryPage.skipHouseholdSectionAnswerEdit()).click(); + await $(SkipEnableSectionPage.yes()).click(); + await click(SkipEnableSectionPage.submit()); + await click(SkipSectionSummaryPage.submit()); +}; + +const answerAndSubmitNameQuestion = async () => { + await $(NamePage.name()).setValue("John Smith"); + await click(NamePage.submit()); +}; + +const answerAndSubmitAgeQuestion = async () => { + await $(AgePage.answer()).setValue("50"); + await click(AgePage.submit()); +}; + +const answerAndSubmitReasonForNoConfirmationQuestion = async () => { + await $(ReasonNoConfirmationPage.iDidNotVisitSection2SoConfirmationWasNotNeeded()).click(); + await click(ReasonNoConfirmationPage.submit()); +}; + +const expectPersonalDetailsName = async () => { + await expect(await $(PrimaryPersonSummaryPage.nameAnswer()).getText()).toBe("John Smith"); +}; + +const expectPersonalDetailsAge = async () => { + await expect(await $(PrimaryPersonSummaryPage.ageAnswer()).getText()).toBe("50"); +}; + +const expectReasonNoConfirmationAnswer = async () => { + await expect(await $(PrimaryPersonSummaryPage.reasonNoConfirmationAnswer()).getText()).toBe("I did not visit section 2, so confirmation was not needed"); +}; + +const expectPersonalDetailsAgeExistingFalse = async () => { + await expect(await $(PrimaryPersonSummaryPage.ageAnswer()).isExisting()).toBe(false); +}; + +const expectReasonNoConfirmationExistingFalse = async () => { + await expect(await $(PrimaryPersonSummaryPage.reasonNoConfirmationAnswer()).isExisting()).toBe(false); +}; diff --git a/tests/functional/spec/journeys/skipping/routing_and_skipping_section_dependencies_calculated_summary.spec.js b/tests/functional/spec/journeys/skipping/routing_and_skipping_section_dependencies_calculated_summary.spec.js new file mode 100644 index 0000000000..7c897bb497 --- /dev/null +++ b/tests/functional/spec/journeys/skipping/routing_and_skipping_section_dependencies_calculated_summary.spec.js @@ -0,0 +1,198 @@ +import CalculatedSummarySectionSummaryPage from "../../../generated_pages/routing_and_skipping_section_dependencies_calculated_summary/calculated-summary-section-summary.page"; +import CurrencyTotalPlaybackPage from "../../../generated_pages/routing_and_skipping_section_dependencies_calculated_summary/currency-total-playback.page"; +import DependentQuestionSectionSummaryPage from "../../../generated_pages/routing_and_skipping_section_dependencies_calculated_summary/dependent-question-section-summary.page"; +import FirstQuestionBlockPage from "../../../generated_pages/routing_and_skipping_section_dependencies_calculated_summary/first-question-block.page"; +import FruitPage from "../../../generated_pages/routing_and_skipping_section_dependencies_calculated_summary/fruit.page"; +import SecondQuestionBlockPage from "../../../generated_pages/routing_and_skipping_section_dependencies_calculated_summary/second-question-block.page"; +import VegetablesPage from "../../../generated_pages/routing_and_skipping_section_dependencies_calculated_summary/vegetables.page"; +import SkipQuestionPage from "../../../generated_pages/routing_and_skipping_section_dependencies_calculated_summary/skip-butter-block.page"; +import ButterPage from "../../../generated_pages/routing_and_skipping_section_dependencies_calculated_summary/butter-block.page"; + +import HubPage from "../../../base_pages/hub.page"; +import { click, verifyUrlContains } from "../../../helpers"; + +describe("Routing and skipping section dependencies based on calculated summaries", () => { + describe("Given the section dependencies based on a calculated summary questionnaire", () => { + beforeEach("Load the survey", async () => { + await browser.openQuestionnaire("test_routing_and_skipping_section_dependencies_calculated_summary.json"); + }); + + it("When the calculated summary total has not been set, Then the dependent section should not be enabled", async () => { + await expect(await $(HubPage.summaryRowLink("calculated-summary-section")).isExisting()).toBe(true); + await expect(await $(HubPage.summaryRowLink("dependent-question-section")).isExisting()).toBe(true); + await expect(await $(HubPage.summaryRowLink("dependent-enabled-section")).isExisting()).toBe(false); + }); + + it("When the calculated summary total is equal to ÂŖ100, Then the dependent section should be enabled", async () => { + await $(HubPage.summaryRowLink("calculated-summary-section")).click(); + await $(FirstQuestionBlockPage.milk()).setValue(25); + await $(FirstQuestionBlockPage.eggs()).setValue(25); + await $(FirstQuestionBlockPage.bread()).setValue(25); + await $(FirstQuestionBlockPage.cheese()).setValue(25); + await click(FirstQuestionBlockPage.submit()); + await $(SkipQuestionPage.yes()).click(); + await click(SkipQuestionPage.submit()); + await click(CurrencyTotalPlaybackPage.submit()); + await click(CalculatedSummarySectionSummaryPage.submit()); + + await expect(await $(HubPage.summaryRowLink("calculated-summary-section")).isExisting()).toBe(true); + await expect(await $(HubPage.summaryRowLink("dependent-question-section")).isExisting()).toBe(true); + await expect(await $(HubPage.summaryRowLink("dependent-enabled-section")).isExisting()).toBe(true); + }); + + it("When a question in another section has a skip condition dependency on a calculated summary total, and the skip condition is not met (total less than ÂŖ10), then the dependent question should be displayed", async () => { + await $(HubPage.summaryRowLink("calculated-summary-section")).click(); + await $(FirstQuestionBlockPage.milk()).setValue(1); + await $(FirstQuestionBlockPage.eggs()).setValue(1); + await $(FirstQuestionBlockPage.bread()).setValue(1); + await $(FirstQuestionBlockPage.cheese()).setValue(1); + await click(FirstQuestionBlockPage.submit()); + await $(SkipQuestionPage.yes()).click(); + await click(SkipQuestionPage.submit()); + await click(CurrencyTotalPlaybackPage.submit()); + await click(CalculatedSummarySectionSummaryPage.submit()); + + await $(HubPage.summaryRowLink("dependent-question-section")).click(); + await verifyUrlContains(FruitPage.pageName); + }); + + it("When a question in another section has a skip condition dependency on a calculated summary total, and the skip condition is met (total greater than ÂŖ10), then the dependent question should not be displayed", async () => { + await $(HubPage.summaryRowLink("calculated-summary-section")).click(); + await $(FirstQuestionBlockPage.milk()).setValue(5); + await $(FirstQuestionBlockPage.eggs()).setValue(5); + await $(FirstQuestionBlockPage.bread()).setValue(5); + await $(FirstQuestionBlockPage.cheese()).setValue(5); + await click(FirstQuestionBlockPage.submit()); + await $(SkipQuestionPage.yes()).click(); + await click(SkipQuestionPage.submit()); + await click(CurrencyTotalPlaybackPage.submit()); + await click(CalculatedSummarySectionSummaryPage.submit()); + + await $(HubPage.summaryRowLink("dependent-question-section")).click(); + await verifyUrlContains(VegetablesPage.pageName); + }); + + it("When a question in another section has a routing rule dependency on a calculated summary total, and the calculated summary total is greater than ÂŖ100, then we should be routed to the second question block", async () => { + await $(HubPage.summaryRowLink("calculated-summary-section")).click(); + await $(FirstQuestionBlockPage.milk()).setValue(30); + await $(FirstQuestionBlockPage.eggs()).setValue(30); + await $(FirstQuestionBlockPage.bread()).setValue(30); + await $(FirstQuestionBlockPage.cheese()).setValue(30); + await click(FirstQuestionBlockPage.submit()); + await $(SkipQuestionPage.yes()).click(); + await click(SkipQuestionPage.submit()); + await click(CurrencyTotalPlaybackPage.submit()); + await click(CalculatedSummarySectionSummaryPage.submit()); + + await $(HubPage.summaryRowLink("dependent-question-section")).click(); + await $(VegetablesPage.yes()).click(); + await click(VegetablesPage.submit()); + await verifyUrlContains(SecondQuestionBlockPage.pageName); + }); + + it("When a question in another section has a routing rule dependency on a calculated summary total, and the calculated summary total is less than ÂŖ100, then we should be routed to the section summary", async () => { + await $(HubPage.summaryRowLink("calculated-summary-section")).click(); + await $(FirstQuestionBlockPage.milk()).setValue(20); + await $(FirstQuestionBlockPage.eggs()).setValue(20); + await $(FirstQuestionBlockPage.bread()).setValue(20); + await $(FirstQuestionBlockPage.cheese()).setValue(20); + await click(FirstQuestionBlockPage.submit()); + await $(SkipQuestionPage.yes()).click(); + await click(SkipQuestionPage.submit()); + await click(CurrencyTotalPlaybackPage.submit()); + await click(CalculatedSummarySectionSummaryPage.submit()); + + await $(HubPage.summaryRowLink("dependent-question-section")).click(); + await $(VegetablesPage.yes()).click(); + await click(VegetablesPage.submit()); + await verifyUrlContains(DependentQuestionSectionSummaryPage.pageName); + }); + + it("When a question in another section has a dependency on a calculated summary total, and both sections are complete, and I go back and edit the calculated summary total, then the dependent section status should be in progress", async () => { + await $(HubPage.summaryRowLink("calculated-summary-section")).click(); + await $(FirstQuestionBlockPage.milk()).setValue(20); + await $(FirstQuestionBlockPage.eggs()).setValue(20); + await $(FirstQuestionBlockPage.bread()).setValue(20); + await $(FirstQuestionBlockPage.cheese()).setValue(20); + await click(FirstQuestionBlockPage.submit()); + await $(SkipQuestionPage.yes()).click(); + await click(SkipQuestionPage.submit()); + await click(CurrencyTotalPlaybackPage.submit()); + await click(CalculatedSummarySectionSummaryPage.submit()); + + await $(HubPage.summaryRowLink("dependent-question-section")).click(); + await $(VegetablesPage.yes()).click(); + await click(VegetablesPage.submit()); + await click(DependentQuestionSectionSummaryPage.submit()); + await expect(await $(HubPage.summaryRowState("dependent-question-section")).getText()).toBe("Completed"); + + await $(HubPage.summaryRowLink("calculated-summary-section")).click(); + await $(CurrencyTotalPlaybackPage.milkAnswerEdit()).click(); + await $(FirstQuestionBlockPage.milk()).setValue(100); + await click(FirstQuestionBlockPage.submit()); + await verifyUrlContains(CurrencyTotalPlaybackPage.pageName); + await click(CurrencyTotalPlaybackPage.submit()); + await click(CalculatedSummarySectionSummaryPage.submit()); + await expect(await $(HubPage.summaryRowState("dependent-question-section")).getText()).toBe("Partially completed"); + }); + + it("When the calculated summary total is less than ÂŖ100 but additional answers on the path are opened up as a result of editing an answer, Then the dependent section should be enabled", async () => { + await $(HubPage.summaryRowLink("calculated-summary-section")).click(); + await $(FirstQuestionBlockPage.milk()).setValue(10); + await $(FirstQuestionBlockPage.eggs()).setValue(10); + await $(FirstQuestionBlockPage.bread()).setValue(10); + await $(FirstQuestionBlockPage.cheese()).setValue(10); + await click(FirstQuestionBlockPage.submit()); + await $(SkipQuestionPage.yes()).click(); + await click(SkipQuestionPage.submit()); + await click(CurrencyTotalPlaybackPage.submit()); + await click(CalculatedSummarySectionSummaryPage.submit()); + + await expect(await $(HubPage.summaryRowLink("calculated-summary-section")).isExisting()).toBe(true); + await expect(await $(HubPage.summaryRowLink("dependent-question-section")).isExisting()).toBe(true); + await expect(await $(HubPage.summaryRowLink("dependent-enabled-section")).isExisting()).toBe(false); + + await $(HubPage.summaryRowLink("calculated-summary-section")).click(); + await $(CalculatedSummarySectionSummaryPage.skipButterBlockAnswerEdit()).click(); + await $(SkipQuestionPage.no()).click(); + await click(SkipQuestionPage.submit()); + await $(ButterPage.butter()).setValue(60); + await click(ButterPage.submit()); + await click(CurrencyTotalPlaybackPage.submit()); + await click(CalculatedSummarySectionSummaryPage.submit()); + + await expect(await $(HubPage.summaryRowLink("calculated-summary-section")).isExisting()).toBe(true); + await expect(await $(HubPage.summaryRowLink("dependent-question-section")).isExisting()).toBe(true); + await expect(await $(HubPage.summaryRowLink("dependent-enabled-section")).isExisting()).toBe(true); + }); + + it("When the calculated summary total is equal to ÂŖ100 but answers on the path are remove as a result of an answer edit, Then the dependent section should be enabled", async () => { + await $(HubPage.summaryRowLink("calculated-summary-section")).click(); + await $(FirstQuestionBlockPage.milk()).setValue(10); + await $(FirstQuestionBlockPage.eggs()).setValue(10); + await $(FirstQuestionBlockPage.bread()).setValue(10); + await $(FirstQuestionBlockPage.cheese()).setValue(10); + await click(FirstQuestionBlockPage.submit()); + await $(SkipQuestionPage.no()).click(); + await click(SkipQuestionPage.submit()); + await $(ButterPage.butter()).setValue(60); + await click(ButterPage.submit()); + await click(CurrencyTotalPlaybackPage.submit()); + await click(CalculatedSummarySectionSummaryPage.submit()); + + await expect(await $(HubPage.summaryRowLink("calculated-summary-section")).isExisting()).toBe(true); + await expect(await $(HubPage.summaryRowLink("dependent-question-section")).isExisting()).toBe(true); + await expect(await $(HubPage.summaryRowLink("dependent-enabled-section")).isExisting()).toBe(true); + + await $(HubPage.summaryRowLink("calculated-summary-section")).click(); + await $(CalculatedSummarySectionSummaryPage.skipButterBlockAnswerEdit()).click(); + await $(SkipQuestionPage.yes()).click(); + await click(SkipQuestionPage.submit()); + await click(CalculatedSummarySectionSummaryPage.submit()); + + await expect(await $(HubPage.summaryRowLink("calculated-summary-section")).isExisting()).toBe(true); + await expect(await $(HubPage.summaryRowLink("dependent-question-section")).isExisting()).toBe(true); + await expect(await $(HubPage.summaryRowLink("dependent-enabled-section")).isExisting()).toBe(false); + }); + }); +}); diff --git a/tests/functional/spec/journeys/skipping/routing_checkbox_contains.spec.js b/tests/functional/spec/journeys/skipping/routing_checkbox_contains.spec.js new file mode 100644 index 0000000000..27a8ffefa9 --- /dev/null +++ b/tests/functional/spec/journeys/skipping/routing_checkbox_contains.spec.js @@ -0,0 +1,80 @@ +import RoutingCheckboxContains from "../../../generated_pages/routing_checkbox_contains/country-checkbox.page"; +import ContainsAllPage from "../../../generated_pages/routing_checkbox_contains/country-interstitial-all.page"; +import ContainsAnyPage from "../../../generated_pages/routing_checkbox_contains/country-interstitial-any.page"; +import SubmitPage from "../../../generated_pages/routing_checkbox_contains/submit.page"; +import { click, verifyUrlContains } from "../../../helpers"; +describe("Routing Checkbox Contains Condition.", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_routing_checkbox_contains.json"); + }); + + it('Given a list of checkbox options, when I have don\'t select "Liechtenstein" and select the option "India" or the option "Azerbaijan" or both then I should be routed to the "contains any" condition page', async () => { + // When + await expect(await $(RoutingCheckboxContains.liechtenstein()).isSelected()).toBe(false); + + await $(RoutingCheckboxContains.india()).click(); + await click(RoutingCheckboxContains.submit()); + // Then + await verifyUrlContains(ContainsAnyPage.pageName); + + // Or + await $(ContainsAnyPage.previous()).click(); + + // When + await $(RoutingCheckboxContains.india()).click(); + await $(RoutingCheckboxContains.azerbaijan()).click(); + await click(RoutingCheckboxContains.submit()); + + // Then + await verifyUrlContains(ContainsAnyPage.pageName); + + // Or + await $(ContainsAnyPage.previous()).click(); + + // When + await $(RoutingCheckboxContains.india()).click(); + await click(RoutingCheckboxContains.submit()); + + // Then + await verifyUrlContains(ContainsAnyPage.pageName); + }); + + it('Given a list of checkbox options, when I select the option "Malta" or the option "Liechtenstein" or both then I should be routed to the summary condition page', async () => { + // When + await $(RoutingCheckboxContains.liechtenstein()).click(); + await click(RoutingCheckboxContains.submit()); + // Then + await verifyUrlContains(SubmitPage.pageName); + + // Or + await $(ContainsAnyPage.previous()).click(); + + // When + await $(RoutingCheckboxContains.liechtenstein()).click(); + await $(RoutingCheckboxContains.malta()).click(); + await click(RoutingCheckboxContains.submit()); + + // Then + await verifyUrlContains(SubmitPage.pageName); + + // Or + await $(ContainsAnyPage.previous()).click(); + + // When + await $(RoutingCheckboxContains.liechtenstein()).click(); + await click(RoutingCheckboxContains.submit()); + + // Then + await verifyUrlContains(SubmitPage.pageName); + }); + + it('Given a list of checkbox options, when I select the options "India", "Azerbaijan" and "Liechtenstein" then I should be routed to the "contains all" condition page', async () => { + // When + await $(RoutingCheckboxContains.india()).click(); + await $(RoutingCheckboxContains.azerbaijan()).click(); + await $(RoutingCheckboxContains.liechtenstein()).click(); + await click(RoutingCheckboxContains.submit()); + // Then + await verifyUrlContains(ContainsAllPage.pageName); + }); +}); diff --git a/tests/functional/spec/journeys/skipping/skip_condition_block.spec.js b/tests/functional/spec/journeys/skipping/skip_condition_block.spec.js new file mode 100644 index 0000000000..256c6412b3 --- /dev/null +++ b/tests/functional/spec/journeys/skipping/skip_condition_block.spec.js @@ -0,0 +1,25 @@ +import QuestionPage from "../../../generated_pages/skip_condition_block/do-you-want-to-skip.page"; +import SkipPage from "../../../generated_pages/skip_condition_block/should-skip.page"; +import SubmitPage from "../../../generated_pages/skip_condition_block/submit.page"; +import { click, verifyUrlContains } from "../../../helpers"; +describe("Skip Conditions - Block", () => { + const schema = "test_skip_condition_block.json"; + + describe("Given I am completing the test skip condition block survey,", () => { + beforeEach("load the survey", async () => { + await browser.openQuestionnaire(schema); + }); + + it("When I choose to skip on the first page, Then I should see the summary page", async () => { + await $(QuestionPage.yes()).click(); + await click(QuestionPage.submit()); + await verifyUrlContains(SubmitPage.pageName); + }); + + it("When I choose not to skip on the first page, Then I should see the should-skip page", async () => { + await $(QuestionPage.no()).click(); + await click(QuestionPage.submit()); + await verifyUrlContains(SkipPage.pageName); + }); + }); +}); diff --git a/tests/functional/spec/journeys/skipping/skip_condition_group.spec.js b/tests/functional/spec/journeys/skipping/skip_condition_group.spec.js new file mode 100644 index 0000000000..29567430cc --- /dev/null +++ b/tests/functional/spec/journeys/skipping/skip_condition_group.spec.js @@ -0,0 +1,25 @@ +import QuestionPage from "../../../generated_pages/skip_condition_group/do-you-want-to-skip.page"; +import SkipPage from "../../../generated_pages/skip_condition_group/should-skip.page"; +import SubmitPage from "../../../generated_pages/skip_condition_group/submit.page"; +import { click, verifyUrlContains } from "../../../helpers"; +describe("Skip Conditions - Group", () => { + const schema = "test_skip_condition_group.json"; + + describe("Given I am completing the test skip condition group survey,", () => { + beforeEach("load the survey", async () => { + await browser.openQuestionnaire(schema); + }); + + it("When I choose to skip on the first page, Then I should see the summary page", async () => { + await $(QuestionPage.yes()).click(); + await click(QuestionPage.submit()); + await verifyUrlContains(SubmitPage.pageName); + }); + + it("When I choose not to skip on the first page, Then I should see the should-skip page", async () => { + await $(QuestionPage.no()).click(); + await click(QuestionPage.submit()); + await verifyUrlContains(SkipPage.pageName); + }); + }); +}); diff --git a/tests/functional/spec/journeys/skipping/skip_condition_list.spec.js b/tests/functional/spec/journeys/skipping/skip_condition_list.spec.js new file mode 100644 index 0000000000..14a48ae88e --- /dev/null +++ b/tests/functional/spec/journeys/skipping/skip_condition_list.spec.js @@ -0,0 +1,67 @@ +import ListCollectorPage from "../../../generated_pages/skip_condition_list/list-collector.page.js"; +import ListCollectorAddPage from "../../../generated_pages/skip_condition_list/list-collector-add.page.js"; +import LessThanTwoInterstitialPage from "../../../generated_pages/skip_condition_list/less-than-two-interstitial.page.js"; +import TwoInterstitialPage from "../../../generated_pages/skip_condition_list/two-interstitial.page.js"; +import MoreThanTwoInterstitialPage from "../../../generated_pages/skip_condition_list/more-than-two-interstitial.page.js"; +import { click, verifyUrlContains } from "../../../helpers"; +describe("Feature: Routing on lists", () => { + describe("Given I start skip condition list survey", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_skip_condition_list.json"); + }); + + it("When I don't add a person to the list, Then the less than two people skippable page should be shown", async () => { + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await verifyUrlContains(LessThanTwoInterstitialPage.pageName); + }); + + it("When I add one person to the list, Then the less than two people skippable page should be shown", async () => { + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Marcus"); + await $(ListCollectorAddPage.lastName()).setValue("Twin"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await verifyUrlContains(LessThanTwoInterstitialPage.pageName); + }); + + it("When I add two people to the list, Then the two people skippable page should be shown", async () => { + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Marcus"); + await $(ListCollectorAddPage.lastName()).setValue("Twin"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Samuel"); + await $(ListCollectorAddPage.lastName()).setValue("Clemens"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await verifyUrlContains(TwoInterstitialPage.pageName); + }); + + it("When I add three people to the list, Then the more than two people skippable page should be shown", async () => { + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Marcus"); + await $(ListCollectorAddPage.lastName()).setValue("Twin"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Samuel"); + await $(ListCollectorAddPage.lastName()).setValue("Clemens"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Olivia"); + await $(ListCollectorAddPage.lastName()).setValue("Clemens"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await verifyUrlContains(MoreThanTwoInterstitialPage.pageName); + }); + }); +}); diff --git a/tests/functional/spec/journeys/skipping/skip_conditions_not_set.spec.js b/tests/functional/spec/journeys/skipping/skip_conditions_not_set.spec.js new file mode 100644 index 0000000000..42f175774e --- /dev/null +++ b/tests/functional/spec/journeys/skipping/skip_conditions_not_set.spec.js @@ -0,0 +1,20 @@ +import FoodPage from "../../../generated_pages/skip_condition_not_set/food-block.page"; +import DrinkPage from "../../../generated_pages/skip_condition_not_set/drink-block.page"; +import SubmitPage from "../../../generated_pages/skip_condition_not_set/submit.page"; +import { click, verifyUrlContains } from "../../../helpers"; +describe("Skip Conditions - Not Set", () => { + beforeEach("Load the survey", async () => { + await browser.openQuestionnaire("test_skip_condition_not_set.json"); + }); + + it("Given I do not complete the first page, Then I should see the summary page", async () => { + await click(FoodPage.submit()); + await verifyUrlContains(SubmitPage.pageName); + }); + + it("Given I complete the first page, Then I should see the drink page", async () => { + await $(FoodPage.bacon()).click(); + await click(FoodPage.submit()); + await verifyUrlContains(DrinkPage.pageName); + }); +}); diff --git a/tests/functional/spec/journeys/skipping/skip_conditions_set.spec.js b/tests/functional/spec/journeys/skipping/skip_conditions_set.spec.js new file mode 100644 index 0000000000..3b9fe3ee4a --- /dev/null +++ b/tests/functional/spec/journeys/skipping/skip_conditions_set.spec.js @@ -0,0 +1,20 @@ +import FoodPage from "../../../generated_pages/skip_condition_set/food-block.page"; +import DrinkPage from "../../../generated_pages/skip_condition_set/drink-block.page"; +import SubmitPage from "../../../generated_pages/skip_condition_set/submit.page"; +import { click, verifyUrlContains } from "../../../helpers"; +describe("Skip Conditions - Set", () => { + beforeEach("Load the survey", async () => { + await browser.openQuestionnaire("test_skip_condition_set.json"); + }); + + it("Given I complete the first page, Then I should see the summary page", async () => { + await $(FoodPage.bacon()).click(); + await click(FoodPage.submit()); + await verifyUrlContains(SubmitPage.pageName); + }); + + it("Given I do not complete the first page, Then I should see the drink page", async () => { + await click(FoodPage.submit()); + await verifyUrlContains(DrinkPage.pageName); + }); +}); diff --git a/tests/functional/spec/language_code.spec.js b/tests/functional/spec/language_code.spec.js index 0995597de0..b47a0961ef 100644 --- a/tests/functional/spec/language_code.spec.js +++ b/tests/functional/spec/language_code.spec.js @@ -3,6 +3,7 @@ import DobPage from "../generated_pages/language/dob-block.page"; import NumberOfPeoplePage from "../generated_pages/language/number-of-people-block.page"; import ConfirmNumberOfPeoplePage from "../generated_pages/language/confirm-number-of-people.page"; import HubPage from "../base_pages/hub.page.js"; +import { click, verifyUrlContains } from "../helpers"; const PLURAL_TEST_DATA_SETS = [ { @@ -96,149 +97,151 @@ const PLURAL_TEST_DATA_SETS = [ ]; describe("Language Code", () => { - it("Given a launch language of Welsh, I should see Welsh text", () => { - browser.openQuestionnaire("test_language.json", { + it("Given a launch language of Welsh, I should see Welsh text", async () => { + await browser.openQuestionnaire("test_language.json", { language: "cy", }); - $(HubPage.submit()).click(); - expect($(NamePage.questionText()).getText()).to.contain("Rhowch enw"); - - $(NamePage.firstName()).setValue("Catherine"); - $(NamePage.lastName()).setValue("Zeta-Jones"); - $(NamePage.submit()).click(); - - $(DobPage.day()).setValue(25); - $(DobPage.month()).setValue(9); - $(DobPage.year()).setValue(1969); - $(DobPage.submit()).click(); - - $(NumberOfPeoplePage.numberOfPeople()).setValue(0); - $(NumberOfPeoplePage.submit()).click(); - $(ConfirmNumberOfPeoplePage.yes()).click(); - $(ConfirmNumberOfPeoplePage.submit()).click(); - - expect($(HubPage.heading()).getText()).to.contain("Teitl cyflwyno"); - expect($(HubPage.warning()).getText()).to.contain("Rhybudd cyflwyno"); - expect($(HubPage.guidance()).getText()).to.contain("Canllawiau cyflwyno"); - expect($(HubPage.submit()).getText()).to.contain("Botwm cyflwyno"); - $(HubPage.submit()).click(); - - expect(browser.getUrl()).to.contain("thank-you"); + await click(HubPage.submit()); + await expect(await $(NamePage.questionText()).getText()).toBe("Rhowch enw"); + + await $(NamePage.firstName()).setValue("Catherine"); + await $(NamePage.lastName()).setValue("Zeta-Jones"); + await click(NamePage.submit()); + + await $(DobPage.day()).setValue(25); + await $(DobPage.month()).setValue(9); + await $(DobPage.year()).setValue(1969); + await click(DobPage.submit()); + + await $(NumberOfPeoplePage.numberOfPeople()).setValue(0); + await click(NumberOfPeoplePage.submit()); + await $(ConfirmNumberOfPeoplePage.yes()).click(); + await click(ConfirmNumberOfPeoplePage.submit()); + + await expect(await $(HubPage.heading()).getText()).toBe("Teitl cyflwyno"); + await expect(await $(HubPage.warning()).getText()).toBe("Rhybudd cyflwyno"); + await expect(await $(HubPage.guidance()).getText()).toBe("Canllawiau cyflwyno"); + await expect(await $(HubPage.submit()).getText()).toBe("Botwm cyflwyno"); + await click(HubPage.submit()); + + await verifyUrlContains("thank-you"); }); - it("Given a launch language of English, I should see English text", () => { - browser.openQuestionnaire("test_language.json", { + it("Given a launch language of English, I should see English text", async () => { + await browser.openQuestionnaire("test_language.json", { language: "en", }); - $(HubPage.submit()).click(); - expect($(NamePage.questionText()).getText()).to.contain("Please enter a name"); - $(NamePage.firstName()).setValue("Catherine"); - $(NamePage.lastName()).setValue("Zeta-Jones"); - $(NamePage.submit()).click(); - - $(DobPage.day()).setValue(25); - $(DobPage.month()).setValue(9); - $(DobPage.year()).setValue(1969); - $(DobPage.submit()).click(); - - $(NumberOfPeoplePage.numberOfPeople()).setValue(0); - $(NumberOfPeoplePage.submit()).click(); - $(ConfirmNumberOfPeoplePage.yes()).click(); - $(ConfirmNumberOfPeoplePage.submit()).click(); - - expect($(HubPage.heading()).getText()).to.contain("Submission title"); - expect($(HubPage.warning()).getText()).to.contain("Submission warning"); - expect($(HubPage.guidance()).getText()).to.contain("Submission guidance"); - expect($(HubPage.submit()).getText()).to.contain("Submission button"); - $(HubPage.submit()).click(); - - expect(browser.getUrl()).to.contain("thank-you"); + await click(HubPage.submit()); + await expect(await $(NamePage.questionText()).getText()).toBe("Please enter a name"); + await $(NamePage.firstName()).setValue("Catherine"); + await $(NamePage.lastName()).setValue("Zeta-Jones"); + await click(NamePage.submit()); + + await $(DobPage.day()).setValue(25); + await $(DobPage.month()).setValue(9); + await $(DobPage.year()).setValue(1969); + await click(DobPage.submit()); + + await $(NumberOfPeoplePage.numberOfPeople()).setValue(0); + await click(NumberOfPeoplePage.submit()); + await $(ConfirmNumberOfPeoplePage.yes()).click(); + await click(ConfirmNumberOfPeoplePage.submit()); + + await expect(await $(HubPage.heading()).getText()).toBe("Submission title"); + await expect(await $(HubPage.warning()).getText()).toBe("Submission warning"); + await expect(await $(HubPage.guidance()).getText()).toBe("Submission guidance"); + await expect(await $(HubPage.submit()).getText()).toBe("Submission button"); + await click(HubPage.submit()); + + await verifyUrlContains("thank-you"); }); - it("Given a launch language of English, When I select Cymraeg, Then the language should be switched to Welsh", () => { - browser.openQuestionnaire("test_language.json", { + it("Given a launch language of English, When I select Cymraeg, Then the language should be switched to Welsh", async () => { + await browser.openQuestionnaire("test_language.json", { language: "en", }); - $(HubPage.submit()).click(); - expect($(NamePage.questionText()).getText()).to.contain("Please enter a name"); - $(NamePage.switchLanguage("cy")).click(); - expect($(NamePage.questionText()).getText()).to.contain("Rhowch enw"); - $(NamePage.switchLanguage("en")).click(); - - $(NamePage.firstName()).setValue("Catherine"); - $(NamePage.lastName()).setValue("Zeta-Jones"); - $(NamePage.submit()).click(); - - $(DobPage.day()).setValue(25); - $(DobPage.month()).setValue(9); - $(DobPage.year()).setValue(1969); - $(DobPage.submit()).click(); - - $(NumberOfPeoplePage.numberOfPeople()).setValue(0); - $(NumberOfPeoplePage.submit()).click(); - $(ConfirmNumberOfPeoplePage.yes()).click(); - $(ConfirmNumberOfPeoplePage.submit()).click(); - - expect($(HubPage.heading()).getText()).to.contain("Submission title"); - expect($(HubPage.warning()).getText()).to.contain("Submission warning"); - expect($(HubPage.guidance()).getText()).to.contain("Submission guidance"); - expect($(HubPage.submit()).getText()).to.contain("Submission button"); - $(HubPage.switchLanguage("cy")).click(); - expect($(HubPage.heading()).getText()).to.contain("Teitl cyflwyno"); - expect($(HubPage.warning()).getText()).to.contain("Rhybudd cyflwyno"); - expect($(HubPage.guidance()).getText()).to.contain("Canllawiau cyflwyno"); - expect($(HubPage.submit()).getText()).to.contain("Botwm cyflwyno"); - $(HubPage.submit()).click(); - - expect(browser.getUrl()).to.contain("thank-you"); + await click(HubPage.submit()); + await expect(await $(NamePage.questionText()).getText()).toBe("Please enter a name"); + await expect(await $("header").getText()).toContain("Test Language Survey"); + await $(NamePage.switchLanguage("cy")).click(); + await expect(await $(NamePage.questionText()).getText()).toBe("Rhowch enw"); + await expect(await $("header").getText()).toContain("Arolwg Iaith Prawf"); + await $(NamePage.switchLanguage("en")).click(); + + await $(NamePage.firstName()).setValue("Catherine"); + await $(NamePage.lastName()).setValue("Zeta-Jones"); + await click(NamePage.submit()); + + await $(DobPage.day()).setValue(25); + await $(DobPage.month()).setValue(9); + await $(DobPage.year()).setValue(1969); + await click(DobPage.submit()); + + await $(NumberOfPeoplePage.numberOfPeople()).setValue(0); + await click(NumberOfPeoplePage.submit()); + await $(ConfirmNumberOfPeoplePage.yes()).click(); + await click(ConfirmNumberOfPeoplePage.submit()); + + await expect(await $(HubPage.heading()).getText()).toBe("Submission title"); + await expect(await $(HubPage.warning()).getText()).toBe("Submission warning"); + await expect(await $(HubPage.guidance()).getText()).toBe("Submission guidance"); + await expect(await $(HubPage.submit()).getText()).toBe("Submission button"); + await $(HubPage.switchLanguage("cy")).click(); + await expect(await $(HubPage.heading()).getText()).toBe("Teitl cyflwyno"); + await expect(await $(HubPage.warning()).getText()).toBe("Rhybudd cyflwyno"); + await expect(await $(HubPage.guidance()).getText()).toBe("Canllawiau cyflwyno"); + await expect(await $(HubPage.submit()).getText()).toBe("Botwm cyflwyno"); + await click(HubPage.submit()); + + await verifyUrlContains("thank-you"); }); - it("Given a launch language of Welsh, When I select English, Then the language should be switched to English", () => { - browser.openQuestionnaire("test_language.json", { + it("Given a launch language of Welsh, When I select English, Then the language should be switched to English", async () => { + await browser.openQuestionnaire("test_language.json", { language: "cy", }); - $(HubPage.submit()).click(); - expect($(NamePage.questionText()).getText()).to.contain("Rhowch enw"); - $(NamePage.switchLanguage("en")).click(); - expect($(NamePage.questionText()).getText()).to.contain("Please enter a name"); + await click(HubPage.submit()); + await expect(await $(NamePage.questionText()).getText()).toBe("Rhowch enw"); + await $(NamePage.switchLanguage("en")).click(); + await expect(await $(NamePage.questionText()).getText()).toBe("Please enter a name"); }); describe("Given a launch language of English and a question with plural forms, When I select switch languages, Then the plural forms are displayed correctly for the chosen language", () => { for (const dataSet of PLURAL_TEST_DATA_SETS) { const numberOfPeople = dataSet.count; - it(`Test plural count: ${numberOfPeople}`, () => { - browser.openQuestionnaire("test_language.json", { + it(`Test plural count: ${numberOfPeople}`, async () => { + await browser.openQuestionnaire("test_language.json", { language: "en", }); - $(HubPage.submit()).click(); - expect($(NamePage.questionText()).getText()).to.contain("Please enter a name"); - $(NamePage.firstName()).setValue("Catherine"); - $(NamePage.lastName()).setValue("Zeta-Jones"); - $(NamePage.submit()).click(); + await click(HubPage.submit()); + await expect(await $(NamePage.questionText()).getText()).toBe("Please enter a name"); + await $(NamePage.firstName()).setValue("Catherine"); + await $(NamePage.lastName()).setValue("Zeta-Jones"); + await click(NamePage.submit()); - $(DobPage.day()).setValue(25); - $(DobPage.month()).setValue(9); - $(DobPage.year()).setValue(1969); - $(DobPage.submit()).click(); + await $(DobPage.day()).setValue(25); + await $(DobPage.month()).setValue(9); + await $(DobPage.year()).setValue(1969); + await click(DobPage.submit()); - $(NumberOfPeoplePage.numberOfPeople()).setValue(numberOfPeople); - $(NumberOfPeoplePage.submit()).click(); + await $(NumberOfPeoplePage.numberOfPeople()).setValue(numberOfPeople); + await click(NumberOfPeoplePage.submit()); - expect($(ConfirmNumberOfPeoplePage.questionText()).getText()).to.contain(dataSet.question_title.en); - expect($(ConfirmNumberOfPeoplePage.yesLabel()).getText()).to.contain(dataSet.answer.en); + await expect(await $(ConfirmNumberOfPeoplePage.questionText()).getText()).toEqual(dataSet.question_title.en); + await expect(await $(ConfirmNumberOfPeoplePage.yesLabel()).getText()).toEqual(dataSet.answer.en); - $(ConfirmNumberOfPeoplePage.switchLanguage("cy")).click(); + await $(ConfirmNumberOfPeoplePage.switchLanguage("cy")).click(); - expect($(ConfirmNumberOfPeoplePage.questionText()).getText()).to.contain(dataSet.question_title.cy); - expect($(ConfirmNumberOfPeoplePage.yesLabel()).getText()).to.contain(dataSet.answer.cy); + await expect(await $(ConfirmNumberOfPeoplePage.questionText()).getText()).toEqual(dataSet.question_title.cy); + await expect(await $(ConfirmNumberOfPeoplePage.yesLabel()).getText()).toEqual(dataSet.answer.cy); - $(ConfirmNumberOfPeoplePage.yes()).click(); - $(ConfirmNumberOfPeoplePage.submit()).click(); + await $(ConfirmNumberOfPeoplePage.yes()).click(); + await click(ConfirmNumberOfPeoplePage.submit()); }); } }); diff --git a/tests/functional/spec/launch_with_cir.spec.js b/tests/functional/spec/launch_with_cir.spec.js new file mode 100644 index 0000000000..dfc5105055 --- /dev/null +++ b/tests/functional/spec/launch_with_cir.spec.js @@ -0,0 +1,18 @@ +import { click, verifyUrlContains } from "../helpers"; +import NameBlockPage from "../generated_pages/textfield/name-block.page.js"; +import HubPage from "../base_pages/hub.page"; +import ThankYouPage from "../base_pages/thank-you.page"; + +describe("Launch a survey from the collection instrument registry", () => { + it("Given I retrieve a Collection Instrument, When I Launch, Then I am able to complete the survey as normal", async () => { + await browser.openQuestionnaire(null, { + version: "v2", + cirInstrumentId: "fd4a527f-c126-da2d-8ee6-51663a43e416", + }); + await verifyUrlContains(NameBlockPage.pageName); + await $(NameBlockPage.name()).setValue("Joe"); + await click(NameBlockPage.submit()); + await click(HubPage.submit()); + await verifyUrlContains(ThankYouPage.pageName); + }); +}); diff --git a/tests/functional/spec/list_collector.spec.js b/tests/functional/spec/list_collector.spec.js deleted file mode 100644 index 69c9de3d30..0000000000 --- a/tests/functional/spec/list_collector.spec.js +++ /dev/null @@ -1,230 +0,0 @@ -import checkPeopleInList from "../helpers"; -import AnotherListCollectorPage from "../generated_pages/list_collector/another-list-collector-block.page.js"; -import AnotherListCollectorAddPage from "../generated_pages/list_collector/another-list-collector-block-add.page.js"; -import AnotherListCollectorEditPage from "../generated_pages/list_collector/another-list-collector-block-edit.page.js"; -import AnotherListCollectorRemovePage from "../generated_pages/list_collector/another-list-collector-block-remove.page.js"; -import ListCollectorPage from "../generated_pages/list_collector/list-collector.page.js"; -import ListCollectorAddPage from "../generated_pages/list_collector/list-collector-add.page.js"; -import ListCollectorEditPage from "../generated_pages/list_collector/list-collector-edit.page.js"; -import ListCollectorRemovePage from "../generated_pages/list_collector/list-collector-remove.page.js"; -import NextInterstitialPage from "../generated_pages/list_collector/next-interstitial.page.js"; -import SummaryPage from "../generated_pages/list_collector/section-summary.page.js"; -import PrimaryPersonListCollectorPage from "../generated_pages/list_collector_section_summary/primary-person-list-collector.page.js"; -import PrimaryPersonListCollectorAddPage from "../generated_pages/list_collector_section_summary/primary-person-list-collector-add.page.js"; -import SectionSummaryListCollectorPage from "../generated_pages/list_collector_section_summary/list-collector.page.js"; -import SectionSummaryListCollectorAddPage from "../generated_pages/list_collector_section_summary/list-collector-add.page.js"; -import SectionSummaryListCollectorEditPage from "../generated_pages/list_collector_section_summary/list-collector-edit.page.js"; -import SectionSummaryListCollectorRemovePage from "../generated_pages/list_collector_section_summary/list-collector-remove.page.js"; -import VisitorListCollectorPage from "../generated_pages/list_collector_section_summary/visitor-list-collector.page.js"; -import VisitorListCollectorAddPage from "../generated_pages/list_collector_section_summary/visitor-list-collector-add.page.js"; -import PeopleListSectionSummaryPage from "../generated_pages/list_collector_section_summary/section-summary.page.js"; -import { SubmitPage } from "../base_pages/submit.page.js"; - -describe("List Collector", () => { - describe("Given a normal journey through the list collector without variants", () => { - before("Load the survey", () => { - browser.openQuestionnaire("test_list_collector.json"); - }); - - it("The user is able to add members of the household", () => { - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("Marcus"); - $(ListCollectorAddPage.lastName()).setValue("Twin"); - $(ListCollectorAddPage.submit()).click(); - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("Samuel"); - $(ListCollectorAddPage.lastName()).setValue("Clemens"); - $(ListCollectorAddPage.submit()).click(); - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("Olivia"); - $(ListCollectorAddPage.lastName()).setValue("Clemens"); - $(ListCollectorAddPage.submit()).click(); - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("Suzy"); - $(ListCollectorAddPage.lastName()).setValue("Clemens"); - $(ListCollectorAddPage.submit()).click(); - }); - - it("The collector shows all of the household members in the summary", () => { - const peopleExpected = ["Marcus Twin", "Samuel Clemens", "Olivia Clemens", "Suzy Clemens"]; - checkPeopleInList(peopleExpected, ListCollectorPage.listLabel); - }); - - it("The questionnaire allows the name of a person to be changed", () => { - $(ListCollectorPage.listEditLink(1)).click(); - $(ListCollectorEditPage.firstName()).setValue("Mark"); - $(ListCollectorEditPage.lastName()).setValue("Twain"); - $(ListCollectorEditPage.submit()).click(); - expect($(ListCollectorPage.listLabel(1)).getText()).to.equal("Mark Twain"); - }); - - it("The questionnaire allows me to remove the first person (Mark Twain) from the summary", () => { - $(ListCollectorPage.listRemoveLink(1)).click(); - $(ListCollectorRemovePage.yes()).click(); - $(ListCollectorRemovePage.submit()).click(); - }); - - it("The collector summary does not show Mark Twain anymore.", () => { - expect($(ListCollectorPage.listLabel(1)).getText()).to.not.have.string("Mark Twain"); - expect($(ListCollectorPage.listLabel(3)).getText()).to.equal("Suzy Clemens"); - }); - - it("The questionnaire allows more people to be added", () => { - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - expect($(ListCollectorAddPage.questionText()).getText()).to.contain("What is the name of the person"); - $(ListCollectorAddPage.firstName()).setValue("Clara"); - $(ListCollectorAddPage.lastName()).setValue("Clemens"); - $(ListCollectorAddPage.submit()).click(); - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("Jean"); - $(ListCollectorAddPage.lastName()).setValue("Clemens"); - $(ListCollectorAddPage.submit()).click(); - }); - - it("The user is returned to the list collector when the cancel link is clicked on the add page.", () => { - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("Someone"); - $(ListCollectorAddPage.lastName()).setValue("Else"); - $(ListCollectorAddPage.cancelAndReturn()).click(); - expect(browser.getUrl()).to.contain(ListCollectorPage.pageName); - }); - - it("The user is returned to the list collector when the cancel link is clicked on the edit page.", () => { - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("Someone"); - $(ListCollectorAddPage.lastName()).setValue("Else"); - $(ListCollectorAddPage.submit()).click(); - $(ListCollectorPage.listEditLink(1)).click(); - $(ListCollectorEditPage.cancelAndReturn()).click(); - expect(browser.getUrl()).to.contain(ListCollectorPage.pageName); - }); - - it("The collector shows everyone on the summary", () => { - const peopleExpected = ["Samuel Clemens", "Olivia Clemens", "Suzy Clemens", "Clara Clemens", "Jean Clemens"]; - checkPeopleInList(peopleExpected, ListCollectorPage.listLabel); - }); - - it("When No is answered on the list collector the user sees an interstitial", () => { - $(ListCollectorPage.no()).click(); - $(ListCollectorPage.submit()).click(); - expect(browser.getUrl()).to.contain(NextInterstitialPage.pageName); - $(NextInterstitialPage.submit()).click(); - }); - - it("After the interstitial, the user should be on the second list collector page", () => { - expect(browser.getUrl()).to.contain(AnotherListCollectorPage.pageName); - }); - - it("The collector still shows the same list of people on the summary", () => { - const peopleExpected = ["Samuel Clemens", "Olivia Clemens", "Suzy Clemens", "Clara Clemens", "Jean Clemens"]; - checkPeopleInList(peopleExpected, ListCollectorPage.listLabel); - }); - - it("The collector allows the user to add another person to the same list", () => { - $(AnotherListCollectorPage.yes()).click(); - $(AnotherListCollectorPage.submit()).click(); - $(AnotherListCollectorAddPage.firstName()).setValue("Someone"); - $(AnotherListCollectorAddPage.lastName()).setValue("Else"); - $(AnotherListCollectorAddPage.submit()).click(); - expect($(AnotherListCollectorPage.listLabel(6)).getText()).to.equal("Someone Else"); - }); - - it("The collector allows the user to remove a person again", () => { - $(AnotherListCollectorPage.listRemoveLink(5)).click(); - $(AnotherListCollectorRemovePage.yes()).click(); - $(AnotherListCollectorRemovePage.submit()).click(); - }); - - it("The user is returned to the list collector when the previous link is clicked.", () => { - $(AnotherListCollectorPage.listRemoveLink(1)).click(); - $(AnotherListCollectorRemovePage.previous()).click(); - expect(browser.getUrl()).to.contain(AnotherListCollectorPage.pageName); - $(AnotherListCollectorPage.listEditLink(1)).click(); - $(AnotherListCollectorEditPage.previous()).click(); - expect(browser.getUrl()).to.contain(AnotherListCollectorPage.pageName); - $(AnotherListCollectorPage.yes()).click(); - $(AnotherListCollectorPage.submit()).click(); - $(AnotherListCollectorEditPage.previous()).click(); - expect(browser.getUrl()).to.contain(AnotherListCollectorPage.pageName); - }); - - it("The questionnaire shows the confirmation page when no more people to add", () => { - $(AnotherListCollectorPage.no()).click(); - $(AnotherListCollectorPage.submit()).click(); - expect(browser.getUrl()).to.contain("/sections/section/"); - }); - - it("The questionnaire allows submission", () => { - $(SummaryPage.submit()).click(); - $(SubmitPage.submit()).click(); - expect(browser.getUrl()).to.contain("thank-you"); - }); - }); - - describe("Given I start a list collector survey and complete to Section Summary", () => { - beforeEach(() => { - browser.openQuestionnaire("test_list_collector_section_summary.json"); - $(PrimaryPersonListCollectorPage.yes()).click(); - $(PrimaryPersonListCollectorPage.submit()).click(); - $(PrimaryPersonListCollectorAddPage.firstName()).setValue("Marcus"); - $(PrimaryPersonListCollectorAddPage.lastName()).setValue("Twin"); - $(PrimaryPersonListCollectorAddPage.submit()).click(); - $(SectionSummaryListCollectorPage.yes()).click(); - $(SectionSummaryListCollectorPage.submit()).click(); - $(SectionSummaryListCollectorAddPage.firstName()).setValue("Samuel"); - $(SectionSummaryListCollectorAddPage.lastName()).setValue("Clemens"); - $(SectionSummaryListCollectorAddPage.submit()).click(); - $(SectionSummaryListCollectorPage.no()).click(); - $(SectionSummaryListCollectorPage.submit()).click(); - $(VisitorListCollectorPage.yes()).click(); - $(VisitorListCollectorPage.submit()).click(); - $(VisitorListCollectorAddPage.firstNameVisitor()).setValue("Olivia"); - $(VisitorListCollectorAddPage.lastNameVisitor()).setValue("Clemens"); - $(VisitorListCollectorAddPage.submit()).click(); - $(VisitorListCollectorPage.no()).click(); - $(VisitorListCollectorPage.submit()).click(); - }); - - it("The section summary should display contents of the list collector", () => { - expect($(PeopleListSectionSummaryPage.peopleListLabel(1)).getText()).to.contain("Marcus Twin (You)"); - expect($(PeopleListSectionSummaryPage.peopleListLabel(2)).getText()).to.contain("Samuel Clemens"); - expect($(PeopleListSectionSummaryPage.visitorsListLabel(1)).getText()).to.contain("Olivia Clemens"); - }); - - it("When the user adds an item to the list, They should return to the section summary and it should display the updated list", () => { - $(PeopleListSectionSummaryPage.visitorsListAddLink(1)).click(); - $(VisitorListCollectorAddPage.firstNameVisitor()).setValue("Joe"); - $(VisitorListCollectorAddPage.lastNameVisitor()).setValue("Bloggs"); - $(VisitorListCollectorAddPage.submit()).click(); - expect($(PeopleListSectionSummaryPage.visitorsListLabel(2)).getText()).to.contain("Joe Bloggs"); - }); - - it("When the user removes an item from the list, They should return to the section summary and it should display the updated list", () => { - $(PeopleListSectionSummaryPage.peopleListRemoveLink(2)).click(); - $(SectionSummaryListCollectorRemovePage.yes()).click(); - $(SectionSummaryListCollectorRemovePage.submit()).click(); - expect($(PeopleListSectionSummaryPage.visitorsListLabel(2)).isExisting()).to.equal(false); - }); - - it("When the user updates the list, They should return to the section summary and it should display the updated list", () => { - $(PeopleListSectionSummaryPage.peopleListEditLink(1)).click(); - $(SectionSummaryListCollectorEditPage.firstName()).setValue("Mark"); - $(SectionSummaryListCollectorEditPage.lastName()).setValue("Twain"); - $(SectionSummaryListCollectorEditPage.submit()).click(); - expect($(PeopleListSectionSummaryPage.peopleListLabel(1)).getText()).to.contain("Mark Twain (You)"); - }); - - it("When the user removes an item from the list, They should see the individual response guidance", () => { - $(PeopleListSectionSummaryPage.peopleListRemoveLink(2)).click(); - expect($(SectionSummaryListCollectorRemovePage.individualResponseGuidance()).isExisting()).to.equal(true); - }); - }); -}); diff --git a/tests/functional/spec/list_collector/list_collector.spec.js b/tests/functional/spec/list_collector/list_collector.spec.js new file mode 100644 index 0000000000..838733c9e8 --- /dev/null +++ b/tests/functional/spec/list_collector/list_collector.spec.js @@ -0,0 +1,241 @@ +import { checkItemsInList, click, verifyUrlContains } from "../../helpers"; +import AnotherListCollectorPage from "../../generated_pages/list_collector/another-list-collector-block.page.js"; +import AnotherListCollectorAddPage from "../../generated_pages/list_collector/another-list-collector-block-add.page.js"; +import AnotherListCollectorEditPage from "../../generated_pages/list_collector/another-list-collector-block-edit.page.js"; +import AnotherListCollectorRemovePage from "../../generated_pages/list_collector/another-list-collector-block-remove.page.js"; +import ListCollectorPage from "../../generated_pages/list_collector/list-collector.page.js"; +import ListCollectorAddPage from "../../generated_pages/list_collector/list-collector-add.page.js"; +import ListCollectorEditPage from "../../generated_pages/list_collector/list-collector-edit.page.js"; +import ListCollectorRemovePage from "../../generated_pages/list_collector/list-collector-remove.page.js"; +import NextInterstitialPage from "../../generated_pages/list_collector/next-interstitial.page.js"; +import SummaryPage from "../../generated_pages/list_collector/section-summary.page.js"; +import PrimaryPersonListCollectorPage from "../../generated_pages/list_collector_list_summary/primary-person-list-collector.page.js"; +import PrimaryPersonListCollectorAddPage from "../../generated_pages/list_collector_list_summary/primary-person-list-collector-add.page.js"; +import SectionSummaryListCollectorPage from "../../generated_pages/list_collector_list_summary/list-collector.page.js"; +import SectionSummaryListCollectorAddPage from "../../generated_pages/list_collector_list_summary/list-collector-add.page.js"; +import SectionSummaryListCollectorEditPage from "../../generated_pages/list_collector_list_summary/list-collector-edit.page.js"; +import SectionSummaryListCollectorRemovePage from "../../generated_pages/list_collector_list_summary/list-collector-remove.page.js"; +import VisitorListCollectorPage from "../../generated_pages/list_collector_list_summary/visitor-list-collector.page.js"; +import VisitorListCollectorAddPage from "../../generated_pages/list_collector_list_summary/visitor-list-collector-add.page.js"; +import PeopleListSectionSummaryPage from "../../generated_pages/list_collector_list_summary/section-summary.page.js"; +import { SubmitPage } from "../../base_pages/submit.page.js"; +import IntroductionPage from "../../generated_pages/list_collector_list_summary/introduction.page.js"; + +describe("List Collector", () => { + describe("Given a normal journey through the list collector without variants", () => { + before("Load the survey", async () => { + await browser.openQuestionnaire("test_list_collector.json"); + }); + + it("The user is able to add members of the household", async () => { + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Marcus"); + await $(ListCollectorAddPage.lastName()).setValue("Twin"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.yes()).click(); + // eslint-disable-next-line no-undef + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Samuel"); + await $(ListCollectorAddPage.lastName()).setValue("Clemens"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Olivia"); + await $(ListCollectorAddPage.lastName()).setValue("Clemens"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Suzy"); + await $(ListCollectorAddPage.lastName()).setValue("Clemens"); + await click(ListCollectorAddPage.submit()); + }); + + it("The collector shows all of the household members in the summary", async () => { + const peopleExpected = ["Marcus Twin", "Samuel Clemens", "Olivia Clemens", "Suzy Clemens"]; + await checkItemsInList(peopleExpected, ListCollectorPage.listLabel); + }); + + it("The questionnaire allows the name of a person to be changed", async () => { + await $(ListCollectorPage.listEditLink(1)).click(); + await $(ListCollectorEditPage.firstName()).setValue("Mark"); + await $(ListCollectorEditPage.lastName()).setValue("Twain"); + await click(ListCollectorEditPage.submit()); + await expect(await $(ListCollectorPage.listLabel(1)).getText()).toBe("Mark Twain"); + }); + + it("The questionnaire allows me to remove the first person (Mark Twain) from the summary", async () => { + await $(ListCollectorPage.listRemoveLink(1)).click(); + await $(ListCollectorRemovePage.yes()).click(); + await click(ListCollectorRemovePage.submit()); + }); + + it("The collector summary does not show Mark Twain anymore.", async () => { + await expect(await $(ListCollectorPage.listLabel(1)).getText()).not.toBe("Mark Twain"); + await expect(await $(ListCollectorPage.listLabel(3)).getText()).toBe("Suzy Clemens"); + }); + + it("The questionnaire allows more people to be added", async () => { + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await expect(await $(ListCollectorAddPage.questionText()).getText()).toBe("What is the name of the person?"); + await $(ListCollectorAddPage.firstName()).setValue("Clara"); + await $(ListCollectorAddPage.lastName()).setValue("Clemens"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Jean"); + await $(ListCollectorAddPage.lastName()).setValue("Clemens"); + await click(ListCollectorAddPage.submit()); + }); + + it("The user is returned to the list collector when the cancel link is clicked on the add page.", async () => { + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Someone"); + await $(ListCollectorAddPage.lastName()).setValue("Else"); + await $(ListCollectorAddPage.cancelAndReturn()).click(); + await verifyUrlContains(ListCollectorPage.pageName); + }); + + it("The user is returned to the list collector when the cancel link is clicked on the edit page.", async () => { + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Someone"); + await $(ListCollectorAddPage.lastName()).setValue("Else"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.listEditLink(1)).click(); + await $(ListCollectorEditPage.cancelAndReturn()).click(); + await verifyUrlContains(ListCollectorPage.pageName); + }); + + it("The collector shows everyone on the summary", async () => { + const peopleExpected = ["Samuel Clemens", "Olivia Clemens", "Suzy Clemens", "Clara Clemens", "Jean Clemens"]; + await checkItemsInList(peopleExpected, ListCollectorPage.listLabel); + }); + + it("When No is answered on the list collector the user sees an interstitial", async () => { + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await verifyUrlContains(NextInterstitialPage.pageName); + await click(NextInterstitialPage.submit()); + }); + + it("After the interstitial, the user should be on the second list collector page", async () => { + await verifyUrlContains(AnotherListCollectorPage.pageName); + }); + + it("The collector still shows the same list of people on the summary", async () => { + const peopleExpected = ["Samuel Clemens", "Olivia Clemens", "Suzy Clemens", "Clara Clemens", "Jean Clemens"]; + await checkItemsInList(peopleExpected, ListCollectorPage.listLabel); + }); + + it("The collector allows the user to add another person to the same list", async () => { + await $(AnotherListCollectorPage.yes()).click(); + await click(AnotherListCollectorPage.submit()); + await $(AnotherListCollectorAddPage.firstName()).setValue("Someone"); + await $(AnotherListCollectorAddPage.lastName()).setValue("Else"); + await click(AnotherListCollectorAddPage.submit()); + await expect(await $(AnotherListCollectorPage.listLabel(6)).getText()).toBe("Someone Else"); + }); + + it("The collector allows the user to remove a person again", async () => { + await $(AnotherListCollectorPage.listRemoveLink(5)).click(); + await $(AnotherListCollectorRemovePage.yes()).click(); + await click(AnotherListCollectorRemovePage.submit()); + }); + + it("The user is returned to the list collector when the previous link is clicked.", async () => { + await $(AnotherListCollectorPage.listRemoveLink(1)).click(); + await $(AnotherListCollectorRemovePage.previous()).click(); + await verifyUrlContains(AnotherListCollectorPage.pageName); + await $(AnotherListCollectorPage.listEditLink(1)).click(); + await $(AnotherListCollectorEditPage.previous()).click(); + await verifyUrlContains(AnotherListCollectorPage.pageName); + await $(AnotherListCollectorPage.yes()).click(); + await click(AnotherListCollectorPage.submit()); + await $(AnotherListCollectorEditPage.previous()).click(); + await verifyUrlContains(AnotherListCollectorPage.pageName); + }); + + it("The questionnaire shows the confirmation page when no more people to add", async () => { + await $(AnotherListCollectorPage.no()).click(); + await click(AnotherListCollectorPage.submit()); + await verifyUrlContains("/sections/section/"); + }); + + it("The questionnaire allows submission", async () => { + await click(SummaryPage.submit()); + await click(SubmitPage.submit()); + await verifyUrlContains("thank-you"); + }); + }); + + describe("Given I start a list collector survey and complete to Section Summary", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_list_collector_list_summary.json"); + await click(IntroductionPage.submit()); + await $(PrimaryPersonListCollectorPage.yes()).click(); + await click(PrimaryPersonListCollectorPage.submit()); + await $(PrimaryPersonListCollectorAddPage.firstName()).setValue("Marcus"); + await $(PrimaryPersonListCollectorAddPage.lastName()).setValue("Twin"); + await click(PrimaryPersonListCollectorAddPage.submit()); + await $(SectionSummaryListCollectorPage.yes()).click(); + await click(SectionSummaryListCollectorPage.submit()); + await $(SectionSummaryListCollectorAddPage.firstName()).setValue("Samuel"); + await $(SectionSummaryListCollectorAddPage.lastName()).setValue("Clemens"); + await click(SectionSummaryListCollectorAddPage.submit()); + await $(SectionSummaryListCollectorPage.no()).click(); + await click(SectionSummaryListCollectorPage.submit()); + await $(VisitorListCollectorPage.yes()).click(); + await click(VisitorListCollectorPage.submit()); + await $(VisitorListCollectorAddPage.firstNameVisitor()).setValue("Olivia"); + await $(VisitorListCollectorAddPage.lastNameVisitor()).setValue("Clemens"); + await click(VisitorListCollectorAddPage.submit()); + await $(VisitorListCollectorPage.no()).click(); + await click(VisitorListCollectorPage.submit()); + }); + + it("The section summary should display contents of the list collector", async () => { + await expect(await $(PeopleListSectionSummaryPage.peopleListLabel(1)).getText()).toBe("Marcus Twin (You)"); + await expect(await $(PeopleListSectionSummaryPage.peopleListLabel(2)).getText()).toBe("Samuel Clemens"); + await expect(await $(PeopleListSectionSummaryPage.visitorsListLabel(1)).getText()).toBe("Olivia Clemens"); + }); + + it("When the user adds an item to the list, They should return to the section summary and it should display the updated list", async () => { + await $(PeopleListSectionSummaryPage.visitorsListAddLink(1)).click(); + await $(VisitorListCollectorAddPage.firstNameVisitor()).setValue("Joe"); + await $(VisitorListCollectorAddPage.lastNameVisitor()).setValue("Bloggs"); + await click(VisitorListCollectorAddPage.submit()); + await $(VisitorListCollectorPage.no()).click(); + await click(VisitorListCollectorPage.submit()); + await expect(await $(PeopleListSectionSummaryPage.visitorsListLabel(2)).getText()).toBe("Joe Bloggs"); + }); + + it("When the user removes an item from the list, They should return to the section summary and it should display the updated list", async () => { + await $(PeopleListSectionSummaryPage.peopleListRemoveLink(2)).click(); + await $(SectionSummaryListCollectorRemovePage.yes()).click(); + await click(SectionSummaryListCollectorRemovePage.submit()); + await expect(await $(PeopleListSectionSummaryPage.visitorsListLabel(2)).isExisting()).toBe(false); + }); + + it("When the user updates the list, They should return to the section summary and it should display the updated list", async () => { + await $(PeopleListSectionSummaryPage.peopleListEditLink(1)).click(); + await $(SectionSummaryListCollectorEditPage.firstName()).setValue("Mark"); + await $(SectionSummaryListCollectorEditPage.lastName()).setValue("Twain"); + await click(SectionSummaryListCollectorEditPage.submit()); + await expect(await $(PeopleListSectionSummaryPage.peopleListLabel(1)).getText()).toBe("Mark Twain (You)"); + }); + + it("When the user removes an item from the list, They should see the individual response guidance", async () => { + await $(PeopleListSectionSummaryPage.peopleListRemoveLink(2)).click(); + await expect(await $(SectionSummaryListCollectorRemovePage.individualResponseGuidance()).isExisting()).toBe(true); + }); + + it("When the user reaches the submit page and navigates back, They should see the Section Summary", async () => { + await click(PeopleListSectionSummaryPage.submit()); + await click(SubmitPage.previous()); + await verifyUrlContains(PeopleListSectionSummaryPage.pageName); + }); + }); +}); diff --git a/tests/functional/spec/list_collector/list_collector_content.spec.js b/tests/functional/spec/list_collector/list_collector_content.spec.js new file mode 100644 index 0000000000..43c02d3f6d --- /dev/null +++ b/tests/functional/spec/list_collector/list_collector_content.spec.js @@ -0,0 +1,185 @@ +import AnyOtherCompaniesOrBranchesPage from "../../generated_pages/list_collector_content_page/any-other-companies-or-branches.page.js"; +import AnyCompaniesOrBranchesAddPage from "../../generated_pages/list_collector_content_page/any-other-companies-or-branches-add.page.js"; +import AnyCompaniesOrBranchesRemovePage from "../../generated_pages/list_collector_content_page/any-other-companies-or-branches-remove.page.js"; + +import AnyCompaniesOrBranchesPage from "../../generated_pages/list_collector_content_page/any-companies-or-branches.page"; +import CompaniesSummaryPage from "../../generated_pages/list_collector_content_page/section-companies-summary.page"; +import HubPage from "../../base_pages/hub.page"; +import ResponsiblePartyQuestionPage from "../../generated_pages/list_collector_content_page/responsible-party.page"; +import ListCollectorFirstRepeatingBlockPage from "../../generated_pages/list_collector_content_page/companies-repeating-block-1-repeating-block.page"; +import ListCollectorSecondRepeatingBlockPage from "../../generated_pages/list_collector_content_page/companies-repeating-block-2-repeating-block.page"; +import ListCollectorContentPage from "../../generated_pages/list_collector_content_page/list-collector-content.page"; +import ListCollectorContentSectionSummaryPage from "../../generated_pages/list_collector_content_page/section-list-collector-contents-summary.page"; +import ConfirmationCheckboxPage from "../../generated_pages/list_collector_content_page/confirmation-checkbox.page"; +import { listItemComplete, click, verifyUrlContains } from "../../helpers"; + +describe("List Collector Section Summary and Summary Items", () => { + describe("Given I launch the test list collector section summary items survey", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_list_collector_content_page.json"); + }); + it("When I get to the Hub, Then from there the next block in list collector content section should be list collector content page.", async () => { + await fillInListCollectorSection(); + await verifyUrlContains(HubPage.url()); + await expect(await $(HubPage.summaryRowState("section-list-collector-contents")).getText()).toBe("Not started"); + await click(HubPage.submit()); + await $(ResponsiblePartyQuestionPage.yes()).click(); + await click(ResponsiblePartyQuestionPage.submit()); + await verifyUrlContains(ListCollectorContentPage.url()); + }); + it("When I get to the list collector content page, Then the relevant content and button is displayed.", async () => { + await fillInListCollectorSection(); + await click(HubPage.submit()); + await $(ResponsiblePartyQuestionPage.yes()).click(); + await click(ResponsiblePartyQuestionPage.submit()); + await expect(await $(ListCollectorContentPage.heading()).getHTML()).toContain("Companies"); + await expect(await $("#main-content > p").getText()).toBe( + "You have previously reported the following companies. Press continue to updated registration and trading information.", + ); + await expect(await $("#main-content > #guidance-1").getText()).toContain("Include all companies"); + await expect(await $("#main-content > #definition").getText()).toBe("Companies definition"); + await expect(await $(ListCollectorContentPage.submit()).getText()).toBe("Continue"); + }); + it("When I get to list collector content block section, Then I should be able to complete repeating blocks and get to the summary.", async () => { + await fillInListCollectorSection(); + await click(HubPage.submit()); + await $(ResponsiblePartyQuestionPage.yes()).click(); + await click(ResponsiblePartyQuestionPage.submit()); + await click(ListCollectorContentPage.submit()); + await completeRepeatingBlocks(123, 1, 1, 1990, true, true); + await click(ListCollectorContentPage.submit()); + await completeRepeatingBlocks(456, 1, 1, 1990, true, true); + await click(ListCollectorContentPage.submit()); + await verifyUrlContains(ListCollectorContentSectionSummaryPage.url()); + }); + it("When I fill in first item repeating blocks, Then after going back to the hub the section should be in progress.", async () => { + await fillInListCollectorSection(); + await click(HubPage.submit()); + await $(ResponsiblePartyQuestionPage.yes()).click(); + await click(ResponsiblePartyQuestionPage.submit()); + await click(ListCollectorContentPage.submit()); + await completeRepeatingBlocks(123, 1, 1, 1990, true, true); + await $(ListCollectorContentPage.previous()).click(); + await $(ResponsiblePartyQuestionPage.previous()).click(); + await expect(await $(HubPage.summaryRowState("section-list-collector-contents")).getText()).toBe("Partially completed"); + }); + it("When I fill in both items repeating blocks, Then after going back to the hub the section should be completed.", async () => { + await fillInListCollectorSection(); + await click(HubPage.submit()); + await $(ResponsiblePartyQuestionPage.yes()).click(); + await click(ResponsiblePartyQuestionPage.submit()); + await click(ListCollectorContentPage.submit()); + await completeRepeatingBlocks(123, 1, 1, 1990, true, true); + await click(ListCollectorContentPage.submit()); + await completeRepeatingBlocks(456, 1, 1, 1990, true, true); + await click(ListCollectorContentPage.submit()); + await $(ListCollectorContentSectionSummaryPage.previous()).click(); + await $(ListCollectorContentPage.previous()).click(); + await $(ResponsiblePartyQuestionPage.previous()).click(); + await expect(await $(HubPage.summaryRowState("section-list-collector-contents")).getText()).toBe("Completed"); + }); + it("When I complete both sections then add another item, The list collector content block reverts to in progress and the new repeating blocks need completing", async () => { + await completeBothSections(); + await expect(await $(HubPage.summaryRowState("section-list-collector-contents")).getText()).toBe("Completed"); + await $(HubPage.summaryRowLink("section-companies")).click(); + await verifyUrlContains(CompaniesSummaryPage.pageName); + await $(CompaniesSummaryPage.companiesListAddLink()).click(); + await addCompany("Company C", "789", false); + await anyMoreCompaniesNo(); + await $(ConfirmationCheckboxPage.yes()).click(); + await click(ConfirmationCheckboxPage.submit()); + await click(CompaniesSummaryPage.submit()); + await expect(await $(HubPage.summaryRowState("section-list-collector-contents")).getText()).toBe("Partially completed"); + await click(HubPage.submit()); + await verifyUrlContains(ListCollectorContentPage.pageName); + await listItemComplete(`li[data-qa="list-item-1-label"]`, true); + await listItemComplete(`li[data-qa="list-item-2-label"]`, true); + await listItemComplete(`li[data-qa="list-item-3-label"]`, false); + await click(ListCollectorContentPage.submit()); + await verifyUrlContains(ListCollectorFirstRepeatingBlockPage.pageName); + await completeRepeatingBlocks(666, 2, 5, 1995, true, true); + await listItemComplete(`li[data-qa="list-item-3-label"]`, true); + await click(ListCollectorContentPage.submit()); + await click(ListCollectorContentSectionSummaryPage.submit()); + await expect(await $(HubPage.summaryRowState("section-list-collector-contents")).getText()).toBe("Completed"); + }); + // :TODO: Currently, this is expected behaviour, if list collector content blocks no longer need revisiting after removing items, this test needs updating. + it("When I complete both sections then remove a list item item, Then the list collector content block reverts to in progress the list summary is revisited", async () => { + await completeBothSections(); + await expect(await $(HubPage.summaryRowState("section-list-collector-contents")).getText()).toBe("Completed"); + await $(HubPage.summaryRowLink("section-companies")).click(); + await $(CompaniesSummaryPage.companiesListRemoveLink(1)).click(); + await $(AnyCompaniesOrBranchesRemovePage.yes()).click(); + await click(AnyCompaniesOrBranchesRemovePage.submit()); + await click(CompaniesSummaryPage.submit()); + await expect(await $(HubPage.summaryRowState("section-list-collector-contents")).getText()).toBe("Partially completed"); + await click(HubPage.submit()); + await listItemComplete(`li[data-qa="list-item-1-label"]`, true); + await click(ListCollectorContentPage.submit()); + await verifyUrlContains(ListCollectorContentSectionSummaryPage.pageName); + await click(ListCollectorContentSectionSummaryPage.submit()); + await expect(await $(HubPage.summaryRowState("section-list-collector-contents")).getText()).toBe("Completed"); + }); + }); +}); +const fillInListCollectorSection = async () => { + await $(AnyCompaniesOrBranchesPage.yes()).click(); + await click(AnyCompaniesOrBranchesPage.submit()); + await addCompany("Company A", "123", true); + await anyMoreCompaniesYes(); + await addCompany("Company B", "456", true); + await anyMoreCompaniesNo(); + await click(CompaniesSummaryPage.submit()); +}; + +const completeBothSections = async () => { + await fillInListCollectorSection(); + await click(HubPage.submit()); + await $(ResponsiblePartyQuestionPage.yes()).click(); + await click(ResponsiblePartyQuestionPage.submit()); + await click(ListCollectorContentPage.submit()); + await completeRepeatingBlocks(654, 2, 6, 1999, true, true); + await click(ListCollectorContentPage.submit()); + await completeRepeatingBlocks(655, 12, 1, 1989, true, false); + await click(ListCollectorContentPage.submit()); + await click(ListCollectorContentSectionSummaryPage.submit()); +}; + +const completeRepeatingBlocks = async (registrationNumber, day, month, year, authorisedUk, authorisedEu) => { + await $(ListCollectorFirstRepeatingBlockPage.registrationNumberRepeatingBlock()).setValue(registrationNumber); + await $(ListCollectorFirstRepeatingBlockPage.registrationDateRepeatingBlockday()).setValue(day); + await $(ListCollectorFirstRepeatingBlockPage.registrationDateRepeatingBlockmonth()).setValue(month); + await $(ListCollectorFirstRepeatingBlockPage.registrationDateRepeatingBlockyear()).setValue(year); + await click(ListCollectorFirstRepeatingBlockPage.submit()); + if (authorisedUk) { + await $(ListCollectorSecondRepeatingBlockPage.authorisedTraderUkRadioRepeatingBlockYes()).click(); + } else { + await $(ListCollectorSecondRepeatingBlockPage.authorisedTraderUkRadioRepeatingBlockNo()).click(); + } + if (authorisedEu) { + await $(ListCollectorSecondRepeatingBlockPage.authorisedTraderEuRadioRepeatingBlockYes()).click(); + } else { + await $(ListCollectorSecondRepeatingBlockPage.authorisedTraderEuRadioRepeatingBlockNo()).click(); + } + await click(ListCollectorSecondRepeatingBlockPage.submit()); +}; +const addCompany = async (name, number, authorised) => { + await $(AnyCompaniesOrBranchesAddPage.companyOrBranchName()).setValue(name); + await $(AnyCompaniesOrBranchesAddPage.registrationNumber()).setValue(number); + if (authorised) { + await $(AnyCompaniesOrBranchesAddPage.authorisedInsurerRadioYes()).click(); + } else { + await $(AnyCompaniesOrBranchesAddPage.authorisedInsurerRadioNo()).click(); + } + await click(AnyCompaniesOrBranchesAddPage.submit()); +}; + +const anyMoreCompaniesYes = async () => { + await $(AnyOtherCompaniesOrBranchesPage.yes()).click(); + await click(AnyOtherCompaniesOrBranchesPage.submit()); +}; + +const anyMoreCompaniesNo = async () => { + await $(AnyOtherCompaniesOrBranchesPage.no()).click(); + await click(AnyOtherCompaniesOrBranchesPage.submit()); +}; diff --git a/tests/functional/spec/list_collector/list_collector_driving_question.spec.js b/tests/functional/spec/list_collector/list_collector_driving_question.spec.js new file mode 100644 index 0000000000..4d7d76a758 --- /dev/null +++ b/tests/functional/spec/list_collector/list_collector_driving_question.spec.js @@ -0,0 +1,61 @@ +import { checkItemsInList, click, verifyUrlContains } from "../../helpers"; +import HubPage from "../../base_pages/hub.page.js"; +import AnyoneUsuallyLiveAtPage from "../../generated_pages/list_collector_driving_question/anyone-usually-live-at.page.js"; +import AnyoneElseLiveAtListCollectorPage from "../../generated_pages/list_collector_driving_question/anyone-else-live-at.page.js"; +import AnyoneElseLiveAtListCollectorAddPage from "../../generated_pages/list_collector_driving_question/anyone-else-live-at-add.page.js"; +import AnyoneElseLiveAtListCollectorRemovePage from "../../generated_pages/list_collector_driving_question/anyone-else-live-at-remove.page.js"; +import SectionSummaryPage from "../../generated_pages/list_collector_driving_question/section-summary.page.js"; + +describe("List Collector Driving Question", () => { + beforeEach("Load the survey", async () => { + await browser.openQuestionnaire("test_list_collector_driving_question.json"); + await click(HubPage.submit()); + }); + + describe("Given a happy journey through the list collector", () => { + it("The collector shows all of the household members in the summary", async () => { + await $(AnyoneUsuallyLiveAtPage.yes()).click(); + await click(AnyoneUsuallyLiveAtPage.submit()); + await $(AnyoneElseLiveAtListCollectorAddPage.firstName()).setValue("Marcus"); + await $(AnyoneElseLiveAtListCollectorAddPage.lastName()).setValue("Twin"); + await click(AnyoneElseLiveAtListCollectorAddPage.submit()); + await $(AnyoneElseLiveAtListCollectorPage.yes()).click(); + await click(AnyoneElseLiveAtListCollectorPage.submit()); + await $(AnyoneElseLiveAtListCollectorAddPage.firstName()).setValue("Suzy"); + await $(AnyoneElseLiveAtListCollectorAddPage.lastName()).setValue("Clemens"); + await click(AnyoneElseLiveAtListCollectorAddPage.submit()); + await $(AnyoneElseLiveAtListCollectorPage.no()).click(); + await click(AnyoneElseLiveAtListCollectorPage.submit()); + + const peopleExpected = ["Marcus Twin", "Suzy Clemens"]; + + await checkItemsInList(peopleExpected, SectionSummaryPage.peopleListLabel); + }); + }); + + describe("Given the user answers no to the driving question", () => { + it("The summary add link returns to the driving question", async () => { + await $(AnyoneUsuallyLiveAtPage.no()).click(); + await click(AnyoneUsuallyLiveAtPage.submit()); + await $(SectionSummaryPage.peopleListAddLink()).click(); + await verifyUrlContains(AnyoneUsuallyLiveAtPage.url()); + }); + }); + + describe("Given the user answers yes to the driving question, adds someone and later removes them", () => { + it("The summary add link should return to the original list collector", async () => { + await $(AnyoneUsuallyLiveAtPage.yes()).click(); + await click(AnyoneUsuallyLiveAtPage.submit()); + await $(AnyoneElseLiveAtListCollectorAddPage.firstName()).setValue("Marcus"); + await $(AnyoneElseLiveAtListCollectorAddPage.lastName()).setValue("Twin"); + await click(AnyoneElseLiveAtListCollectorAddPage.submit()); + await $(AnyoneElseLiveAtListCollectorPage.no()).click(); + await click(AnyoneElseLiveAtListCollectorPage.submit()); + await $(SectionSummaryPage.peopleListRemoveLink(1)).click(); + await $(AnyoneElseLiveAtListCollectorRemovePage.yes()).click(); + await click(AnyoneElseLiveAtListCollectorRemovePage.submit()); + await $(SectionSummaryPage.peopleListAddLink()).click(); + await verifyUrlContains(AnyoneElseLiveAtListCollectorAddPage.pageName); + }); + }); +}); diff --git a/tests/functional/spec/list_collector/list_collector_driving_question_checkbox.spec.js b/tests/functional/spec/list_collector/list_collector_driving_question_checkbox.spec.js new file mode 100644 index 0000000000..647ad20daa --- /dev/null +++ b/tests/functional/spec/list_collector/list_collector_driving_question_checkbox.spec.js @@ -0,0 +1,107 @@ +import { checkItemsInList, click } from "../../helpers"; +import HubPage from "../../base_pages/hub.page.js"; +import PrimaryPersonListCollectorPage from "../../generated_pages/list_collector_driving_checkbox/primary-person-list-collector.page.js"; +import PrimaryPersonListCollectorAddPage from "../../generated_pages/list_collector_driving_checkbox/primary-person-list-collector-add.page.js"; +import AnyoneUsuallyLiveAtPage from "../../generated_pages/list_collector_driving_checkbox/anyone-usually-live-at.page.js"; +import ListCollectorAddPage from "../../generated_pages/list_collector_driving_checkbox/list-collector-add.page.js"; +import ListCollectorPage from "../../generated_pages/list_collector_driving_checkbox/list-collector.page.js"; +import ListCollectorTemporaryAwayPage from "../../generated_pages/list_collector_driving_checkbox/list-collector-temporary-away-stay.page"; +import ListCollectorTemporaryAwayAddPage from "../../generated_pages/list_collector_driving_checkbox/list-collector-temporary-away-stay-add.page"; +import SummaryPage from "../../generated_pages/list_collector_driving_checkbox/section-summary.page"; + +const beforeSetup = async () => { + await browser.openQuestionnaire("test_list_collector_driving_checkbox.json"); + await click(HubPage.submit()); +}; + +describe("List Collector Driving Checkbox Question", () => { + before("Load the survey", beforeSetup); + + describe("Given a happy journey through the list collectors", () => { + it("All of the household members and visitors are shown in the summary", async () => { + await $(PrimaryPersonListCollectorPage.yesIUsuallyLiveHere()).click(); + await click(PrimaryPersonListCollectorPage.submit()); + await $(PrimaryPersonListCollectorAddPage.firstName()).setValue("Marcus"); + await $(PrimaryPersonListCollectorAddPage.lastName()).setValue("Twin"); + await click(PrimaryPersonListCollectorAddPage.submit()); + await $(AnyoneUsuallyLiveAtPage.familyMembersAndPartners()).click(); + await click(AnyoneUsuallyLiveAtPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Suzy"); + await $(ListCollectorAddPage.lastName()).setValue("Clemens"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.noIDoNotNeedToAddAPerson()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorTemporaryAwayPage.noThereAreNumberOfPeoplePeopleLivingHere()).click(); + await click(ListCollectorTemporaryAwayPage.submit()); + + const householdMembersExpected = ["Marcus Twin (You)", "Suzy Clemens"]; + await checkItemsInList(householdMembersExpected, SummaryPage.peopleListLabel); + }); + }); + + describe("Given the primary person is removed", () => { + it("Then they aren't shown on the summary screen", async () => { + await $(SummaryPage.previous()).click(); + await $(ListCollectorTemporaryAwayPage.previous()).click(); + await $(ListCollectorPage.previous()).click(); + await $(AnyoneUsuallyLiveAtPage.previous()).click(); + await $(PrimaryPersonListCollectorPage.noIDonTUsuallyLiveHere()).click(); + await click(PrimaryPersonListCollectorPage.submit()); + + const householdMembersExpected = ["Suzy Clemens"]; + await checkItemsInList(householdMembersExpected, SummaryPage.peopleListLabel); + }); + }); + + describe("Given the user chooses yes from the second list collector", () => { + it("Then they are taken to the correct list add screen", async () => { + await $(SummaryPage.previous()).click(); + await $(ListCollectorTemporaryAwayPage.yesINeedToAddSomeone()).click(); + await click(ListCollectorTemporaryAwayPage.submit()); + await $(ListCollectorTemporaryAwayAddPage.firstName()).setValue("Christopher"); + await $(ListCollectorTemporaryAwayAddPage.lastName()).setValue("Pike"); + await click(ListCollectorTemporaryAwayAddPage.submit()); + await $(ListCollectorTemporaryAwayPage.noThereAreNumberOfPeoplePeopleLivingHere()).click(); + await click(ListCollectorTemporaryAwayPage.submit()); + + const householdMembersExpected = ["Suzy Clemens", "Christopher Pike"]; + await checkItemsInList(householdMembersExpected, SummaryPage.peopleListLabel); + }); + }); +}); + +describe("Given the user says no one else lives in the house", () => { + before("Load the survey", beforeSetup); + + it("The user is asked if they need to add anyone that is temporarily away", async () => { + await $(PrimaryPersonListCollectorPage.yesIUsuallyLiveHere()).click(); + await click(PrimaryPersonListCollectorPage.submit()); + await $(PrimaryPersonListCollectorAddPage.firstName()).setValue("Marcus"); + await $(PrimaryPersonListCollectorAddPage.lastName()).setValue("Twin"); + await click(PrimaryPersonListCollectorAddPage.submit()); + await $(AnyoneUsuallyLiveAtPage.exclusiveNoneOfTheseApplyNoOneUsuallyLivesHere()).click(); + await click(AnyoneUsuallyLiveAtPage.submit()); + + await expect(await $(ListCollectorTemporaryAwayPage.questionText()).getText()).toBe( + "You said 1 person lives at 12 Lovely Villas. Do you need to add anyone?", + ); + }); +}); + +describe("Given a person does not live in the house", () => { + before("Load the survey", beforeSetup); + it("The user is asked whether they live there", async () => { + await $(PrimaryPersonListCollectorPage.noIDonTUsuallyLiveHere()).click(); + await click(PrimaryPersonListCollectorPage.submit()); + await expect(await $(AnyoneUsuallyLiveAtPage.questionText()).getText()).toBe("Do any of the following usually live at 12 Lovely Villas on 21 March?"); + + await $(AnyoneUsuallyLiveAtPage.exclusiveNoneOfTheseApplyNoOneUsuallyLivesHere()).click(); + await click(AnyoneUsuallyLiveAtPage.submit()); + await expect(await $(ListCollectorTemporaryAwayPage.questionText()).getText()).toBe( + "You said 0 people lives at 12 Lovely Villas. Do you need to add anyone?", + ); + + await $(ListCollectorTemporaryAwayPage.noThereAreNumberOfPeoplePeopleLivingHere()).click(); + await click(AnyoneUsuallyLiveAtPage.submit()); + }); +}); diff --git a/tests/functional/spec/list_collector/list_collector_primary_person.spec.js b/tests/functional/spec/list_collector/list_collector_primary_person.spec.js new file mode 100644 index 0000000000..c71a79ccf7 --- /dev/null +++ b/tests/functional/spec/list_collector/list_collector_primary_person.spec.js @@ -0,0 +1,127 @@ +import ListCollectorPage from "../../generated_pages/list_collector_primary_person/list-collector.page.js"; +import ListCollectorAddPage from "../../generated_pages/list_collector_primary_person/list-collector-add.page.js"; +import ListCollectorEditPage from "../../generated_pages/list_collector_primary_person/list-collector-edit.page.js"; +import PrimaryPersonListCollectorPage from "../../generated_pages/list_collector_primary_person/primary-person-list-collector.page.js"; +import PrimaryPersonListCollectorAddPage from "../../generated_pages/list_collector_primary_person/primary-person-list-collector-add.page.js"; +import SectionSummaryPage from "../../generated_pages/list_collector/section-summary.page.js"; +import { SubmitPage } from "../../base_pages/submit.page.js"; +import ThankYouPage from "../../base_pages/thank-you.page.js"; +import AnyoneUsuallyLiveAtPage from "../../generated_pages/list_collector_primary_person/anyone-usually-live-at.page.js"; +import { click, verifyUrlContains } from "../../helpers"; + +describe("Primary Person List Collector Survey", () => { + describe("Given the user starts on the 'do you live here' question", () => { + before("Load the survey", async () => { + await browser.openQuestionnaire("test_list_collector_primary_person.json"); + }); + + it.skip("When the user says they do not live there, and changes their answer to yes, then the user can't navigate to the list collector", async () => { + await $(PrimaryPersonListCollectorPage.noLabel()).click(); + await click(PrimaryPersonListCollectorPage.submit()); + await $(PrimaryPersonListCollectorAddPage.previous()).click(); + await $(PrimaryPersonListCollectorPage.yesLabel()).click(); + await click(PrimaryPersonListCollectorPage.submit()); + await browser.url("questionnaire/list-collector"); + await expect(await $(PrimaryPersonListCollectorPage.questionText()).getText()).toBe("Do you live here"); + }); + }); + + describe("Given the user starts on the 'do you live here' question", () => { + before("Load the survey", async () => { + await browser.openQuestionnaire("test_list_collector_primary_person.json"); + }); + + it("When the user says that they do live there, then they are shown as the primary person", async () => { + await $(PrimaryPersonListCollectorPage.yesLabel()).click(); + await click(PrimaryPersonListCollectorPage.submit()); + await $(PrimaryPersonListCollectorAddPage.firstName()).setValue("Mark"); + await $(PrimaryPersonListCollectorAddPage.lastName()).setValue("Twin"); + await click(PrimaryPersonListCollectorAddPage.submit()); + await expect(await $(ListCollectorPage.listLabel(1)).getText()).toBe("Mark Twin (You)"); + }); + + it("When the user adds another person, they are shown in the summary", async () => { + await $(ListCollectorPage.yesLabel()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Samuel"); + await $(ListCollectorAddPage.lastName()).setValue("Clemens"); + await click(ListCollectorAddPage.submit()); + await expect(await $(ListCollectorPage.listLabel(2)).getText()).toBe("Samuel Clemens"); + }); + + it("When the user goes back and answers No, the primary person is not shown", async () => { + await $(ListCollectorPage.previous()).click(); + await $(PrimaryPersonListCollectorPage.no()).click(); + await click(PrimaryPersonListCollectorPage.submit()); + await $(AnyoneUsuallyLiveAtPage.no()).click(); + await click(AnyoneUsuallyLiveAtPage.submit()); + await expect(await $(ListCollectorPage.listLabel(1)).getText()).toBe("Samuel Clemens"); + }); + + it("When the user adds the primary person again, then the primary person is first in the list", async () => { + await $(ListCollectorPage.previous()).click(); + await $(AnyoneUsuallyLiveAtPage.previous()).click(); + await $(PrimaryPersonListCollectorPage.yes()).click(); + await click(PrimaryPersonListCollectorPage.submit()); + await $(PrimaryPersonListCollectorAddPage.firstName()).setValue("Mark"); + await $(PrimaryPersonListCollectorAddPage.lastName()).setValue("Twin"); + await click(PrimaryPersonListCollectorAddPage.submit()); + await expect(await $(ListCollectorPage.listLabel(1)).getText()).toBe("Mark Twin (You)"); + }); + + it("When the user views the summary, then it does not show the remove link for the primary person", async () => { + await expect(await $(ListCollectorPage.listRemoveLink(1)).isExisting()).toBe(false); + await expect(await $(ListCollectorPage.listRemoveLink(2)).isExisting()).toBe(true); + }); + + it("When the user changes the primary person's name on the summary, then the name should be updated", async () => { + await $(ListCollectorPage.listEditLink(1)).click(); + await $(ListCollectorEditPage.firstName()).setValue("Mark"); + await $(ListCollectorEditPage.lastName()).setValue("Twain"); + await click(ListCollectorEditPage.submit()); + await expect(await $(ListCollectorPage.listLabel(1)).getText()).toBe("Mark Twain (You)"); + await expect(await $(ListCollectorPage.listLabel(2)).getText()).toBe("Samuel Clemens"); + }); + + it("When the user views the summary, then it does not show the does anyone usually live here question", async () => { + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await expect($("body").getText()).not.toBe("usually live here"); + }); + + it("When the user attempts to submit, then they are shown the confirmation page", async () => { + await click(SectionSummaryPage.submit()); + await expect(await $(SubmitPage.guidance()).getText()).toBe("Thank you for your answers, do you wish to submit"); + }); + + it("When the user submits, then they are allowed to submit the survey", async () => { + await click(SubmitPage.submit()); + await verifyUrlContains(ThankYouPage.pageName); + }); + }); + + describe("Given the user starts on the 'do you live here' question", () => { + before("Load the survey", async () => { + await browser.openQuestionnaire("test_list_collector_primary_person.json"); + }); + + it("When the user says they do not live there, then an empty list is displayed", async () => { + await $(PrimaryPersonListCollectorPage.no()).click(); + await click(PrimaryPersonListCollectorPage.submit()); + await $(AnyoneUsuallyLiveAtPage.no()).click(); + await expect(await $(ListCollectorPage.listLabel(1)).isExisting()).toBe(false); + }); + + it("When the user clicks on the add person button multiple times, then only one person is added", async () => { + await $(ListCollectorPage.previous()).click(); + await $(PrimaryPersonListCollectorPage.yes()).click(); + await click(PrimaryPersonListCollectorPage.submit()); + await $(PrimaryPersonListCollectorAddPage.firstName()).setValue("Mark"); + await $(PrimaryPersonListCollectorAddPage.lastName()).setValue("Twain"); + await click(PrimaryPersonListCollectorPage.submit()); + await click(PrimaryPersonListCollectorPage.submit()); + await expect(await $(ListCollectorPage.listLabel(1)).getText()).toBe("Mark Twain (You)"); + await expect(await $(ListCollectorPage.listLabel(2)).isExisting()).toBe(false); + }); + }); +}); diff --git a/tests/functional/spec/list_collector/list_collector_section_summary.spec.js b/tests/functional/spec/list_collector/list_collector_section_summary.spec.js new file mode 100644 index 0000000000..ab018ca044 --- /dev/null +++ b/tests/functional/spec/list_collector/list_collector_section_summary.spec.js @@ -0,0 +1,359 @@ +import AnyCompaniesOrBranchesDrivingQuestionPage from "../../generated_pages/list_collector_section_summary/any-companies-or-branches.page.js"; +import AnyCompaniesOrBranchesPage from "../../generated_pages/list_collector_section_summary/any-other-companies-or-branches.page.js"; +import AnyCompaniesOrBranchesAddPage from "../../generated_pages/list_collector_section_summary/any-other-companies-or-branches-add.page.js"; +import AnyCompaniesOrBranchesRemovePage from "../../generated_pages/list_collector_section_summary/any-other-companies-or-branches-remove.page.js"; +import SectionSummaryPage from "../../generated_pages/list_collector_section_summary/section-companies-summary.page"; +import SectionSummaryTwoPage from "../../generated_pages/list_collector_section_summary/section-household-summary.page"; +import UkBasedPage from "../../generated_pages/list_collector_section_summary/confirmation-checkbox.page"; +import ListCollectorPage from "../../generated_pages/list_collector_section_summary/list-collector.page"; +import HouseholderCheckboxPage from "../../generated_pages/list_collector_section_summary/householder-checkbox.page"; +import SubmitPage from "../../generated_pages/list_collector_section_summary/submit.page"; +import ThankYouPage from "../../base_pages/thank-you.page"; +import ViewSubmittedResponsePage from "../../generated_pages/list_collector_section_summary/view-submitted-response.page"; +import { click, listItemIds, verifyUrlContains } from "../../helpers"; + +describe("List Collector Section Summary and Summary Items", () => { + describe("Given I launch the test list collector section summary items survey", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_list_collector_section_summary.json"); + }); + it("When I get to the section summary, Then the driving question should be visible.", async () => { + await drivingQuestionYes(); + await addCompany("Company A", "123", true); + await anyMoreCompaniesNo(); + await verifyUrlContains(SectionSummaryPage.url()); + await expect(await $(SectionSummaryPage.anyCompaniesOrBranchesQuestion()).isExisting()).toBe(true); + await expect(await $(SectionSummaryPage.anyCompaniesOrBranchesAnswer()).getText()).toBe("Yes"); + }); + it("When I add my own item, Then the item should be visible on the section summary and have correct values", async () => { + await drivingQuestionYes(); + await addCompany("Company A", "123", true); + await anyMoreCompaniesNo(); + await expect(await $(SectionSummaryPage.companiesListLabel(1)).getText()).toContain("Name of UK company or branch"); + await expect(await $(companiesListRowItem(1, 1)).getText()).toContain("Company A"); + await expect(await $(companiesListRowItem(1, 2)).getText()).toContain("123"); + await expect(await $(companiesListRowItem(1, 3)).getText()).toContain("Yes"); + const listItemId = (await listItemIds())[0]; + await expect(await $(companiesListRowItemAnchor(1)).getHTML()).toContain( + `return_to=section-summary&return_to_answer_id=${listItemId}#company-or-branch-name`, + ); + await expect(await $(companiesListRowItemAnchor(2)).getHTML()).toContain(`return_to_answer_id=registration-number-${listItemId}#registration-number`); + await expect(await $(companiesListRowItemAnchor(3)).getHTML()).toContain( + `return_to_answer_id=authorised-insurer-radio-${listItemId}#authorised-insurer-radio`, + ); + }); + it("When I add multiple items, Then all the items should be visible on the section summary and have correct values", async () => { + await drivingQuestionYes(); + await addCompany("Company A", "123", true); + await anyMoreCompaniesYes(); + await addCompany("Company B", "456", false); + await anyMoreCompaniesYes(); + await addCompany("Company C", "789", true); + await anyMoreCompaniesNo(); + await answerUkBasedQuestion(); + await expect(await $(companiesListRowItem(1, 1)).getText()).toContain("Company A"); + await expect(await $(companiesListRowItem(1, 2)).getText()).toContain("123"); + await expect(await $(companiesListRowItem(1, 3)).getText()).toContain("Yes"); + await expect(await $(companiesListRowItem(2, 1)).getText()).toContain("Company B"); + await expect(await $(companiesListRowItem(2, 2)).getText()).toContain("456"); + await expect(await $(companiesListRowItem(2, 3)).getText()).toContain("No"); + await expect(await $(companiesListRowItem(3, 1)).getText()).toContain("Company C"); + await expect(await $(companiesListRowItem(3, 2)).getText()).toContain("789"); + await expect(await $(companiesListRowItem(3, 3)).getText()).toContain("Yes"); + }); + it("When I remove an item, Then the list of answers should no longer be visible on the section summary.", async () => { + await drivingQuestionYes(); + await addCompany("Company A", "123", true); + await anyMoreCompaniesNo(); + await removeFirstCompany(); + await verifyUrlContains(SectionSummaryPage.url()); + await expect(await $("body").getText()).not.toBe("Company A"); + await expect(await $(SectionSummaryPage.companiesListEditLink(1)).isExisting()).toBe(false); + await expect(await $(SectionSummaryPage.companiesListRemoveLink(1)).isExisting()).toBe(false); + }); + it("When I remove an item but the list collector is still on the path, Then the placeholder text should be visible on the section summary.", async () => { + await drivingQuestionYes(); + await addCompany("Company A", "123", true); + await anyMoreCompaniesNo(); + await removeFirstCompany(); + await verifyUrlContains(SectionSummaryPage.url()); + await expect(await $("body").getText()).toContain("No UK company or branch added"); + }); + it("When I have multiple items in the list and I remove the first item, Then only the item that was not deleted should be visible on the section summary.", async () => { + await drivingQuestionYes(); + await addCompany("Company A", "123", true); + await anyMoreCompaniesYes(); + await addCompany("Company B", "234", true); + await anyMoreCompaniesNo(); + await removeFirstCompany(); + await verifyUrlContains(SectionSummaryPage.url()); + await expect(await $("body").getText()).not.toBe("Company A"); + await expect(await $("body").getText()).toContain("Company B"); + }); + it("When I add an item and relevant data and answer No on the additional items page, Then I should get to the section summary page.", async () => { + await drivingQuestionYes(); + await addCompany("Company A", "123", true); + await anyMoreCompaniesNo(); + await verifyUrlContains(SectionSummaryPage.url()); + await expect(await $(SectionSummaryPage.companiesListAddLink()).isExisting()).toBe(true); + }); + it("When I add an item and relevant data and answer Yes on the additional items page, Then I should be able to and add a new item and relevant data.", async () => { + await drivingQuestionYes(); + await addCompany("Company A", "123", true); + await anyMoreCompaniesYes(); + await expect(await $(AnyCompaniesOrBranchesAddPage.companyOrBranchName()).isExisting()).toBe(true); + await expect(await $(AnyCompaniesOrBranchesAddPage.registrationNumber()).isExisting()).toBe(true); + await expect(await $(AnyCompaniesOrBranchesAddPage.authorisedInsurerRadioYes()).isExisting()).toBe(true); + await expect(await $(AnyCompaniesOrBranchesAddPage.heading()).getText()).toBe( + "Give details about the company or branch that undertakes general insurance business", + ); + }); + it("When I add an item and relevant data, Then I should be able to edit that item from the section summary page.", async () => { + await drivingQuestionYes(); + await addCompany("Company A", "123", true); + await anyMoreCompaniesNo(); + await expect(await $(companiesListRowItem(1, 1)).getText()).toContain("Company A"); + await $(SectionSummaryPage.companiesListEditLink(1)).click(); + await verifyUrlContains("edit-company/?return_to=section-summary"); + await expect(await $(AnyCompaniesOrBranchesAddPage.companyOrBranchName()).getValue()).toBe("Company A"); + }); + it("When I edit an item after adding it, Then I should be redirected to the summary page", async () => { + await drivingQuestionYes(); + await addCompany("Company A", "123", true); + await anyMoreCompaniesNo(); + await expect(await $(companiesListRowItem(1, 1)).getText()).toContain("Company A"); + await $(SectionSummaryPage.companiesListEditLink(1)).click(); + await $(AnyCompaniesOrBranchesAddPage.companyOrBranchName()).setValue("Changed Company"); + await click(AnyCompaniesOrBranchesAddPage.submit()); + await verifyUrlContains(SectionSummaryPage.url()); + await expect(await $(companiesListRowItem(1, 1)).getText()).toContain("Changed Company"); + }); + it("When no item is added but I change my answer to the driving question to Yes, Then I should be able to add a new item.", async () => { + await drivingQuestionNo(); + await verifyUrlContains(SectionSummaryPage.url()); + await expect(await $(SectionSummaryPage.companiesListEditLink(1)).isExisting()).toBe(false); + await expect(await $(SectionSummaryPage.companiesListRemoveLink(1)).isExisting()).toBe(false); + await expect(await $(SectionSummaryPage.companiesListAddLink()).isExisting()).toBe(false); + await $(SectionSummaryPage.anyCompaniesOrBranchesAnswerEdit()).click(); + await drivingQuestionYes(); + await addCompany("Company A", "123", true); + await anyMoreCompaniesNo(); + await verifyUrlContains(SectionSummaryPage.url()); + await expect(await $(SectionSummaryPage.companiesListEditLink(1)).isExisting()).toBe(true); + await expect(await $(SectionSummaryPage.companiesListRemoveLink(1)).isExisting()).toBe(true); + await expect(await $(SectionSummaryPage.companiesListAddLink()).isExisting()).toBe(true); + }); + it("When I add an item and relevant data but change my answer to the driving question to No, Then I should see the original item on the summary if change the answer back to Yes.", async () => { + await drivingQuestionYes(); + await addCompany("Company A", "123", true); + await anyMoreCompaniesNo(); + await expect(await $(companiesListRowItem(1, 1)).getText()).toContain("Company A"); + await $(SectionSummaryPage.anyCompaniesOrBranchesAnswerEdit()).click(); + await drivingQuestionNo(); + await verifyUrlContains(SectionSummaryPage.url()); + await expect(await $(SectionSummaryPage.companiesListEditLink(1)).isExisting()).toBe(false); + await expect(await $(SectionSummaryPage.companiesListRemoveLink(1)).isExisting()).toBe(false); + await expect(await $("body").getText()).not.toBe("No UK company or branch added"); + await expect(await $(SectionSummaryPage.companiesListAddLink()).isExisting()).toBe(false); + await $(SectionSummaryPage.anyCompaniesOrBranchesAnswerEdit()).click(); + await drivingQuestionYes(); + await verifyUrlContains(SectionSummaryPage.url()); + await expect(await $(companiesListRowItem(1, 1)).getText()).toContain("Company A"); + await expect(await $(SectionSummaryPage.companiesListEditLink(1)).isExisting()).toBe(true); + await expect(await $(SectionSummaryPage.companiesListRemoveLink(1)).isExisting()).toBe(true); + await expect(await $(SectionSummaryPage.companiesListAddLink()).isExisting()).toBe(true); + }); + it("When I add another company from the summary page, Then I am asked if I want to add any more company before accessing the section summary", async () => { + await drivingQuestionYes(); + await addCompany("Company A", "123", true); + await anyMoreCompaniesNo(); + await $(SectionSummaryPage.companiesListAddLink()).click(); + await verifyUrlContains("/questionnaire/companies/add-company"); + await verifyUrlContains("?return_to=section-summary"); + await addCompany("Company B", "456", true); + await verifyUrlContains(AnyCompaniesOrBranchesPage.url()); + await expect(await $("body").getText()).toContain("Company A"); + await expect(await $("body").getText()).toContain("Company B"); + await anyMoreCompaniesNo(); + await verifyUrlContains(SectionSummaryPage.url()); + }); + it("When I add three companies, Then I am prompted with the confirmation question", async () => { + await drivingQuestionYes(); + await addCompany("Company A", "123", true); + await anyMoreCompaniesYes(); + await addCompany("Company B", "456", true); + await anyMoreCompaniesYes(); + await addCompany("Company C", "789", true); + await anyMoreCompaniesNo(); + await verifyUrlContains(UkBasedPage.url()); + }); + it("When I add less than 3 companies, Then I am not prompted with the confirmation question", async () => { + await drivingQuestionYes(); + await addCompany("Company A", "123", true); + await anyMoreCompaniesYes(); + await addCompany("Company B", "456", true); + await anyMoreCompaniesNo(); + await verifyUrlContains(SectionSummaryPage.url()); + }); + it("When I add more than 3 companies, Then I am not prompted with the confirmation question", async () => { + await drivingQuestionYes(); + await addCompany("Company A", "123", true); + await anyMoreCompaniesYes(); + await addCompany("Company B", "456", true); + await anyMoreCompaniesYes(); + await addCompany("Company C", "789", true); + await anyMoreCompaniesYes(); + await addCompany("Company D", "135", true); + await anyMoreCompaniesNo(); + await verifyUrlContains(SectionSummaryPage.url()); + }); + it("When I add another company from the summary page, and the amount then totals to 3, and the confirmation question hasn't been previously answered, Then I am prompted with the confirmation question", async () => { + await drivingQuestionYes(); + await addCompany("Company A", "123", true); + await anyMoreCompaniesYes(); + await addCompany("Company B", "456", true); + await anyMoreCompaniesNo(); + await verifyUrlContains(SectionSummaryPage.url()); + await $(SectionSummaryPage.companiesListAddLink()).click(); + await verifyUrlContains("/questionnaire/companies/add-company"); + await verifyUrlContains("?return_to=section-summary"); + await addCompany("Company C", "234", true); + await anyMoreCompaniesNo(); + await verifyUrlContains(UkBasedPage.url()); + await answerUkBasedQuestion(); + await verifyUrlContains(SectionSummaryPage.url()); + }); + it("When I remove a company from the summary page, and the amount then totals to 3, and the confirmation question hasn't been previously answered, Then I am prompted with the confirmation question", async () => { + await drivingQuestionYes(); + await addCompany("Company A", "123", true); + await anyMoreCompaniesYes(); + await addCompany("Company B", "456", true); + await anyMoreCompaniesYes(); + await addCompany("Company C", "234", true); + await anyMoreCompaniesYes(); + await addCompany("Company D", "345", true); + await anyMoreCompaniesNo(); + await verifyUrlContains(SectionSummaryPage.url()); + await removeFirstCompany(); + await verifyUrlContains(UkBasedPage.url()); + await answerUkBasedQuestion(); + await verifyUrlContains(SectionSummaryPage.url()); + }); + + it("When I get to the summary page, Then the summary should be displayed as expected with change links", async () => { + await drivingQuestionYes(); + await addCompany("Company A", "123", true); + await anyMoreCompaniesYes(); + await addCompany("Company B", "456", true); + await anyMoreCompaniesYes(); + await addCompany("Company C", "234", true); + await anyMoreCompaniesNo(); + await verifyUrlContains(UkBasedPage.url()); + await answerUkBasedQuestion(); + await verifyUrlContains(SectionSummaryPage.url()); + await click(SectionSummaryPage.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await $(HouseholderCheckboxPage.no()).click(); + await click(HouseholderCheckboxPage.submit()); + await click(SectionSummaryTwoPage.submit()); + + await verifyUrlContains(SubmitPage.url()); + await expect(await $(companiesListRowItem(1, 1)).getText()).toContain("Company A"); + await expect(await $(companiesListRowItem(1, 2)).getText()).toContain("123"); + await expect(await $(companiesListRowItem(1, 3)).getText()).toContain("Change"); + await expect(await $(companiesListRowItem(2, 1)).getText()).toContain("Company B"); + await expect(await $(companiesListRowItem(2, 2)).getText()).toContain("456"); + await expect(await $(companiesListRowItem(2, 3)).getText()).toContain("Change"); + await expect(await $(companiesListRowItem(3, 1)).getText()).toContain("Company C"); + await expect(await $(companiesListRowItem(3, 2)).getText()).toContain("234"); + await expect(await $(companiesListRowItem(3, 3)).getText()).toContain("Change"); + await expect(await $(SubmitPage.householderCheckboxAnswer()).getText()).toContain("No"); + await expect(await $("body").getHTML()).toContain("Add another UK company or branch"); + await expect(await $("body").getHTML()).toContain("Remove"); + }); + + it("When I get to the view submitted response page, Then the summary should be displayed as expected without any change or remove links", async () => { + await drivingQuestionYes(); + await addCompany("Company A", "123", true); + await anyMoreCompaniesYes(); + await addCompany("Company B", "456", true); + await anyMoreCompaniesYes(); + await addCompany("Company C", "234", true); + await anyMoreCompaniesNo(); + await verifyUrlContains(UkBasedPage.url()); + await answerUkBasedQuestion(); + await verifyUrlContains(SectionSummaryPage.url()); + await click(SectionSummaryPage.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await $(HouseholderCheckboxPage.no()).click(); + await click(HouseholderCheckboxPage.submit()); + await click(SectionSummaryTwoPage.submit()); + await click(SubmitPage.submit()); + await expect(await $(ThankYouPage.title()).getHTML()).toContain("Thank you for completing the Test"); + await $(ThankYouPage.savePrintAnswersLink()).click(); + + await verifyUrlContains(ViewSubmittedResponsePage.pageName); + await expect(await $(companiesListRowItem(1, 1)).getText()).toContain("Company A"); + await expect(await $(companiesListRowItem(1, 2)).getText()).toContain("123"); + await expect(await $(companiesListRowItem(2, 1)).getText()).toContain("Company B"); + await expect(await $(companiesListRowItem(2, 2)).getText()).toContain("456"); + await expect(await $(companiesListRowItem(3, 1)).getText()).toContain("Company C"); + await expect(await $(companiesListRowItem(3, 2)).getText()).toContain("234"); + await expect(await $("body").getHTML()).not.toContain("Change"); + await expect(await $("body").getHTML()).not.toContain("Remove"); + await expect(await $("body").getHTML()).not.toContain("Add another UK company or branch"); + }); + }); +}); + +const drivingQuestionYes = async () => { + await $(AnyCompaniesOrBranchesDrivingQuestionPage.yes()).click(); + await click(AnyCompaniesOrBranchesDrivingQuestionPage.submit()); +}; + +const drivingQuestionNo = async () => { + await $(AnyCompaniesOrBranchesDrivingQuestionPage.no()).click(); + await click(AnyCompaniesOrBranchesDrivingQuestionPage.submit()); +}; + +const addCompany = async (name, number, authorised) => { + await $(AnyCompaniesOrBranchesAddPage.companyOrBranchName()).setValue(name); + await $(AnyCompaniesOrBranchesAddPage.registrationNumber()).setValue(number); + if (authorised) { + await $(AnyCompaniesOrBranchesAddPage.authorisedInsurerRadioYes()).click(); + } else { + await $(AnyCompaniesOrBranchesAddPage.authorisedInsurerRadioNo()).click(); + } + await click(AnyCompaniesOrBranchesAddPage.submit()); +}; + +const anyMoreCompaniesYes = async () => { + await $(AnyCompaniesOrBranchesPage.yes()).click(); + await click(AnyCompaniesOrBranchesPage.submit()); +}; + +const anyMoreCompaniesNo = async () => { + await $(AnyCompaniesOrBranchesPage.no()).click(); + await click(AnyCompaniesOrBranchesPage.submit()); +}; + +const removeFirstCompany = async () => { + await $(SectionSummaryPage.companiesListRemoveLink(1)).click(); + await $(AnyCompaniesOrBranchesRemovePage.yes()).click(); + await click(AnyCompaniesOrBranchesRemovePage.submit()); +}; + +const answerUkBasedQuestion = async () => { + await $(UkBasedPage.yes()).click(); + await click(UkBasedPage.submit()); +}; + +const companiesListRowItem = (row, index) => { + return `#group-companies-1 .ons-summary__items .ons-summary__item:nth-of-type(${row}) .ons-summary__row:nth-of-type(${index})`; +}; + +const companiesListRowItemAnchor = (index) => { + return `#group-companies-1 .ons-summary__items .ons-summary__item .ons-summary__row:nth-of-type(${index}) a`; +}; diff --git a/tests/functional/spec/list_collector/list_collector_variants.spec.js b/tests/functional/spec/list_collector/list_collector_variants.spec.js new file mode 100644 index 0000000000..f11fabed05 --- /dev/null +++ b/tests/functional/spec/list_collector/list_collector_variants.spec.js @@ -0,0 +1,102 @@ +import { checkItemsInList, click, verifyUrlContains } from "../../helpers"; +import YouLiveHerePage from "../../generated_pages/list_collector_variants/you-live-here-block.page.js"; +import ListCollectorPage from "../../generated_pages/list_collector_variants/list-collector.page.js"; +import ListCollectorAddPage from "../../generated_pages/list_collector_variants/list-collector-add.page.js"; +import ListCollectorEditPage from "../../generated_pages/list_collector_variants/list-collector-edit.page.js"; +import ListCollectorRemovePage from "../../generated_pages/list_collector_variants/list-collector-remove.page.js"; +import { SubmitPage } from "../../base_pages/submit.page.js"; +import ThankYouPage from "../../base_pages/thank-you.page.js"; + +describe("List Collector With Variants", () => { + describe("Given that a person lives in house", () => { + before("Load the survey", async () => { + await browser.openQuestionnaire("test_list_collector_variants.json"); + }); + + it("The user is asked questions about whether they live there", async () => { + await $(YouLiveHerePage.yes()).click(); + await click(YouLiveHerePage.submit()); + await expect(await $(ListCollectorPage.questionText()).getText()).toBe("Does anyone else live at 1 Pleasant Lane?"); + }); + + it("The user is able to add members of the household", async () => { + await $(ListCollectorPage.anyoneElseYes()).click(); + await click(ListCollectorPage.submit()); + await expect(await $(ListCollectorAddPage.questionText()).getText()).toBe("What is the name of the person?"); + await $(ListCollectorAddPage.firstName()).setValue("Samuel"); + await $(ListCollectorAddPage.lastName()).setValue("Clemens"); + await click(ListCollectorAddPage.submit()); + }); + + it("The user can see all household members in the summary", async () => { + const peopleExpected = ["Samuel Clemens"]; + await checkItemsInList(peopleExpected, ListCollectorPage.listLabel); + }); + + it("The questionnaire has the correct question text on the change and remove pages", async () => { + await $(ListCollectorPage.listEditLink(1)).click(); + await expect(await $(ListCollectorEditPage.questionText()).getText()).toBe("What is the name of the person?"); + await $(ListCollectorEditPage.previous()).click(); + await $(ListCollectorPage.listRemoveLink(1)).click(); + await expect(await $(ListCollectorRemovePage.questionText()).getText()).toBe("Are you sure you want to remove this person?"); + await $(ListCollectorRemovePage.previous()).click(); + }); + + it("The questionnaire shows the confirmation page when no more people to add", async () => { + await $(ListCollectorPage.anyoneElseNo()).click(); + await click(ListCollectorPage.submit()); + await verifyUrlContains(SubmitPage.url()); + }); + + it("The questionnaire allows submission", async () => { + await click(SubmitPage.submit()); + await verifyUrlContains("thank-you"); + }); + }); + + describe("Given a person does not live in house", () => { + before("Load the survey", async () => { + await browser.openQuestionnaire("test_list_collector_variants.json"); + }); + + it("The user is asked questions about whether they live there", async () => { + await $(YouLiveHerePage.no()).click(); + await click(YouLiveHerePage.submit()); + await expect(await $(ListCollectorPage.questionText()).getText()).toBe("Does anyone live at 1 Pleasant Lane?"); + }); + + it("The user is able to add members of the household", async () => { + await $(ListCollectorPage.anyoneElseYes()).click(); + await click(ListCollectorPage.submit()); + await expect(await $(ListCollectorAddPage.questionText()).getText()).toBe("What is the name of the person who isn’t you?"); + await $(ListCollectorAddPage.firstName()).setValue("Samuel"); + await $(ListCollectorAddPage.lastName()).setValue("Clemens"); + await click(ListCollectorAddPage.submit()); + }); + + it("The user can see all household members in the summary", async () => { + const peopleExpected = ["Samuel Clemens"]; + await checkItemsInList(peopleExpected, ListCollectorPage.listLabel); + }); + + it("The questionnaire has the correct question text on the change and remove pages", async () => { + await $(ListCollectorPage.listEditLink(1)).click(); + await expect(await $(ListCollectorEditPage.questionText()).getText()).toBe("What is the name of the person who isn’t you?"); + await $(ListCollectorEditPage.previous()).click(); + await $(ListCollectorPage.listRemoveLink(1)).click(); + await expect(await $(ListCollectorRemovePage.questionText()).getText()).toBe("Are you sure you want to remove this person who isn’t you?"); + await $(ListCollectorRemovePage.previous()).click(); + }); + + it("The questionnaire shows the confirmation page when no more people to add", async () => { + await $(ListCollectorPage.anyoneElseNo()).click(); + await click(ListCollectorPage.submit()); + await verifyUrlContains(SubmitPage.url()); + }); + + it("The questionnaire allows submission", async () => { + await click(SubmitPage.submit()); + await verifyUrlContains(ThankYouPage.url()); + }); + }); +}); diff --git a/tests/functional/spec/list_collector/list_collector_variants_primary_person.spec.js b/tests/functional/spec/list_collector/list_collector_variants_primary_person.spec.js new file mode 100644 index 0000000000..957f54592b --- /dev/null +++ b/tests/functional/spec/list_collector/list_collector_variants_primary_person.spec.js @@ -0,0 +1,122 @@ +import VariantBlockPage from "../../generated_pages/list_collector_variants_primary_person/variant-block.page"; +import PrimaryPersonListCollectorPage from "../../generated_pages/list_collector_variants_primary_person/primary-person-list-collector.page"; +import ListCollectorAddPage from "../../generated_pages/list_collector_variants_primary_person/list-collector-add.page"; +import ListCollectorPage from "../../generated_pages/list_collector_variants_primary_person/list-collector.page"; +import EditPersonPage from "../../generated_pages/list_collector_variants_primary_person/list-collector-edit.page"; +import SubmitPage from "../../generated_pages/list_collector_variants_primary_person/submit.page"; +import ThankYouPage from "../../base_pages/thank-you.page.js"; +import { click, verifyUrlContains } from "../../helpers"; + +describe("List collector with variants primary person", () => { + describe("Given that person lives in house", () => { + before("Load the survey", async () => { + await browser.openQuestionnaire("test_list_collector_variants_primary_person.json"); + it("When the user is asked questions about whether they like variant, Then they are routed to section asking if they live in the house", async () => { + await $(VariantBlockPage.yes()).click(); + await click(VariantBlockPage.submit()); + await expect(await $(PrimaryPersonListCollectorPage.legend()).getText()).toBe("Do you live here? (variant)"); + }); + }); + }); + describe("Given the user starts on the 'Do you like variant' question", () => { + before("Load the survey", async () => { + await browser.openQuestionnaire("test_list_collector_variants_primary_person.json"); + }); + it("When the user says that they do live there, Then they are shown as the primary person", async () => { + await $(VariantBlockPage.yes()).click(); + await click(VariantBlockPage.submit()); + await $(PrimaryPersonListCollectorPage.youLiveHereYes()).click(); + await click(PrimaryPersonListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("John"); + await $(ListCollectorAddPage.lastName()).setValue("Doe"); + await click(ListCollectorAddPage.submit()); + await expect(await $(ListCollectorPage.listLabel(1)).getText()).toBe("John Doe (You)"); + }); + it("When the user adds another person, Then they are shown in the list collector summary", async () => { + await $(ListCollectorPage.yesLabel()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Samuel"); + await $(ListCollectorAddPage.lastName()).setValue("Clemens"); + await click(ListCollectorAddPage.submit()); + await expect(await $(ListCollectorPage.listLabel(2)).getText()).toBe("Samuel Clemens"); + }); + it("When the user goes back and answers 'No' for 'Do you live here' question, Then the primary person is not shown", async () => { + await $(ListCollectorPage.previous()).click(); + await $(PrimaryPersonListCollectorPage.youLiveHereNo()).click(); + await click(PrimaryPersonListCollectorPage.submit()); + await expect(await $(ListCollectorPage.listLabel(1)).getText()).toBe("Samuel Clemens"); + }); + + it("When the user adds another person, Then the user is able to add members of the household", async () => { + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await expect(await $(ListCollectorAddPage.questionText()).getText()).toBe("What is the name of the person?"); + await $(ListCollectorAddPage.firstName()).setValue("Samuel"); + await $(ListCollectorAddPage.lastName()).setValue("Clemens"); + await click(ListCollectorAddPage.submit()); + }); + it("When the user adds the primary person again, Then the primary person is first in the list", async () => { + await $(ListCollectorPage.previous()).click(); + await $(PrimaryPersonListCollectorPage.youLiveHereYes()).click(); + await click(PrimaryPersonListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Mark"); + await $(ListCollectorAddPage.lastName()).setValue("Twin"); + await click(ListCollectorAddPage.submit()); + await expect(await $(ListCollectorPage.listLabel(1)).getText()).toBe("Mark Twin (You)"); + }); + it("When the user views the summary, Then it does not show the remove link for the primary person", async () => { + await expect(await $(ListCollectorPage.listRemoveLink(1)).isExisting()).toBe(false); + await expect(await $(ListCollectorPage.listRemoveLink(2)).isExisting()).toBe(true); + }); + it("When the user changes the primary person's name on the summary, Then the name should be updated", async () => { + await $(ListCollectorPage.listEditLink(1)).click(); + await $(EditPersonPage.firstName()).setValue("John"); + await $(EditPersonPage.lastName()).setValue("Doe"); + await click(EditPersonPage.submit()); + await expect(await $(ListCollectorPage.listLabel(1)).getText()).toBe("John Doe (You)"); + await expect(await $(ListCollectorPage.listLabel(2)).getText()).toBe("Samuel Clemens"); + }); + + it("When the user answers 'no' to add any person, Then the questionnaire shows the confirmation page", async () => { + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await verifyUrlContains(SubmitPage.url()); + }); + + it("When the user attempts to submit, Then they are shown the confirmation page", async () => { + await expect(await $(SubmitPage.guidance()).getText()).toBe("Please submit this survey to complete it"); + }); + + it("When user updates the variant answer, Then it should come back to summary screen with updated answer", async () => { + await $(SubmitPage.variantAnswerEdit()).click(); + await $(VariantBlockPage.no()).click(); + await click(VariantBlockPage.submit()); + await expect(await $(SubmitPage.variantAnswer()).getText()).toBe("No"); + }); + + it("When the user submits, Then they are allowed to submit the survey", async () => { + await click(SubmitPage.submit()); + await verifyUrlContains(ThankYouPage.pageName); + }); + }); +}); + +describe("Given the user starts on the 'Do you like variant' question", () => { + before("Load the survey", async () => { + await browser.openQuestionnaire("test_list_collector_variants_primary_person.json"); + }); + it("When the user answers 'No' for variant question, Then they are routed to section asking if they live in the house", async () => { + await $(VariantBlockPage.no()).click(); + await click(VariantBlockPage.submit()); + await expect(await $(PrimaryPersonListCollectorPage.legend()).getText()).toBe("Do you live here?"); + }); + + it("When the user says they do not live there and anyone else, Then confirmation screen is displayed", async () => { + await $(PrimaryPersonListCollectorPage.youLiveHereNo()).click(); + await click(PrimaryPersonListCollectorPage.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + + await expect(await $(SubmitPage.guidance()).getText()).toBe("Please submit this survey to complete it"); + }); +}); diff --git a/tests/functional/spec/list_collector/relationships-unrelated.spec.js b/tests/functional/spec/list_collector/relationships-unrelated.spec.js new file mode 100644 index 0000000000..ea61569815 --- /dev/null +++ b/tests/functional/spec/list_collector/relationships-unrelated.spec.js @@ -0,0 +1,96 @@ +import ListCollectorPage from "../../generated_pages/relationships_unrelated/list-collector.page.js"; +import ListCollectorAddPage from "../../generated_pages/relationships_unrelated/list-collector-add.page.js"; +import RelationshipsPage from "../../generated_pages/relationships_unrelated/relationships.page.js"; +import RelatedToAnyoneElsePage from "../../generated_pages/relationships_unrelated/related-to-anyone-else.page.js"; +import RelationshipsInterstitialPage from "../../generated_pages/relationships_unrelated/relationship-interstitial.page.js"; +import { click, verifyUrlContains } from "../../helpers"; + +describe("Unrelated Relationships", () => { + const schema = "test_relationships_unrelated.json"; + + describe("Given I am completing the test_relationships_unrelated survey,", () => { + before("load the survey", async () => { + await browser.openQuestionnaire(schema); + }); + + describe("And I add six people", () => { + before("add people", async () => { + await addPerson("Andrew", "Austin"); + await addPerson("Betty", "Burns"); + await addPerson("Carla", "Clark"); + await addPerson("Daniel", "Davis"); + await addPerson("Eve", "Elliot"); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + }); + + it("When I answer 'Unrelated' twice, Then I will be asked if anyone else is related with a list of the remaining people", async () => { + await $(RelationshipsPage.unrelated()).click(); + await click(RelationshipsPage.submit()); + await $(RelationshipsPage.unrelated()).click(); + await click(RelationshipsPage.submit()); + await expect(await $(RelatedToAnyoneElsePage.questionText()).getText()).toBe("Are any of these people related to you?"); + await expect(await $(RelatedToAnyoneElsePage.listLabel(1)).getText()).toBe("Daniel Davis"); + await expect(await $(RelatedToAnyoneElsePage.listLabel(2)).getText()).toBe("Eve Elliot"); + }); + + it("When I click previous, Then I will go back to the previous relationship", async () => { + await $(RelatedToAnyoneElsePage.previous()).click(); + await expect(await $(RelationshipsPage.questionText()).getText()).toContain("Carla Clark is unrelated to Andrew Austin"); + }); + + it("When I return to the 'related to anyone else' question and select 'Yes', Then I will be taken to the next relationship for the first person", async () => { + await click(RelationshipsPage.submit()); + await $(RelatedToAnyoneElsePage.yes()).click(); + await click(RelatedToAnyoneElsePage.submit()); + await expect(await $(RelationshipsPage.questionText()).getText()).toContain("Thinking about Andrew Austin, Daniel Davis is their"); + }); + + it("When I click previous, Then I will go back to the 'related to anyone else' question", async () => { + await $(RelationshipsPage.previous()).click(); + await expect(await $(RelatedToAnyoneElsePage.questionText()).getText()).toBe("Are any of these people related to you?"); + await expect(await $(RelatedToAnyoneElsePage.yes()).isSelected()).toBe(true); + }); + + it("When I select 'No' to the 'related to anyone else' question, Then I will be taken to the first relationship for the second person", async () => { + await $(RelatedToAnyoneElsePage.noNoneOfThesePeopleAreRelatedToMe()).click(); + await click(RelatedToAnyoneElsePage.submit()); + await expect(await $(RelationshipsPage.questionText()).getText()).toContain("Thinking about Betty Burns, Carla Clark is their"); + }); + + it("When I click previous, Then I will go back to the 'related to anyone else' question for the first person", async () => { + await $(RelationshipsPage.previous()).click(); + await expect(await $(RelatedToAnyoneElsePage.questionText()).getText()).toBe("Are any of these people related to you?"); + await expect(await $(RelatedToAnyoneElsePage.listLabel(1)).getText()).toBe("Daniel Davis"); + await expect(await $(RelatedToAnyoneElsePage.listLabel(2)).getText()).toBe("Eve Elliot"); + await expect(await $(RelatedToAnyoneElsePage.noNoneOfThesePeopleAreRelatedToMe()).isSelected()).toBe(true); + }); + + it("When I click complete the remaining relationships, Then I will go to the relationships section complete page", async () => { + await click(RelatedToAnyoneElsePage.submit()); + await $(RelationshipsPage.unrelated()).click(); + await click(RelationshipsPage.submit()); + await $(RelationshipsPage.unrelated()).click(); + await click(RelationshipsPage.submit()); + await $(RelationshipsPage.unrelated()).click(); + await click(RelationshipsPage.submit()); + await $(RelationshipsPage.unrelated()).click(); + await click(RelationshipsPage.submit()); + await $(RelationshipsPage.unrelated()).click(); + await click(RelationshipsPage.submit()); + await $(RelationshipsPage.unrelated()).click(); + await click(RelationshipsPage.submit()); + await verifyUrlContains(RelationshipsInterstitialPage.pageName); + }); + }); + + async function addPerson(firstName, lastName) { + await $(ListCollectorPage.yes()).click(); + await $(ListCollectorPage.submit()).scrollIntoView(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue(firstName); + await $(ListCollectorAddPage.lastName()).setValue(lastName); + await click(ListCollectorAddPage.submit()); + } + }); +}); diff --git a/tests/functional/spec/list_collector/relationships.spec.js b/tests/functional/spec/list_collector/relationships.spec.js new file mode 100644 index 0000000000..8dac968fe9 --- /dev/null +++ b/tests/functional/spec/list_collector/relationships.spec.js @@ -0,0 +1,212 @@ +import ListCollectorPage from "../../generated_pages/relationships/list-collector.page.js"; +import ListCollectorAddPage from "../../generated_pages/relationships/list-collector-add.page.js"; +import ListCollectorRemovePage from "../../generated_pages/relationships/list-collector-remove.page.js"; +import RelationshipsPage from "../../generated_pages/relationships/relationships.page.js"; +import RelationshipsInterstitialPage from "../../generated_pages/relationships/relationship-interstitial.page.js"; +import SectionSummaryPage from "../../generated_pages/relationships/section-summary.page.js"; +import { click, verifyUrlContains } from "../../helpers"; + +describe("Relationships", () => { + const schema = "test_relationships.json"; + + describe("Given I am completing the test_relationships survey,", () => { + beforeEach("load the survey", async () => { + await browser.openQuestionnaire(schema); + }); + + it("When I have one household member, Then I will be not be asked about relationships", async () => { + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Marcus"); + await $(ListCollectorAddPage.lastName()).setValue("Twin"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.no()).click(); + // eslint-disable-next-line no-undef + await click(ListCollectorPage.submit()); + await verifyUrlContains("/sections/section/"); + }); + + it("When I add two household members, Then I will be asked about one relationship", async () => { + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Marcus"); + await $(ListCollectorAddPage.lastName()).setValue("Twin"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Samuel"); + await $(ListCollectorAddPage.lastName()).setValue("Clemens"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.no()).click(); + await $(ListCollectorPage.submit()).scrollIntoView(); + await click(ListCollectorPage.submit()); + await verifyUrlContains(RelationshipsPage.pageName); + await $(RelationshipsPage.husbandOrWife()).click(); + await click(RelationshipsPage.submit()); + await click(RelationshipsInterstitialPage.submit()); + await verifyUrlContains("/sections/section/"); + }); + + describe("When I add three household members,", () => { + beforeEach("add three people", async () => { + await addThreePeople(); + }); + + it("Then I will be asked about all relationships", async () => { + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await $(RelationshipsPage.husbandOrWife()).click(); + await click(RelationshipsPage.submit()); + await $(RelationshipsPage.legallyRegisteredCivilPartner()).click(); + await click(RelationshipsPage.submit()); + await $(RelationshipsPage.husbandOrWife()).click(); + await click(RelationshipsPage.submit()); + await click(RelationshipsInterstitialPage.submit()); + await verifyUrlContains("/sections/section/"); + }); + + it("And go to the first relationship, Then the previous link should return to the list collector", async () => { + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await $(RelationshipsPage.previous()).click(); + await verifyUrlContains("/questionnaire/list-collector/"); + }); + + it("And go to the first relationship, Then the 'Brother or Sister' option should have the text 'Including half brother or half sister'", async () => { + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await expect(await $(RelationshipsPage.brotherOrSisterLabelDescription()).getText()).toBe("Including half brother or half sister"); + }); + + it("And go to the second relationship, Then the previous link should return to the first relationship", async () => { + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await $(RelationshipsPage.husbandOrWife()).click(); + await click(RelationshipsPage.submit()); + await $(RelationshipsPage.previous()).click(); + await click(RelationshipsInterstitialPage.submit()); + await verifyUrlContains(RelationshipsPage.pageName); + await expect(await $(RelationshipsPage.questionText()).getText()).toContain("Marcus"); + }); + + it("And go to the section summary, Then the previous link should return to the last relationship Interstitial", async () => { + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await $(RelationshipsPage.husbandOrWife()).click(); + await click(RelationshipsPage.submit()); + await $(RelationshipsPage.legallyRegisteredCivilPartner()).click(); + await click(RelationshipsPage.submit()); + await $(RelationshipsPage.husbandOrWife()).click(); + await click(RelationshipsPage.submit()); + await click(RelationshipsInterstitialPage.submit()); + await verifyUrlContains("/sections/section/"); + await $(SectionSummaryPage.previous()).click(); + await $(RelationshipsInterstitialPage.previous()).click(); + await verifyUrlContains(RelationshipsPage.pageName); + await expect(await $(RelationshipsPage.questionText()).getText()).toContain("Olivia"); + }); + + it("When I add all relationships and return to the relationships, Then the relationships should be populated", async () => { + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await $(RelationshipsPage.husbandOrWife()).click(); + await click(RelationshipsPage.submit()); + await $(RelationshipsPage.legallyRegisteredCivilPartner()).click(); + await click(RelationshipsPage.submit()); + await $(RelationshipsPage.husbandOrWife()).click(); + await click(RelationshipsPage.submit()); + await click(RelationshipsInterstitialPage.submit()); + await verifyUrlContains("/sections/section/"); + await $(SectionSummaryPage.previous()).click(); + await $(RelationshipsInterstitialPage.previous()).click(); + await expect(await $(RelationshipsPage.husbandOrWife()).isSelected()).toBe(true); + await $(RelationshipsPage.previous()).click(); + await expect(await $(RelationshipsPage.legallyRegisteredCivilPartner()).isSelected()).toBe(true); + }); + + it("And go to the first relationship, Then the person's name should be in the question title and playback text", async () => { + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await expect(await $(ListCollectorPage.questionText()).getText()).toContain("Marcus Twin"); + await expect(await $(RelationshipsPage.playback()).getText()).toContain("Marcus Twin"); + }); + + it("And go to the first relationship and submit without selecting an option, Then an error should be displayed", async () => { + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await click(RelationshipsPage.submit()); + await expect(await $(RelationshipsPage.error()).isDisplayed()).toBe(true); + }); + + it("And go to the first relationship and click 'Save and sign out', Then I should be signed out", async () => { + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await $(RelationshipsPage.husbandOrWife()).click(); + await $(RelationshipsPage.saveSignOut()).click(); + await expect(await browser.getUrl()).not.toContain("questionnaire"); + }); + + it("And go to the first relationship, select a relationship and click 'Save and sign out', Then I should be signed out", async () => { + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await $(RelationshipsPage.saveSignOut()).click(); + await expect(await browser.getUrl()).not.toContain("questionnaire"); + }); + }); + + describe("When I have added one or more household members after answering the relationships question,", () => { + beforeEach("add three people and complete their relationships", async () => { + await addThreePeopleAndCompleteRelationships(); + }); + + it("Then I delete one of the original household members I will not be asked for the original members relationships again", async () => { + await $(SectionSummaryPage.peopleListRemoveLink(1)).click(); + await $(ListCollectorRemovePage.yes()).click(); + await click(ListCollectorRemovePage.submit()); + await verifyUrlContains("/sections/section/"); + }); + + it("Then I add another household member I will be redirected to parent list collector", async () => { + await $(SectionSummaryPage.peopleListAddLink()).click(); + await $(ListCollectorAddPage.firstName()).setValue("Tom"); + await $(ListCollectorAddPage.lastName()).setValue("Bowden"); + await click(ListCollectorAddPage.submit()); + await verifyUrlContains("/questionnaire/list-collector/"); + }); + }); + + async function addThreePeopleAndCompleteRelationships() { + await addThreePeople(); + + await $(ListCollectorPage.no()).click(); + await $(ListCollectorPage.submit()).scrollIntoView(); + await click(ListCollectorPage.submit()); + await $(RelationshipsPage.husbandOrWife()).click(); + await click(RelationshipsPage.submit()); + await $(RelationshipsPage.legallyRegisteredCivilPartner()).click(); + await click(RelationshipsPage.submit()); + await $(RelationshipsPage.husbandOrWife()).click(); + await click(RelationshipsPage.submit()); + await click(RelationshipsInterstitialPage.submit()); + } + + async function addThreePeople() { + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Marcus"); + await $(ListCollectorAddPage.lastName()).setValue("Twin"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Samuel"); + await $(ListCollectorAddPage.lastName()).setValue("Clemens"); + await $(ListCollectorAddPage.submit()).scrollIntoView(); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Olivia"); + await $(ListCollectorAddPage.lastName()).setValue("Clemens"); + await click(ListCollectorAddPage.submit()); + } + }); +}); diff --git a/tests/functional/spec/list_collector/relationships_primary.spec.js b/tests/functional/spec/list_collector/relationships_primary.spec.js new file mode 100644 index 0000000000..ef696073f7 --- /dev/null +++ b/tests/functional/spec/list_collector/relationships_primary.spec.js @@ -0,0 +1,89 @@ +import PrimaryPersonListCollectorPage from "../../generated_pages/relationships_primary/primary-person-list-collector.page.js"; +import PrimaryPersonListCollectorAddPage from "../../generated_pages/relationships_primary/primary-person-list-collector-add.page.js"; +import ListCollectorPage from "../../generated_pages/relationships_primary/list-collector.page.js"; +import ListCollectorAddPage from "../../generated_pages/relationships_primary/list-collector-add.page.js"; +import RelationshipsPage from "../../generated_pages/relationships_primary/relationships.page.js"; +import { click } from "../../helpers"; + +describe("Relationships - Primary Person", () => { + const schema = "test_relationships_primary.json"; + + describe("Given I am completing the test_relationships_primary survey", () => { + beforeEach(async () => { + await browser.openQuestionnaire(schema); + }); + + it("When I add household members, Then I will be asked my relationships as a primary person", async () => { + await addPrimaryAndTwoOthers(); + + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await expect(await $(RelationshipsPage.questionText()).getText()).toContain("is your"); + }); + + it("When I add household members, Then non-primary relationships will be asked as a non primary person", async () => { + await addPrimaryAndTwoOthers(); + + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await $(RelationshipsPage.relationshipBrotherOrSister()).click(); + await click(RelationshipsPage.submit()); + await $(RelationshipsPage.relationshipSonOrDaughter()).click(); + await click(RelationshipsPage.submit()); + await expect(await $(RelationshipsPage.questionText()).getText()).toContain("is their"); + }); + + it("When I add household members And add their relationships And remove the primary person And add a new primary person then I will be asked for the relationships again", async () => { + await addPrimaryAndTwoOthersAndCompleteRelationships(); + + await browser.url("/questionnaire/primary-person-list-collector"); + + await $(PrimaryPersonListCollectorPage.no()).click(); + await click(PrimaryPersonListCollectorPage.submit()); + + await browser.url("/questionnaire/primary-person-list-collector"); + + await $(PrimaryPersonListCollectorPage.yes()).click(); + await click(PrimaryPersonListCollectorPage.submit()); + await $(PrimaryPersonListCollectorAddPage.firstName()).setValue("Marcus"); + await $(PrimaryPersonListCollectorAddPage.lastName()).setValue("Twin"); + await click(PrimaryPersonListCollectorAddPage.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + + await expect(await $(RelationshipsPage.questionText()).getText()).toContain("Samuel Clemens is your"); + }); + + async function addPrimaryAndTwoOthersAndCompleteRelationships() { + await addPrimaryAndTwoOthers(); + + await $(ListCollectorPage.no()).click(); + await $(ListCollectorPage.submit()).scrollIntoView(); + await click(ListCollectorPage.submit()); + await $(RelationshipsPage.relationshipBrotherOrSister()).click(); + await click(RelationshipsPage.submit()); + await $(RelationshipsPage.relationshipSonOrDaughter()).click(); + await click(RelationshipsPage.submit()); + await $(RelationshipsPage.relationshipBrotherOrSister()).click(); + } + + async function addPrimaryAndTwoOthers() { + await $(PrimaryPersonListCollectorPage.yes()).click(); + await $(PrimaryPersonListCollectorPage.submit()).scrollIntoView(); + await click(PrimaryPersonListCollectorPage.submit()); + await $(PrimaryPersonListCollectorAddPage.firstName()).setValue("Marcus"); + await $(PrimaryPersonListCollectorAddPage.lastName()).setValue("Twin"); + await click(PrimaryPersonListCollectorAddPage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Samuel"); + await $(ListCollectorAddPage.lastName()).setValue("Clemens"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Olivia"); + await $(ListCollectorAddPage.lastName()).setValue("Clemens"); + await click(ListCollectorAddPage.submit()); + } + }); +}); diff --git a/tests/functional/spec/list_collector_driving_question.spec.js b/tests/functional/spec/list_collector_driving_question.spec.js deleted file mode 100644 index 3643b05d14..0000000000 --- a/tests/functional/spec/list_collector_driving_question.spec.js +++ /dev/null @@ -1,61 +0,0 @@ -import checkPeopleInList from "../helpers"; -import HubPage from "../base_pages/hub.page.js"; -import AnyoneUsuallyLiveAtPage from "../generated_pages/list_collector_driving_question/anyone-usually-live-at.page.js"; -import AnyoneElseLiveAtListCollectorPage from "../generated_pages/list_collector_driving_question/anyone-else-live-at.page.js"; -import AnyoneElseLiveAtListCollectorAddPage from "../generated_pages/list_collector_driving_question/anyone-else-live-at-add.page.js"; -import AnyoneElseLiveAtListCollectorRemovePage from "../generated_pages/list_collector_driving_question/anyone-else-live-at-remove.page.js"; -import SectionSummaryPage from "../generated_pages/list_collector_driving_question/section-summary.page.js"; - -describe("List Collector Driving Question", () => { - beforeEach("Load the survey", () => { - browser.openQuestionnaire("test_list_collector_driving_question.json"); - $(HubPage.submit()).click(); - }); - - describe("Given a happy journey through the list collector", () => { - it("The collector shows all of the household members in the summary", () => { - $(AnyoneUsuallyLiveAtPage.yes()).click(); - $(AnyoneUsuallyLiveAtPage.submit()).click(); - $(AnyoneElseLiveAtListCollectorAddPage.firstName()).setValue("Marcus"); - $(AnyoneElseLiveAtListCollectorAddPage.lastName()).setValue("Twin"); - $(AnyoneElseLiveAtListCollectorAddPage.submit()).click(); - $(AnyoneElseLiveAtListCollectorPage.yes()).click(); - $(AnyoneElseLiveAtListCollectorPage.submit()).click(); - $(AnyoneElseLiveAtListCollectorAddPage.firstName()).setValue("Suzy"); - $(AnyoneElseLiveAtListCollectorAddPage.lastName()).setValue("Clemens"); - $(AnyoneElseLiveAtListCollectorAddPage.submit()).click(); - $(AnyoneElseLiveAtListCollectorPage.no()).click(); - $(AnyoneElseLiveAtListCollectorPage.submit()).click(); - - const peopleExpected = ["Marcus Twin", "Suzy Clemens"]; - - checkPeopleInList(peopleExpected, SectionSummaryPage.peopleListLabel); - }); - }); - - describe("Given the user answers no to the driving question", () => { - it("The summary add link returns to the driving question", () => { - $(AnyoneUsuallyLiveAtPage.no()).click(); - $(AnyoneUsuallyLiveAtPage.submit()).click(); - $(SectionSummaryPage.peopleListAddLink()).click(); - expect(browser.getUrl()).to.contain(AnyoneUsuallyLiveAtPage.url()); - }); - }); - - describe("Given the user answers yes to the driving question, adds someone and later removes them", () => { - it("The summary add link should return to the original list collector", () => { - $(AnyoneUsuallyLiveAtPage.yes()).click(); - $(AnyoneUsuallyLiveAtPage.submit()).click(); - $(AnyoneElseLiveAtListCollectorAddPage.firstName()).setValue("Marcus"); - $(AnyoneElseLiveAtListCollectorAddPage.lastName()).setValue("Twin"); - $(AnyoneElseLiveAtListCollectorAddPage.submit()).click(); - $(AnyoneElseLiveAtListCollectorPage.no()).click(); - $(AnyoneElseLiveAtListCollectorPage.submit()).click(); - $(SectionSummaryPage.peopleListRemoveLink(1)).click(); - $(AnyoneElseLiveAtListCollectorRemovePage.yes()).click(); - $(AnyoneElseLiveAtListCollectorRemovePage.submit()).click(); - $(SectionSummaryPage.peopleListAddLink()).click(); - expect(browser.getUrl()).to.contain(AnyoneElseLiveAtListCollectorAddPage.pageName); - }); - }); -}); diff --git a/tests/functional/spec/list_collector_driving_question_checkbox.spec.js b/tests/functional/spec/list_collector_driving_question_checkbox.spec.js deleted file mode 100644 index bb65f3bc0d..0000000000 --- a/tests/functional/spec/list_collector_driving_question_checkbox.spec.js +++ /dev/null @@ -1,103 +0,0 @@ -import checkPeopleInList from "../helpers"; -import HubPage from "../base_pages/hub.page.js"; -import PrimaryPersonListCollectorPage from "../generated_pages/list_collector_driving_checkbox/primary-person-list-collector.page.js"; -import PrimaryPersonListCollectorAddPage from "../generated_pages/list_collector_driving_checkbox/primary-person-list-collector-add.page.js"; -import AnyoneUsuallyLiveAtPage from "../generated_pages/list_collector_driving_checkbox/anyone-usually-live-at.page.js"; -import ListCollectorAddPage from "../generated_pages/list_collector_driving_checkbox/list-collector-add.page.js"; -import ListCollectorPage from "../generated_pages/list_collector_driving_checkbox/list-collector.page.js"; -import ListCollectorTemporaryAwayPage from "../generated_pages/list_collector_driving_checkbox/list-collector-temporary-away-stay.page"; -import ListCollectorTemporaryAwayAddPage from "../generated_pages/list_collector_driving_checkbox/list-collector-temporary-away-stay-add.page"; -import SummaryPage from "../generated_pages/list_collector_driving_checkbox/section-summary.page"; - -const beforeSetup = () => { - browser.openQuestionnaire("test_list_collector_driving_checkbox.json"); - $(HubPage.submit()).click(); -}; - -describe("List Collector Driving Checkbox Question", () => { - before("Load the survey", beforeSetup); - - describe("Given a happy journey through the list collectors", () => { - it("All of the household members and visitors are shown in the summary", () => { - $(PrimaryPersonListCollectorPage.yesIUsuallyLiveHere()).click(); - $(PrimaryPersonListCollectorPage.submit()).click(); - $(PrimaryPersonListCollectorAddPage.firstName()).setValue("Marcus"); - $(PrimaryPersonListCollectorAddPage.lastName()).setValue("Twin"); - $(PrimaryPersonListCollectorAddPage.submit()).click(); - $(AnyoneUsuallyLiveAtPage.familyMembersAndPartners()).click(); - $(AnyoneUsuallyLiveAtPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("Suzy"); - $(ListCollectorAddPage.lastName()).setValue("Clemens"); - $(ListCollectorAddPage.submit()).click(); - $(ListCollectorPage.noIDoNotNeedToAddAPerson()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorTemporaryAwayPage.noThereAreNumberOfPeoplePeopleLivingHere()).click(); - $(ListCollectorTemporaryAwayPage.submit()).click(); - - const householdMembersExpected = ["Marcus Twin (You)", "Suzy Clemens"]; - checkPeopleInList(householdMembersExpected, SummaryPage.peopleListLabel); - }); - }); - - describe("Given the primary person is removed", () => { - it("Then they aren't shown on the summary screen", () => { - $(SummaryPage.previous()).click(); - $(ListCollectorTemporaryAwayPage.previous()).click(); - $(ListCollectorPage.previous()).click(); - $(AnyoneUsuallyLiveAtPage.previous()).click(); - $(PrimaryPersonListCollectorPage.noIDonTUsuallyLiveHere()).click(); - $(PrimaryPersonListCollectorPage.submit()).click(); - - const householdMembersExpected = ["Suzy Clemens"]; - checkPeopleInList(householdMembersExpected, SummaryPage.peopleListLabel); - }); - }); - - describe("Given the user chooses yes from the second list collector", () => { - it("Then they are taken to the correct list add screen", () => { - $(SummaryPage.previous()).click(); - $(ListCollectorTemporaryAwayPage.yesINeedToAddSomeone()).click(); - $(ListCollectorTemporaryAwayPage.submit()).click(); - $(ListCollectorTemporaryAwayAddPage.firstName()).setValue("Christopher"); - $(ListCollectorTemporaryAwayAddPage.lastName()).setValue("Pike"); - $(ListCollectorTemporaryAwayAddPage.submit()).click(); - $(ListCollectorTemporaryAwayPage.noThereAreNumberOfPeoplePeopleLivingHere()).click(); - $(ListCollectorTemporaryAwayPage.submit()).click(); - - const householdMembersExpected = ["Suzy Clemens", "Christopher Pike"]; - checkPeopleInList(householdMembersExpected, SummaryPage.peopleListLabel); - }); - }); -}); - -describe("Given the user says no one else lives in the house", () => { - before("Load the survey", beforeSetup); - - it("The user is asked if they need to add anyone that is temporarily away", () => { - $(PrimaryPersonListCollectorPage.yesIUsuallyLiveHere()).click(); - $(PrimaryPersonListCollectorPage.submit()).click(); - $(PrimaryPersonListCollectorAddPage.firstName()).setValue("Marcus"); - $(PrimaryPersonListCollectorAddPage.lastName()).setValue("Twin"); - $(PrimaryPersonListCollectorAddPage.submit()).click(); - $(AnyoneUsuallyLiveAtPage.exclusiveNoneOfTheseApplyNoOneUsuallyLivesHere()).click(); - $(AnyoneUsuallyLiveAtPage.submit()).click(); - - expect($(ListCollectorTemporaryAwayPage.questionText()).getText()).to.equal("You said 1 person lives at 12 Lovely Villas. Do you need to add anyone?"); - }); -}); - -describe("Given a person does not live in the house", () => { - before("Load the survey", beforeSetup); - it("The user is asked whether they live there", () => { - $(PrimaryPersonListCollectorPage.noIDonTUsuallyLiveHere()).click(); - $(PrimaryPersonListCollectorPage.submit()).click(); - expect($(AnyoneUsuallyLiveAtPage.questionText()).getText()).to.equal("Do any of the following usually live at 12 Lovely Villas on 21 March?"); - - $(AnyoneUsuallyLiveAtPage.exclusiveNoneOfTheseApplyNoOneUsuallyLivesHere()).click(); - $(AnyoneUsuallyLiveAtPage.submit()).click(); - expect($(ListCollectorTemporaryAwayPage.questionText()).getText()).to.equal("You said 0 people lives at 12 Lovely Villas. Do you need to add anyone?"); - - $(ListCollectorTemporaryAwayPage.noThereAreNumberOfPeoplePeopleLivingHere()).click(); - $(AnyoneUsuallyLiveAtPage.submit()).click(); - }); -}); diff --git a/tests/functional/spec/list_collector_primary_person.spec.js b/tests/functional/spec/list_collector_primary_person.spec.js deleted file mode 100644 index baa56b8571..0000000000 --- a/tests/functional/spec/list_collector_primary_person.spec.js +++ /dev/null @@ -1,126 +0,0 @@ -import ListCollectorPage from "../generated_pages/list_collector_primary_person/list-collector.page.js"; -import ListCollectorAddPage from "../generated_pages/list_collector_primary_person/list-collector-add.page.js"; -import ListCollectorEditPage from "../generated_pages/list_collector_primary_person/list-collector-edit.page.js"; -import PrimaryPersonListCollectorPage from "../generated_pages/list_collector_primary_person/primary-person-list-collector.page.js"; -import PrimaryPersonListCollectorAddPage from "../generated_pages/list_collector_primary_person/primary-person-list-collector-add.page.js"; -import SectionSummaryPage from "../generated_pages/list_collector/section-summary.page.js"; -import { SubmitPage } from "../base_pages/submit.page.js"; -import ThankYouPage from "../base_pages/thank-you.page.js"; -import AnyoneUsuallyLiveAtPage from "../generated_pages/list_collector_primary_person/anyone-usually-live-at.page.js"; - -describe("Primary Person List Collector Survey", () => { - describe("Given the user starts on the 'do you live here' question", () => { - before("Load the survey", () => { - browser.openQuestionnaire("test_list_collector_primary_person.json"); - }); - - it.skip("When the user says they do not live there, and changes their answer to yes, then the user can't navigate to the list collector", () => { - $(PrimaryPersonListCollectorPage.noLabel()).click(); - $(PrimaryPersonListCollectorPage.submit()).click(); - $(PrimaryPersonListCollectorAddPage.previous()).click(); - $(PrimaryPersonListCollectorPage.yesLabel()).click(); - $(PrimaryPersonListCollectorPage.submit()).click(); - browser.url("questionnaire/list-collector"); - expect($(PrimaryPersonListCollectorPage.questionText()).getText()).to.contain("Do you live here"); - }); - }); - - describe("Given the user starts on the 'do you live here' question", () => { - before("Load the survey", () => { - browser.openQuestionnaire("test_list_collector_primary_person.json"); - }); - - it("When the user says that they do live there, then they are shown as the primary person", () => { - $(PrimaryPersonListCollectorPage.yesLabel()).click(); - $(PrimaryPersonListCollectorPage.submit()).click(); - $(PrimaryPersonListCollectorAddPage.firstName()).setValue("Mark"); - $(PrimaryPersonListCollectorAddPage.lastName()).setValue("Twin"); - $(PrimaryPersonListCollectorAddPage.submit()).click(); - expect($(ListCollectorPage.listLabel(1)).getText()).to.equal("Mark Twin (You)"); - }); - - it("When the user adds another person, they are shown in the summary", () => { - $(ListCollectorPage.yesLabel()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("Samuel"); - $(ListCollectorAddPage.lastName()).setValue("Clemens"); - $(ListCollectorAddPage.submit()).click(); - expect($(ListCollectorPage.listLabel(2)).getText()).to.equal("Samuel Clemens"); - }); - - it("When the user goes back and answers No, the primary person is not shown", () => { - $(ListCollectorPage.previous()).click(); - $(PrimaryPersonListCollectorPage.no()).click(); - $(PrimaryPersonListCollectorPage.submit()).click(); - $(AnyoneUsuallyLiveAtPage.no()).click(); - $(AnyoneUsuallyLiveAtPage.submit()).click(); - expect($(ListCollectorPage.listLabel(1)).getText()).to.equal("Samuel Clemens"); - }); - - it("When the user adds the primary person again, then the primary person is first in the list", () => { - $(ListCollectorPage.previous()).click(); - $(AnyoneUsuallyLiveAtPage.previous()).click(); - $(PrimaryPersonListCollectorPage.yes()).click(); - $(PrimaryPersonListCollectorPage.submit()).click(); - $(PrimaryPersonListCollectorAddPage.firstName()).setValue("Mark"); - $(PrimaryPersonListCollectorAddPage.lastName()).setValue("Twin"); - $(PrimaryPersonListCollectorAddPage.submit()).click(); - expect($(ListCollectorPage.listLabel(1)).getText()).to.equal("Mark Twin (You)"); - }); - - it("When the user views the summary, then it does not show the remove link for the primary person", () => { - expect($(ListCollectorPage.listRemoveLink(1)).isExisting()).to.be.false; - expect($(ListCollectorPage.listRemoveLink(2)).isExisting()).to.be.true; - }); - - it("When the user changes the primary person's name on the summary, then the name should be updated", () => { - $(ListCollectorPage.listEditLink(1)).click(); - $(ListCollectorEditPage.firstName()).setValue("Mark"); - $(ListCollectorEditPage.lastName()).setValue("Twain"); - $(ListCollectorEditPage.submit()).click(); - expect($(ListCollectorPage.listLabel(1)).getText()).to.equal("Mark Twain (You)"); - expect($(ListCollectorPage.listLabel(2)).getText()).to.equal("Samuel Clemens"); - }); - - it("When the user views the summary, then it does not show the does anyone usually live here question", () => { - $(ListCollectorPage.no()).click(); - $(ListCollectorPage.submit()).click(); - expect($("body").getText()).to.not.equal("usually live here"); - }); - - it("When the user attempts to submit, then they are shown the confirmation page", () => { - $(SectionSummaryPage.submit()).click(); - expect($(SubmitPage.guidance()).getText()).to.contain("Thank you for your answers, do you wish to submit"); - }); - - it("When the user submits, then they are allowed to submit the survey", () => { - $(SubmitPage.submit()).click(); - expect(browser.getUrl()).to.contain(ThankYouPage.pageName); - }); - }); - - describe("Given the user starts on the 'do you live here' question", () => { - before("Load the survey", () => { - browser.openQuestionnaire("test_list_collector_primary_person.json"); - }); - - it("When the user says they do not live there, then an empty list is displayed", () => { - $(PrimaryPersonListCollectorPage.no()).click(); - $(PrimaryPersonListCollectorPage.submit()).click(); - $(AnyoneUsuallyLiveAtPage.no()).click(); - expect($(ListCollectorPage.listLabel(1)).isExisting()).to.be.false; - }); - - it("When the user clicks on the add person button multiple times, then only one person is added", () => { - $(ListCollectorPage.previous()).click(); - $(PrimaryPersonListCollectorPage.yes()).click(); - $(PrimaryPersonListCollectorPage.submit()).click(); - $(PrimaryPersonListCollectorAddPage.firstName()).setValue("Mark"); - $(PrimaryPersonListCollectorAddPage.lastName()).setValue("Twain"); - $(PrimaryPersonListCollectorPage.submit()).click(); - $(PrimaryPersonListCollectorPage.submit()).click(); - expect($(ListCollectorPage.listLabel(1)).getText()).to.equal("Mark Twain (You)"); - expect($(ListCollectorPage.listLabel(2)).isExisting()).to.be.false; - }); - }); -}); diff --git a/tests/functional/spec/list_collector_variants.spec.js b/tests/functional/spec/list_collector_variants.spec.js deleted file mode 100644 index 64238c73c7..0000000000 --- a/tests/functional/spec/list_collector_variants.spec.js +++ /dev/null @@ -1,102 +0,0 @@ -import checkPeopleInList from "../helpers"; -import YouLiveHerePage from "../generated_pages/list_collector_variants/you-live-here-block.page.js"; -import ListCollectorPage from "../generated_pages/list_collector_variants/list-collector.page.js"; -import ListCollectorAddPage from "../generated_pages/list_collector_variants/list-collector-add.page.js"; -import ListCollectorEditPage from "../generated_pages/list_collector_variants/list-collector-edit.page.js"; -import ListCollectorRemovePage from "../generated_pages/list_collector_variants/list-collector-remove.page.js"; -import { SubmitPage } from "../base_pages/submit.page.js"; -import ThankYouPage from "../base_pages/thank-you.page.js"; - -describe("List Collector With Variants", () => { - describe("Given that a person lives in house", () => { - before("Load the survey", () => { - browser.openQuestionnaire("test_list_collector_variants.json"); - }); - - it("The user is asked questions about whether they live there", () => { - $(YouLiveHerePage.yes()).click(); - $(YouLiveHerePage.submit()).click(); - expect($(ListCollectorPage.questionText()).getText()).to.equal("Does anyone else live at 1 Pleasant Lane?"); - }); - - it("The user is able to add members of the household", () => { - $(ListCollectorPage.anyoneElseYes()).click(); - $(ListCollectorPage.submit()).click(); - expect($(ListCollectorAddPage.questionText()).getText()).to.equal("What is the name of the person?"); - $(ListCollectorAddPage.firstName()).setValue("Samuel"); - $(ListCollectorAddPage.lastName()).setValue("Clemens"); - $(ListCollectorAddPage.submit()).click(); - }); - - it("The user can see all household members in the summary", () => { - const peopleExpected = ["Samuel Clemens"]; - checkPeopleInList(peopleExpected, ListCollectorPage.listLabel); - }); - - it("The questionnaire has the correct question text on the change and remove pages", () => { - $(ListCollectorPage.listEditLink(1)).click(); - expect($(ListCollectorEditPage.questionText()).getText()).to.equal("What is the name of the person?"); - $(ListCollectorEditPage.previous()).click(); - $(ListCollectorPage.listRemoveLink(1)).click(); - expect($(ListCollectorRemovePage.questionText()).getText()).to.equal("Are you sure you want to remove this person?"); - $(ListCollectorRemovePage.previous()).click(); - }); - - it("The questionnaire shows the confirmation page when no more people to add", () => { - $(ListCollectorPage.anyoneElseNo()).click(); - $(ListCollectorPage.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.url()); - }); - - it("The questionnaire allows submission", () => { - $(SubmitPage.submit()).click(); - expect(browser.getUrl()).to.contain("thank-you"); - }); - }); - - describe("Given a person does not live in house", () => { - before("Load the survey", () => { - browser.openQuestionnaire("test_list_collector_variants.json"); - }); - - it("The user is asked questions about whether they live there", () => { - $(YouLiveHerePage.no()).click(); - $(YouLiveHerePage.submit()).click(); - expect($(ListCollectorPage.questionText()).getText()).to.equal("Does anyone live at 1 Pleasant Lane?"); - }); - - it("The user is able to add members of the household", () => { - $(ListCollectorPage.anyoneElseYes()).click(); - $(ListCollectorPage.submit()).click(); - expect($(ListCollectorAddPage.questionText()).getText()).to.equal("What is the name of the person who isn’t you?"); - $(ListCollectorAddPage.firstName()).setValue("Samuel"); - $(ListCollectorAddPage.lastName()).setValue("Clemens"); - $(ListCollectorAddPage.submit()).click(); - }); - - it("The user can see all household members in the summary", () => { - const peopleExpected = ["Samuel Clemens"]; - checkPeopleInList(peopleExpected, ListCollectorPage.listLabel); - }); - - it("The questionnaire has the correct question text on the change and remove pages", () => { - $(ListCollectorPage.listEditLink(1)).click(); - expect($(ListCollectorEditPage.questionText()).getText()).to.equal("What is the name of the person who isn’t you?"); - $(ListCollectorEditPage.previous()).click(); - $(ListCollectorPage.listRemoveLink(1)).click(); - expect($(ListCollectorRemovePage.questionText()).getText()).to.equal("Are you sure you want to remove this person who isn’t you?"); - $(ListCollectorRemovePage.previous()).click(); - }); - - it("The questionnaire shows the confirmation page when no more people to add", () => { - $(ListCollectorPage.anyoneElseNo()).click(); - $(ListCollectorPage.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.url()); - }); - - it("The questionnaire allows submission", () => { - $(SubmitPage.submit()).click(); - expect(browser.getUrl()).to.contain(ThankYouPage.url()); - }); - }); -}); diff --git a/tests/functional/spec/list_collector_variants_primary_person.spec.js b/tests/functional/spec/list_collector_variants_primary_person.spec.js deleted file mode 100644 index 79dfbc3403..0000000000 --- a/tests/functional/spec/list_collector_variants_primary_person.spec.js +++ /dev/null @@ -1,121 +0,0 @@ -import VariantBlockPage from "../generated_pages/list_collector_variants_primary_person/variant-block.page"; -import PrimaryPersonListCollectorPage from "../generated_pages/list_collector_variants_primary_person/primary-person-list-collector.page"; -import ListCollectorAddPage from "../generated_pages/list_collector_variants_primary_person/list-collector-add.page"; -import ListCollectorPage from "../generated_pages/list_collector_variants_primary_person/list-collector.page"; -import EditPersonPage from "../generated_pages/list_collector_variants_primary_person/list-collector-edit.page"; -import SubmitPage from "../generated_pages/list_collector_variants_primary_person/submit.page"; -import ThankYouPage from "../base_pages/thank-you.page.js"; - -describe("List collector with variants primary person", () => { - describe("Given that person lives in house", () => { - before("Load the survey", () => { - browser.openQuestionnaire("test_list_collector_variants_primary_person.json"); - it("When the user is asked questions about whether they like variant, Then they are routed to section asking if they live in the house", () => { - $(VariantBlockPage.yes()).click(); - $(VariantBlockPage.submit()).click(); - expect($(PrimaryPersonListCollectorPage.legend()).getText()).to.contain("Do you live here? (variant)"); - }); - }); - }); - describe("Given the user starts on the 'Do you like variant' question", () => { - before("Load the survey", () => { - browser.openQuestionnaire("test_list_collector_variants_primary_person.json"); - }); - it("When the user says that they do live there, Then they are shown as the primary person", () => { - $(VariantBlockPage.yes()).click(); - $(VariantBlockPage.submit()).click(); - $(PrimaryPersonListCollectorPage.youLiveHereYes()).click(); - $(PrimaryPersonListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("John"); - $(ListCollectorAddPage.lastName()).setValue("Doe"); - $(ListCollectorAddPage.submit()).click(); - expect($(ListCollectorPage.listLabel(1)).getText()).to.equal("John Doe (You)"); - }); - it("When the user adds another person, Then they are shown in the list collector summary", () => { - $(ListCollectorPage.yesLabel()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("Samuel"); - $(ListCollectorAddPage.lastName()).setValue("Clemens"); - $(ListCollectorAddPage.submit()).click(); - expect($(ListCollectorPage.listLabel(2)).getText()).to.equal("Samuel Clemens"); - }); - it("When the user goes back and answers 'No' for 'Do you live here' question, Then the primary person is not shown", () => { - $(ListCollectorPage.previous()).click(); - $(PrimaryPersonListCollectorPage.youLiveHereNo()).click(); - $(PrimaryPersonListCollectorPage.submit()).click(); - expect($(ListCollectorPage.listLabel(1)).getText()).to.equal("Samuel Clemens"); - }); - - it("When the user adds another person, Then the user is able to add members of the household", () => { - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - expect($(ListCollectorAddPage.questionText()).getText()).to.equal("What is the name of the person?"); - $(ListCollectorAddPage.firstName()).setValue("Samuel"); - $(ListCollectorAddPage.lastName()).setValue("Clemens"); - $(ListCollectorAddPage.submit()).click(); - }); - it("When the user adds the primary person again, Then the primary person is first in the list", () => { - $(ListCollectorPage.previous()).click(); - $(PrimaryPersonListCollectorPage.youLiveHereYes()).click(); - $(PrimaryPersonListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("Mark"); - $(ListCollectorAddPage.lastName()).setValue("Twin"); - $(ListCollectorAddPage.submit()).click(); - expect($(ListCollectorPage.listLabel(1)).getText()).to.equal("Mark Twin (You)"); - }); - it("When the user views the summary, Then it does not show the remove link for the primary person", () => { - expect($(ListCollectorPage.listRemoveLink(1)).isExisting()).to.be.false; - expect($(ListCollectorPage.listRemoveLink(2)).isExisting()).to.be.true; - }); - it("When the user changes the primary person's name on the summary, Then the name should be updated", () => { - $(ListCollectorPage.listEditLink(1)).click(); - $(EditPersonPage.firstName()).setValue("John"); - $(EditPersonPage.lastName()).setValue("Doe"); - $(EditPersonPage.submit()).click(); - expect($(ListCollectorPage.listLabel(1)).getText()).to.equal("John Doe (You)"); - expect($(ListCollectorPage.listLabel(2)).getText()).to.equal("Samuel Clemens"); - }); - - it("When the user answers 'no' to add any person, Then the questionnaire shows the confirmation page", () => { - $(ListCollectorPage.no()).click(); - $(ListCollectorPage.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.url()); - }); - - it("When the user attempts to submit, Then they are shown the confirmation page", () => { - expect($(SubmitPage.guidance()).getText()).to.contain("Please submit this survey to complete it"); - }); - - it("When user updates the variant answer, Then it should come back to summary screen with updated answer", () => { - $(SubmitPage.variantAnswerEdit()).click(); - $(VariantBlockPage.no()).click(); - $(VariantBlockPage.submit()).click(); - expect($(SubmitPage.variantAnswer()).getText()).to.equal("No"); - }); - - it("When the user submits, Then they are allowed to submit the survey", () => { - $(SubmitPage.submit()).click(); - expect(browser.getUrl()).to.contain(ThankYouPage.pageName); - }); - }); -}); - -describe("Given the user starts on the 'Do you like variant' question", () => { - before("Load the survey", () => { - browser.openQuestionnaire("test_list_collector_variants_primary_person.json"); - }); - it("When the user answers 'No' for variant question, Then they are routed to section asking if they live in the house", () => { - $(VariantBlockPage.no()).click(); - $(VariantBlockPage.submit()).click(); - expect($(PrimaryPersonListCollectorPage.legend()).getText()).to.contain("Do you live here?"); - }); - - it("When the user says they do not live there and anyone else, Then confirmation screen is displayed", () => { - $(PrimaryPersonListCollectorPage.youLiveHereNo()).click(); - $(PrimaryPersonListCollectorPage.submit()).click(); - $(ListCollectorPage.no()).click(); - $(ListCollectorPage.submit()).click(); - - expect($(SubmitPage.guidance()).getText()).to.contain("Please submit this survey to complete it"); - }); -}); diff --git a/tests/functional/spec/mobile_number.spec.js b/tests/functional/spec/mobile_number.spec.js index 786468dd23..f3ab63b2c3 100644 --- a/tests/functional/spec/mobile_number.spec.js +++ b/tests/functional/spec/mobile_number.spec.js @@ -1,23 +1,24 @@ import MobileNumberBlockPage from "../generated_pages/mobile_number/mobile-number-block.page"; import submitPage from "../generated_pages/mobile_number/submit.page"; +import { click } from "../helpers"; describe("Mobile number validation", () => { - beforeEach("Load the survey", () => { - browser.openQuestionnaire("test_mobile_number.json"); + beforeEach("Load the survey", async () => { + await browser.openQuestionnaire("test_mobile_number.json"); }); - it("Given I am asked to enter Mobile no, When I enter a valid mobile number with no prefix and submit, Then confirmation section is displayed with entered mobile number", () => { - $(MobileNumberBlockPage.mobileNumber()).setValue(7712345678); - $(MobileNumberBlockPage.submit()).click(); - expect($(submitPage.mobileNumberAnswer()).getText()).to.contain("7712345678"); + it("Given I am asked to enter Mobile no, When I enter a valid mobile number with no prefix and submit, Then confirmation section is displayed with entered mobile number", async () => { + await $(MobileNumberBlockPage.mobileNumber()).setValue(7712345678); + await click(MobileNumberBlockPage.submit()); + await expect(await $(submitPage.mobileNumberAnswer()).getText()).toBe("7712345678"); }); - it("Given I am asked to enter Mobile no, When I enter a valid mobile number with prefix (+44) and submit, Then confirmation section is displayed with entered mobile number", () => { - $(MobileNumberBlockPage.mobileNumber()).setValue("+447712345678"); - $(MobileNumberBlockPage.submit()).click(); - expect($(submitPage.mobileNumberAnswer()).getText()).to.contain("+447712345678"); + it("Given I am asked to enter Mobile no, When I enter a valid mobile number with prefix (+44) and submit, Then confirmation section is displayed with entered mobile number", async () => { + await $(MobileNumberBlockPage.mobileNumber()).setValue("+447712345678"); + await click(MobileNumberBlockPage.submit()); + await expect(await $(submitPage.mobileNumberAnswer()).getText()).toBe("+447712345678"); }); - it("Given I am asked to enter Mobile no, When I enter an invalid mobile number and submit, Then an error screen with invalid number information is displayed", () => { - $(MobileNumberBlockPage.mobileNumber()).setValue("12345678"); - $(MobileNumberBlockPage.submit()).click(); - expect($("body").getText()).to.contain("Enter a UK mobile number in a valid format"); + it("Given I am asked to enter Mobile no, When I enter an invalid mobile number and submit, Then an error screen with invalid number information is displayed", async () => { + await $(MobileNumberBlockPage.mobileNumber()).setValue("12345678"); + await click(MobileNumberBlockPage.submit()); + await expect(await $("body").getText()).toContain("Enter a UK mobile number in a valid format"); }); }); diff --git a/tests/functional/spec/multiple_answers.spec.js b/tests/functional/spec/multiple_answers.spec.js index d2803d3d40..20d4fb455e 100644 --- a/tests/functional/spec/multiple_answers.spec.js +++ b/tests/functional/spec/multiple_answers.spec.js @@ -1,87 +1,88 @@ import AboutYou from "../generated_pages/multiple_answers/about-you-block.page"; import AgeBlock from "../generated_pages/multiple_answers/age-block.page"; import SubmitPage from "../generated_pages/multiple_answers/submit.page.js"; +import { click, verifyUrlContains } from "../helpers"; -function answerAllQuestions() { - $(AboutYou.textfield()).setValue("John Doe"); - $(AboutYou.dateday()).setValue("1"); - $(AboutYou.datemonth()).setValue("1"); - $(AboutYou.dateyear()).setValue("1995"); - $(AboutYou.checkboxBmw()).click(); - $(AboutYou.radioYes()).click(); - $(AboutYou.currency()).setValue("50000"); - $(AboutYou.monthYearDateMonth()).setValue("10"); - $(AboutYou.monthYearDateYear()).setValue("2021"); - $(AboutYou.dropdown()).selectByAttribute("value", "Silver"); - $(AboutYou.unit()).setValue("10000"); - $(AboutYou.durationMonths()).setValue("3"); - $(AboutYou.durationYears()).setValue("3"); - $(AboutYou.yearDateYear()).setValue("2019"); - $(AboutYou.number()).setValue("5"); - $(AboutYou.percentage()).setValue("3"); - $(AboutYou.mobileNumber()).setValue("07700900111"); - $(AboutYou.textarea()).setValue("Fuel type petrol"); - $(AboutYou.submit()).click(); +async function answerAllQuestions() { + await $(AboutYou.textfield()).setValue("John Doe"); + await $(AboutYou.dateday()).setValue("1"); + await $(AboutYou.datemonth()).setValue("1"); + await $(AboutYou.dateyear()).setValue("1995"); + await $(AboutYou.checkboxBmw()).click(); + await $(AboutYou.radioYes()).click(); + await $(AboutYou.currency()).setValue("50000"); + await $(AboutYou.monthYearDateMonth()).setValue("10"); + await $(AboutYou.monthYearDateYear()).setValue("2021"); + await $(AboutYou.dropdown()).selectByAttribute("value", "Silver"); + await $(AboutYou.unit()).setValue("10000"); + await $(AboutYou.durationMonths()).setValue("3"); + await $(AboutYou.durationYears()).setValue("3"); + await $(AboutYou.yearDateYear()).setValue("2019"); + await $(AboutYou.number()).setValue("5"); + await $(AboutYou.percentage()).setValue("3"); + await $(AboutYou.mobileNumber()).setValue("07700900111"); + await $(AboutYou.textarea()).setValue("Fuel type petrol"); + await click(AboutYou.submit()); - $(AgeBlock.age()).setValue("10"); - $(AgeBlock.ageEstimateThisAgeIsAnEstimate()).click(); - $(AgeBlock.submit()).click(); + await $(AgeBlock.age()).setValue("10"); + await $(AgeBlock.ageEstimateThisAgeIsAnEstimate()).click(); + await click(AgeBlock.submit()); } describe("Multiple Answers", () => { describe("Given I have completed a questionnaire that has multiple answers per question", () => { - beforeEach("Load the questionnaire and answer all questions", () => { - browser.openQuestionnaire("test_multiple_answers.json"); - answerAllQuestions(); + beforeEach("Load the questionnaire and answer all questions", async () => { + await browser.openQuestionnaire("test_multiple_answers.json"); + await answerAllQuestions(); }); - it("When I am on the summary, Then all answers are displayed", () => { - expect($(SubmitPage.textfieldAnswer()).getText()).to.equal("John Doe"); - expect($(SubmitPage.dateAnswer()).getText()).to.equal("1 January 1995"); - expect($(SubmitPage.checkboxAnswer()).getText()).to.equal("BMW"); - expect($(SubmitPage.radioAnswer()).getText()).to.equal("Yes"); - expect($(SubmitPage.currencyAnswer()).getText()).to.equal("ÂŖ50,000.00"); - expect($(SubmitPage.monthYearDateAnswer()).getText()).to.equal("October 2021"); - expect($(SubmitPage.dropdownAnswer()).getText()).to.equal("Silver"); - expect($(SubmitPage.unitAnswer()).getText()).to.equal("10,000 mi"); - expect($(SubmitPage.durationAnswer()).getText()).to.equal("3 years 3 months"); - expect($(SubmitPage.yearDateAnswer()).getText()).to.equal("2019"); - expect($(SubmitPage.numberAnswer()).getText()).to.equal("5"); - expect($(SubmitPage.percentageAnswer()).getText()).to.equal("3%"); - expect($(SubmitPage.mobileNumberAnswer()).getText()).to.equal("07700900111"); - expect($(SubmitPage.textareaAnswer()).getText()).to.equal("Fuel type petrol"); + it("When I am on the summary, Then all answers are displayed", async () => { + await expect(await $(SubmitPage.textfieldAnswer()).getText()).toBe("John Doe"); + await expect(await $(SubmitPage.dateAnswer()).getText()).toBe("1 January 1995"); + await expect(await $(SubmitPage.checkboxAnswer()).getText()).toBe("BMW"); + await expect(await $(SubmitPage.radioAnswer()).getText()).toBe("Yes"); + await expect(await $(SubmitPage.currencyAnswer()).getText()).toBe("ÂŖ50,000.00"); + await expect(await $(SubmitPage.monthYearDateAnswer()).getText()).toBe("October 2021"); + await expect(await $(SubmitPage.dropdownAnswer()).getText()).toBe("Silver"); + await expect(await $(SubmitPage.unitAnswer()).getText()).toBe("10,000 mi"); + await expect(await $(SubmitPage.durationAnswer()).getText()).toBe("3 years 3 months"); + await expect(await $(SubmitPage.yearDateAnswer()).getText()).toBe("2019"); + await expect(await $(SubmitPage.numberAnswer()).getText()).toBe("5"); + await expect(await $(SubmitPage.percentageAnswer()).getText()).toBe("3%"); + await expect(await $(SubmitPage.mobileNumberAnswer()).getText()).toBe("07700900111"); + await expect(await $(SubmitPage.textareaAnswer()).getText()).toBe("Fuel type petrol"); - expect($(SubmitPage.ageAnswer()).getText()).to.equal("10"); - expect($(SubmitPage.ageEstimateAnswer()).getText()).to.equal("This age is an estimate"); + await expect(await $(SubmitPage.ageAnswer()).getText()).toBe("10"); + await expect(await $(SubmitPage.ageEstimateAnswer()).getText()).toBe("This age is an estimate"); }); - it("When I click 'Change' an answer, Then I should be taken to the correct page and the answer input should be focused", () => { - $(SubmitPage.currencyAnswerEdit()).click(); - expect(browser.getUrl()).to.contain(AboutYou.url()); - expect(browser.getUrl()).to.contain(AboutYou.currency()); - expect($(AboutYou.currency()).isFocused()).to.be.true; + it("When I click 'Change' an answer, Then I should be taken to the correct page and the answer input should be focused", async () => { + await $(SubmitPage.currencyAnswerEdit()).click(); + await verifyUrlContains(AboutYou.url()); + await verifyUrlContains(AboutYou.currency()); + await expect(await $(AboutYou.currency()).isFocused()).toBe(true); }); }); describe("Given I have launched a questionnaire that has multiple answers per question", () => { - beforeEach("Load the questionnaire", () => { - browser.openQuestionnaire("test_multiple_answers.json"); + beforeEach("Load the questionnaire", async () => { + await browser.openQuestionnaire("test_multiple_answers.json"); }); - it("When I am on the question page, Then all answers should have a label/legend", () => { - expect($(AboutYou.dateLegend()).getText()).to.equal("What is your date of birth?"); - expect($(AboutYou.monthYearDateLegend()).getText()).to.equal("When would you like the car by?"); - expect($(AboutYou.radioLegend()).getText()).to.equal("Would you like the sports package?"); - expect($(AboutYou.durationLegend()).getText()).to.equal("How long have you had your licence?"); - expect($(AboutYou.checkboxLegend()).getText()).to.equal("What are your favourite car brands?"); - expect($(AboutYou.textfieldLabel()).getText()).to.equal("Your name"); - expect($(AboutYou.currencyLabel()).getText()).to.equal("What is your budget?"); - expect($(AboutYou.dropdownLabel()).getText()).to.equal("Select a colour"); - expect($(AboutYou.unitLabel()).getText()).to.equal("Max mileage"); - expect($(AboutYou.numberLabel()).getText()).to.equal("How many seats?"); - expect($(AboutYou.percentageLabel()).getText()).to.equal("Max CO2 emissions"); - expect($(AboutYou.mobileNumberLabel()).getText()).to.equal("What is your mobile number?"); - expect($(AboutYou.textareaLabel()).getText()).to.equal("Other comments"); + it("When I am on the question page, Then all answers should have a label/legend", async () => { + await expect(await $(AboutYou.dateLegend()).getText()).toBe("What is your date of birth?"); + await expect(await $(AboutYou.monthYearDateLegend()).getText()).toBe("When would you like the car by?"); + await expect(await $(AboutYou.radioLegend()).getText()).toBe("Would you like the sports package?"); + await expect(await $(AboutYou.durationLegend()).getText()).toBe("How long have you had your licence?"); + await expect(await $(AboutYou.checkboxLegend()).getText()).toBe("What are your favourite car brands?"); + await expect(await $(AboutYou.textfieldLabel()).getText()).toBe("Your name"); + await expect(await $(AboutYou.currencyLabel()).getText()).toBe("What is your budget?"); + await expect(await $(AboutYou.dropdownLabel()).getText()).toBe("Select a colour"); + await expect(await $(AboutYou.unitLabel()).getText()).toBe("Max mileage"); + await expect(await $(AboutYou.numberLabel()).getText()).toBe("How many seats?"); + await expect(await $(AboutYou.percentageLabel()).getText()).toBe("Max CO2 emissions"); + await expect(await $(AboutYou.mobileNumberLabel()).getText()).toBe("What is your mobile number?"); + await expect(await $(AboutYou.textareaLabel()).getText()).toBe("Other comments"); }); }); }); diff --git a/tests/functional/spec/multiple_piping.spec.js b/tests/functional/spec/multiple_piping.spec.js index 74006d85d3..6520d65f72 100644 --- a/tests/functional/spec/multiple_piping.spec.js +++ b/tests/functional/spec/multiple_piping.spec.js @@ -1,34 +1,35 @@ import AddressPage from "../generated_pages/multiple_piping/what-is-your-address.page"; import TextfieldPage from "../generated_pages/multiple_piping/textfield.page"; import MultiplePipingPage from "../generated_pages/multiple_piping/piping-question.page"; +import { click } from "../helpers"; describe("Piping", () => { const pipingSchema = "test_multiple_piping.json"; describe("Multiple piping into question and answer", () => { - beforeEach("load the survey", () => { - browser.openQuestionnaire(pipingSchema); + beforeEach("load the survey", async () => { + await browser.openQuestionnaire(pipingSchema); }); - it("Given I enter multiple fields in one question, When I navigate to the multiple piping answer, Then I should see all values piped into an answer", () => { - $(AddressPage.addressLine1()).setValue("1 The ONS"); - $(AddressPage.townCity()).setValue("Newport"); - $(AddressPage.postcode()).setValue("NP10 8XG"); - $(AddressPage.country()).setValue("Wales"); - $(AddressPage.submit()).click(); - $(TextfieldPage.firstText()).setValue("Fireman"); - $(TextfieldPage.secondText()).setValue("Sam"); - $(TextfieldPage.submit()).click(); - expect($(MultiplePipingPage.answerAddressLabel()).getText()).to.contain("1 The ONS, Newport, NP10 8XG, Wales"); + it("Given I enter multiple fields in one question, When I navigate to the multiple piping answer, Then I should see all values piped into an answer", async () => { + await $(AddressPage.addressLine1()).setValue("1 The ONS"); + await $(AddressPage.townCity()).setValue("Newport"); + await $(AddressPage.postcode()).setValue("NP10 8XG"); + await $(AddressPage.country()).setValue("Wales"); + await click(AddressPage.submit()); + await $(TextfieldPage.firstText()).setValue("Fireman"); + await $(TextfieldPage.secondText()).setValue("Sam"); + await click(TextfieldPage.submit()); + await expect(await $(MultiplePipingPage.answerAddressLabel()).getText()).toBe("1 The ONS, Newport, NP10 8XG, Wales"); }); - it("Given I enter values in multiple questions, When I navigate to the multiple piping question, Then I should see both values piped into the question", () => { - $(AddressPage.addressLine1()).setValue("1 The ONS"); - $(AddressPage.submit()).click(); - $(TextfieldPage.firstText()).setValue("Fireman"); - $(TextfieldPage.secondText()).setValue("Sam"); - $(TextfieldPage.submit()).click(); - expect($(MultiplePipingPage.questionText()).getText()).to.contain("Does Fireman Sam live at 1 The ONS"); + it("Given I enter values in multiple questions, When I navigate to the multiple piping question, Then I should see both values piped into the question", async () => { + await $(AddressPage.addressLine1()).setValue("1 The ONS"); + await click(AddressPage.submit()); + await $(TextfieldPage.firstText()).setValue("Fireman"); + await $(TextfieldPage.secondText()).setValue("Sam"); + await click(TextfieldPage.submit()); + await expect(await $(MultiplePipingPage.questionText()).getText()).toBe("Does Fireman Sam live at 1 The ONS"); }); }); }); diff --git a/tests/functional/spec/my_account_header_link.spec.js b/tests/functional/spec/my_account_header_link.spec.js index 48721d762c..97f318a2e8 100644 --- a/tests/functional/spec/my_account_header_link.spec.js +++ b/tests/functional/spec/my_account_header_link.spec.js @@ -1,9 +1,11 @@ import IntroductionPage from "../generated_pages/introduction/introduction.page"; +import { verifyUrlContains } from "../helpers"; describe("My Account header link", () => { - it("Given I start a survey, When I visit a page then I should not see the My account button", () => { - browser.openQuestionnaire("test_introduction.json"); - expect(browser.getUrl()).to.contain("introduction"); - expect($(IntroductionPage.myAccountLink()).isExisting()).to.be.false; + it("Given I start a survey, When I visit a page then I should not see the My account button", async () => { + await browser.openQuestionnaire("test_introduction.json"); + await browser.pause(100); + await verifyUrlContains("introduction"); + await expect(await $(IntroductionPage.myAccountLink()).isExisting()).toBe(false); }); }); diff --git a/tests/functional/spec/numbers.spec.js b/tests/functional/spec/numbers.spec.js index 6c442629f1..00025eef14 100644 --- a/tests/functional/spec/numbers.spec.js +++ b/tests/functional/spec/numbers.spec.js @@ -2,97 +2,131 @@ import SetMinMax from "../generated_pages/numbers/set-min-max-block.page.js"; import TestMinMax from "../generated_pages/numbers/test-min-max-block.page.js"; import DetailAnswer from "../generated_pages/numbers/detail-answer-block.page"; import SubmitPage from "../generated_pages/numbers/submit.page"; +import currencyBlock from "../generated_pages/variants_question/currency-block.page.js"; +import firstNumberBlock from "../generated_pages/variants_question/first-number-block.page.js"; +import secondNumberBlock from "../generated_pages/variants_question/second-number-block.page.js"; +import currencySectionSummary from "../generated_pages/variants_question/currency-section-summary.page.js"; +import { click, verifyUrlContains } from "../helpers"; describe("Number validation", () => { - before(() => { - browser.openQuestionnaire("test_numbers.json"); + before(async () => { + await browser.openQuestionnaire("test_numbers.json"); }); + describe("Given I am completing the test numbers questionnaire,", () => { - it("When I am on the set minimum and maximum page, Then each field has a label", () => { - expect($(SetMinMax.setMinimumLabelDescription()).getText()).to.contain("This is a description of the minimum value"); - expect($(SetMinMax.setMaximumLabelDescription()).getText()).to.contain("This is a description of the maximum value"); + it("When a minimum value with decimals is used and I enter a value less than the minimum, Then the error message includes the minimum value with the decimals values", async () => { + await $(SetMinMax.setMinimum()).setValue(-1000.99); + await $(SetMinMax.setMaximum()).setValue(1000); + await click(SetMinMax.submit()); + await expect(await $(SetMinMax.errorNumber(1)).getText()).toBe("Enter an answer more than or equal to -1,000.98"); + }); + + it("When a maximum value with decimals is used and I enter a value greater than the maximum, Then the error message includes the minimum value with the decimal values", async () => { + await $(SetMinMax.setMinimum()).setValue(100); + await $(SetMinMax.setMaximum()).setValue(10000.99); + await click(SetMinMax.submit()); + await expect(await $(SetMinMax.errorNumber(1)).getText()).toBe("Enter an answer less than or equal to 10,000.98"); }); - it("When I enter values outside of the set range, Then the correct error messages are displayed", () => { - $(SetMinMax.setMinimum()).setValue("10"); - $(SetMinMax.setMaximum()).setValue("1020"); - $(SetMinMax.submit()).click(); - - $(TestMinMax.testRange()).setValue("9"); - $(TestMinMax.testRangeExclusive()).setValue("10"); - $(TestMinMax.testMin()).setValue("0"); - $(TestMinMax.testMax()).setValue("12345"); - $(TestMinMax.testMinExclusive()).setValue("123"); - $(TestMinMax.testMaxExclusive()).setValue("12345"); - $(TestMinMax.testPercent()).setValue("101"); - $(TestMinMax.testDecimal()).setValue("5.4"); - $(TestMinMax.submit()).click(); - - expect($(TestMinMax.errorNumber(1)).getText()).to.contain("Enter an answer more than or equal to 10"); - expect($(TestMinMax.errorNumber(2)).getText()).to.contain("Enter an answer more than 10"); - expect($(TestMinMax.errorNumber(3)).getText()).to.contain("Enter an answer more than or equal to 123"); - expect($(TestMinMax.errorNumber(4)).getText()).to.contain("Enter an answer less than or equal to 1,234"); - expect($(TestMinMax.errorNumber(5)).getText()).to.contain("Enter an answer more than 123"); - expect($(TestMinMax.errorNumber(6)).getText()).to.contain("Enter an answer less than 1,234"); - expect($(TestMinMax.errorNumber(7)).getText()).to.contain("Enter an answer less than or equal to 100"); - expect($(TestMinMax.errorNumber(8)).getText()).to.contain("Enter an answer more than or equal to ÂŖ10.00"); + it("When I am on the set minimum and maximum page, Then each field has a label", async () => { + await expect(await $(SetMinMax.setMinimumLabelDescription()).getText()).toBe("This is a description of the minimum value"); + await expect(await $(SetMinMax.setMaximumLabelDescription()).getText()).toBe("This is a description of the maximum value"); }); - it("When I enter values inside the set range but provide too many decimal places, Then the correct error messages are displayed", () => { - $(TestMinMax.testRange()).setValue("1020"); - $(TestMinMax.testRangeExclusive()).setValue("11"); - $(TestMinMax.testMin()).setValue("123"); - $(TestMinMax.testMax()).setValue("1019"); - $(TestMinMax.testMinExclusive()).setValue("124"); - $(TestMinMax.testMaxExclusive()).setValue("1233"); - $(TestMinMax.testPercent()).setValue("100"); - $(TestMinMax.testRange()).setValue("12.344"); - $(TestMinMax.testDecimal()).setValue("11.234"); - $(TestMinMax.submit()).click(); - - expect($(TestMinMax.errorNumber(1)).getText()).to.contain("Enter a number rounded to 2 decimal places"); - expect($(TestMinMax.errorNumber(2)).getText()).to.contain("Enter a number rounded to 2 decimal places"); + it("When I enter values outside of the set range, Then the correct error messages are displayed", async () => { + await $(SetMinMax.setMinimum()).setValue("10"); + await $(SetMinMax.setMaximum()).setValue("1020"); + await click(SetMinMax.submit()); + + await $(TestMinMax.testRange()).setValue("9"); + await $(TestMinMax.testRangeExclusive()).setValue("10"); + await $(TestMinMax.testMin()).setValue("-124"); + await $(TestMinMax.testMax()).setValue("12345"); + await $(TestMinMax.testMinExclusive()).setValue("123"); + await $(TestMinMax.testMaxExclusive()).setValue("12345"); + await $(TestMinMax.testPercent()).setValue("101"); + await $(TestMinMax.testDecimal()).setValue("5.4"); + await click(TestMinMax.submit()); + + await expect(await $(TestMinMax.errorNumber(1)).getText()).toBe("Enter an answer more than or equal to 10"); + await expect(await $(TestMinMax.errorNumber(2)).getText()).toBe("Enter an answer more than 10"); + await expect(await $(TestMinMax.errorNumber(3)).getText()).toBe("Enter an answer more than or equal to -123"); + await expect(await $(TestMinMax.errorNumber(4)).getText()).toBe("Enter an answer less than or equal to 1,234"); + await expect(await $(TestMinMax.errorNumber(5)).getText()).toBe("Enter an answer more than 123"); + await expect(await $(TestMinMax.errorNumber(6)).getText()).toBe("Enter an answer less than 1,234"); + await expect(await $(TestMinMax.errorNumber(7)).getText()).toBe("Enter an answer less than or equal to 100"); + await expect(await $(TestMinMax.errorNumber(8)).getText()).toBe("Enter an answer more than or equal to ÂŖ10.00"); }); - it("When I enter values inside the set range, Then I should be able to submit the survey", () => { - $(TestMinMax.testRange()).setValue("12"); - $(TestMinMax.testDecimal()).setValue("11.23"); - $(TestMinMax.testPercent()).setValue("99"); - $(TestMinMax.submit()).click(); - $(DetailAnswer.other()).click(); - $(DetailAnswer.otherDetail()).setValue("1020"); - $(DetailAnswer.submit()).click(); + it("When I enter values inside the set range but provide too many decimal places, Then the correct error messages are displayed", async () => { + await $(TestMinMax.testRange()).setValue("12.344"); + await $(TestMinMax.testRangeExclusive()).setValue("11"); + await $(TestMinMax.testMin()).setValue("123"); + await $(TestMinMax.testMax()).setValue("1019"); + await $(TestMinMax.testMinExclusive()).setValue("124"); + await $(TestMinMax.testMaxExclusive()).setValue("1233"); + await $(TestMinMax.testPercent()).setValue("100"); + await $(TestMinMax.testRange()).setValue("12.123456"); + await $(TestMinMax.testDecimal()).setValue("11.123456"); + await click(TestMinMax.submit()); + + await expect(await $(TestMinMax.errorNumber(1)).getText()).toBe("Enter a number rounded to 2 decimal places"); + await expect(await $(TestMinMax.errorNumber(2)).getText()).toBe("Enter a number rounded to 5 decimal places"); + }); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); + it("When I enter values inside the set range, Then I should be able to submit the survey", async () => { + await $(TestMinMax.testRange()).setValue("1019"); + await $(TestMinMax.testDecimal()).setValue("11.10000"); + await $(TestMinMax.testPercent()).setValue("99"); + await click(TestMinMax.submit()); + await $(DetailAnswer.other()).click(); + await $(DetailAnswer.otherDetail()).setValue("1019"); + await click(TestMinMax.submit()); + await $(currencyBlock.usDollars()).click(); + await click(currencyBlock.submit()); + await $(firstNumberBlock.firstNumber()).setValue("50"); + await click(firstNumberBlock.submit()); + await $(secondNumberBlock.secondNumber()).setValue("321"); + await click(secondNumberBlock.submit()); + await click(currencySectionSummary.submit()); + + await verifyUrlContains(SubmitPage.pageName); }); - it("When I edit and change the maximum value, Then I must re-validate and submit any dependent answers before I can return to the summary", () => { - $(SubmitPage.setMaximumEdit()).click(); - $(SetMinMax.setMaximum()).setValue("1019"); - $(SetMinMax.submit()).click(); - $(TestMinMax.submit()).click(); - $(DetailAnswer.submit()).click(); + it("When I edit and change the maximum value, Then I must re-validate and submit any dependent answers before I can return to the summary", async () => { + await $(SubmitPage.setMaximumEdit()).click(); + await $(SetMinMax.setMaximum()).setValue("1018"); + await click(SetMinMax.submit()); + await $(TestMinMax.testRange()).setValue("1018"); + await click(TestMinMax.submit()); + await click(DetailAnswer.submit()); - expect($(DetailAnswer.errorNumber(1)).getText()).to.contain("Enter an answer less than or equal to 1,019"); + await expect(await $(DetailAnswer.errorNumber(1)).getText()).toBe("Enter an answer less than or equal to 1,018"); - $(DetailAnswer.otherDetail()).setValue("1019"); - $(DetailAnswer.submit()).click(); + await $(DetailAnswer.otherDetail()).setValue("1001"); + await click(DetailAnswer.submit()); + await click(secondNumberBlock.submit()); + await click(currencySectionSummary.submit()); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); + await verifyUrlContains(SubmitPage.pageName); }); - it("When I edit and change the minimum value, Then I must re-validate and submit any dependent answers again before I can return to the summary", () => { - $(SubmitPage.setMinimumEdit()).click(); - $(SetMinMax.setMinimum()).setValue("11"); - $(SetMinMax.submit()).click(); - $(TestMinMax.submit()).click(); + it("When I edit and change the minimum value, Then I must re-validate and submit any dependent answers again before I can return to the summary", async () => { + await $(SubmitPage.setMinimumEdit()).click(); + await $(SetMinMax.setMinimum()).setValue("11"); + await click(SetMinMax.submit()); + await click(TestMinMax.submit()); + + await expect(await $(TestMinMax.errorNumber(1)).getText()).toBe("Enter an answer more than 11"); - expect($(TestMinMax.errorNumber(1)).getText()).to.contain("Enter an answer more than 11"); + await $(TestMinMax.testRangeExclusive()).setValue("12"); + await click(TestMinMax.submit()); - $(TestMinMax.testRangeExclusive()).setValue("12"); - $(TestMinMax.submit()).click(); + await verifyUrlContains(SubmitPage.pageName); + }); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); + it("When a number with more than 3 decimal places has been entered, Then it should be displayed correctly on the summary", async () => { + await expect(await $(SubmitPage.testDecimal()).getText()).toBe("ÂŖ11.10000"); }); }); }); diff --git a/tests/functional/spec/page_layout.spec.js b/tests/functional/spec/page_layout.spec.js new file mode 100644 index 0000000000..b28c0752ec --- /dev/null +++ b/tests/functional/spec/page_layout.spec.js @@ -0,0 +1,10 @@ +import HubPage from "../base_pages/hub.page"; + +describe("Page Layout", () => { + it("Given a page in the questionnaire, When I visit the page, Then the page width should be as expected", async () => { + await browser.url(HubPage.url()); + + const cssWidthSelector = await $('div[class*="ons-col-"][class*="@m"]').getAttribute("class"); + await expect(cssWidthSelector).toContain("ons-col-8@m"); + }); +}); diff --git a/tests/functional/spec/percentage_decimal.spec.js b/tests/functional/spec/percentage_decimal.spec.js new file mode 100644 index 0000000000..63b582668b --- /dev/null +++ b/tests/functional/spec/percentage_decimal.spec.js @@ -0,0 +1,26 @@ +import PercentagePage from "../generated_pages/percentage/block.page.js"; +import PercentageDecimalPage from "../generated_pages/percentage/block-decimal.page.js"; +import SubmitPage from "../generated_pages/percentage/submit.page.js"; +import { click, verifyUrlContains } from "../helpers"; + +describe("Decimal places", () => { + it("Given an answer allows 3 decimal places, When I enter a value to 3 decimal places and return to edit the value, Then the answer should be displayed with 3 decimal places", async () => { + await browser.openQuestionnaire("test_percentage.json"); + await click(PercentagePage.submit()); + await $(PercentageDecimalPage.decimal()).setValue("3.333"); + await click(PercentageDecimalPage.submit()); + await $(SubmitPage.previous()).click(); + await verifyUrlContains(PercentageDecimalPage.pageName); + await expect(await $(PercentageDecimalPage.decimal()).getValue()).toBe("3.333"); + }); + + it("Given an answer allows 3 decimal places, When I enter a value to 1 decimal place and return to edit the value, Then the answer should be displayed with 3 decimal places", async () => { + await browser.openQuestionnaire("test_percentage.json"); + await click(PercentagePage.submit()); + await $(PercentageDecimalPage.decimal()).setValue("3.3"); + await click(PercentageDecimalPage.submit()); + await $(SubmitPage.previous()).click(); + await verifyUrlContains(PercentageDecimalPage.pageName); + await expect(await $(PercentageDecimalPage.decimal()).getValue()).toBe("3.300"); + }); +}); diff --git a/tests/functional/spec/placeholder_answers_on_the_path.spec.js b/tests/functional/spec/placeholder_answers_on_the_path.spec.js new file mode 100644 index 0000000000..66ec230d3d --- /dev/null +++ b/tests/functional/spec/placeholder_answers_on_the_path.spec.js @@ -0,0 +1,111 @@ +import DateEntryBlockPage from "../generated_pages/placeholder_first_non_empty_item/date-entry-block.page"; +import DateQuestionBlockPage from "../generated_pages/placeholder_first_non_empty_item/date-question-block.page"; +import TotalTurnoverBlockPage from "../generated_pages/placeholder_first_non_empty_item/total-turnover-block.page"; +import FoodQuestionBlockPage from "../generated_pages/placeholder_first_non_empty_item_cross_section_dependencies/food-question-block.page"; + +import AddPersonPage from "../generated_pages/placeholder_first_non_empty_item_repeating_sections/list-collector-add.page"; +import ListCollectorPage from "../generated_pages/placeholder_first_non_empty_item_repeating_sections/list-collector.page"; +import PersonalDetailsBlockPage from "../generated_pages/placeholder_first_non_empty_item_repeating_sections/personal-details-block.page"; +import HubPage from "../base_pages/hub.page.js"; +import { click, verifyUrlContains } from "../helpers"; + +describe("First Non Empty Item Transform", () => { + before("Launch survey", async () => { + await browser.openQuestionnaire("test_placeholder_first_non_empty_item.json"); + }); + + it("When the custom date range is entered and the answer is changed back to metadata date range, Then metadata date should be displayed", async () => { + // Set the date + await $(DateQuestionBlockPage.noINeedToReportForADifferentPeriod()).click(); + await click(DateQuestionBlockPage.submit()); + await $(DateEntryBlockPage.dateEntryFromday()).setValue("5"); + await $(DateEntryBlockPage.dateEntryFrommonth()).setValue("01"); + await $(DateEntryBlockPage.dateEntryFromyear()).setValue("2017"); + await $(DateEntryBlockPage.dateEntryToday()).setValue("25"); + await $(DateEntryBlockPage.dateEntryTomonth()).setValue("01"); + await $(DateEntryBlockPage.dateEntryToyear()).setValue("2017"); + await click(DateEntryBlockPage.submit()); + // Change to original dates + await $(TotalTurnoverBlockPage.previous()).click(); + await $(DateEntryBlockPage.previous()).click(); + await $(DateQuestionBlockPage.yesICanReportForThisPeriod()).click(); + await click(DateQuestionBlockPage.submit()); + await verifyUrlContains(TotalTurnoverBlockPage.pageName); + expect(await $(TotalTurnoverBlockPage.questionTitle()).getText()).toContain("1 January 2017 to 1 February 2017"); + }); +}); + +describe("First Non Empty Item Transform Cross Section", () => { + before("Launch survey", async () => { + await browser.openQuestionnaire("test_placeholder_first_non_empty_item_cross_section_dependencies.json"); + await click(HubPage.submit()); + }); + + it("Given a custom date range is entered, When the answer is changed back to metadata range, Then the metadata date should be displayed for both sections", async () => { + // Set the date + await $(DateQuestionBlockPage.noINeedToReportForADifferentPeriod()).click(); + await click(DateQuestionBlockPage.submit()); + await $(DateEntryBlockPage.dateEntryFromday()).setValue("5"); + await $(DateEntryBlockPage.dateEntryFrommonth()).setValue("01"); + await $(DateEntryBlockPage.dateEntryFromyear()).setValue("2017"); + await $(DateEntryBlockPage.dateEntryToday()).setValue("25"); + await $(DateEntryBlockPage.dateEntryTomonth()).setValue("01"); + await $(DateEntryBlockPage.dateEntryToyear()).setValue("2017"); + await click(DateEntryBlockPage.submit()); + + // Check date changed and then change to original dates + await click(HubPage.submit()); + expect(await $(FoodQuestionBlockPage.questionTitle()).getText()).toContain("5 January 2017 to 25 January 2017"); + await $(FoodQuestionBlockPage.previous()).click(); + await $(HubPage.summaryRowLink("default-section")).click(); + await $(DateQuestionBlockPage.yesICanReportForThisPeriod()).click(); + await click(DateQuestionBlockPage.submit()); + // Check the next section if the metadata date is shown + await click(HubPage.submit()); + await verifyUrlContains(FoodQuestionBlockPage.pageName); + expect(await $(FoodQuestionBlockPage.questionTitle()).getText()).toContain("1 January 2017 to 1 February 2017"); + }); +}); + +describe("First Non Empty Item Transform Repeating Sections", () => { + before("Launch survey", async () => { + await browser.openQuestionnaire("test_placeholder_first_non_empty_item_repeating_sections.json"); + await click(HubPage.submit()); + }); + it("Given a custom date range is entered, When the answer is changed back to metadata range, Then the metadata date should be displayed for the repeating section title", async () => { + // Set the date + await $(DateQuestionBlockPage.noINeedToReportForADifferentPeriod()).click(); + await click(DateQuestionBlockPage.submit()); + await $(DateEntryBlockPage.dateEntryFromday()).setValue("5"); + await $(DateEntryBlockPage.dateEntryFrommonth()).setValue("01"); + await $(DateEntryBlockPage.dateEntryFromyear()).setValue("2017"); + await $(DateEntryBlockPage.dateEntryToday()).setValue("25"); + await $(DateEntryBlockPage.dateEntryTomonth()).setValue("01"); + await $(DateEntryBlockPage.dateEntryToyear()).setValue("2017"); + await click(DateEntryBlockPage.submit()); + await click(HubPage.submit()); + + // Add a person to the list collector + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(AddPersonPage.firstName()).setValue("Paul"); + await $(AddPersonPage.lastName()).setValue("Pogba"); + await click(AddPersonPage.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await click(ListCollectorPage.submit()); + // Check Repeating Section has the set dates + await click(HubPage.submit()); + await verifyUrlContains(PersonalDetailsBlockPage.pageName); + expect(await $(PersonalDetailsBlockPage.questionTitle()).getText()).toContain("5 January 2017 to 25 January 2017"); + await $(PersonalDetailsBlockPage.previous()).click(); + // Change to original dates + await $(HubPage.summaryRowLink("date-section")).click(); + await $(DateQuestionBlockPage.yesICanReportForThisPeriod()).click(); + await click(DateQuestionBlockPage.submit()); + await click(HubPage.submit()); + // Check the list collector has metadata dates in the title + await verifyUrlContains(PersonalDetailsBlockPage.pageName); + expect(await $(PersonalDetailsBlockPage.questionTitle()).getText()).toContain("1 January 2017 to 1 February 2017"); + }); +}); diff --git a/tests/functional/spec/preview.spec.js b/tests/functional/spec/preview.spec.js new file mode 100644 index 0000000000..8700553462 --- /dev/null +++ b/tests/functional/spec/preview.spec.js @@ -0,0 +1,89 @@ +import IntroductionPageHub from "../generated_pages/introduction_hub/introduction.page"; +import IntroductionPageLinear from "../generated_pages/introduction/introduction.page"; +import { verifyUrlContains } from "../helpers"; + +describe("Introduction preview questions", () => { + const introductionSchemaHub = "test_introduction_hub.json"; + const introductionSchemaLinear = "test_introduction.json"; + const showButton = 'button[data-ga-label="Hide all"]'; + const previewSummaryContent = "#summary-accordion-1-content"; + const previewSectionTitle = ".ons-summary__group-title"; + const previewQuestion = ".ons-summary__item"; + const printButton = 'button[data-qa="btn-print"]'; + const pdfButton = 'a[data-qa="btn-pdf"]'; + // const detailsHeading = ".ons-details__heading"; + const startSurveyButton = ".qa-btn-get-started"; + const noRadio = "#report-radio-answer-1"; + const submitButton = 'button[data-qa="btn-submit"]'; + const answerFromDay = "#answer-from-day"; + const answerFromMonth = "#answer-from-month"; + const answerFromYear = "#answer-from-year"; + const answerToDay = "#answer-to-day"; + const answerToMonth = "#answer-to-month"; + const answerToYear = "#answer-to-year"; + + async function testPreview(schema, page) { + await browser.openQuestionnaire(schema); + await $(page.previewQuestions()).click(); + await verifyUrlContains("questionnaire/preview"); + if (schema === "test_introduction.json") { + expect(await $(previewSectionTitle).getText()).toBe("Main section"); + } else { + await $(showButton).click(); + } + // :TODO: Add data attributes to elements below so we don't rely on tags or classes that are subject to DS changes + expect(await $(previewQuestion).$("h3").getText()).toBe("Are you able to report for the calendar month 1 January 2017 to 1 February 2017?"); + expect(await $(previewQuestion).$(".ons-question__description").getText()).toBe("Your return should relate to the calendar year 2021."); + expect(await $(previewQuestion).$$(".ons-panel__body")[0].getText()).toBe("Please provide figures for the period in which you were trading."); + expect(await $(showButton).length).toBeUndefined(); + expect(await $(printButton).isClickable()).toBe(true); + expect(await $(pdfButton).isClickable()).toBe(true); + // answer guidance not implemented yet due to some work that needs to be done in the DS will be implemented in iteration 2 + // $(detailsHeading).click(); + // expect($(previewQuestion).$("#answer-guidance--content div p").getText()).to.equal("For example select `yes` if you can report for this period"); + expect(await $(previewQuestion).$$("p")[2].getText()).toBe("You can answer with one of the following options:"); + expect(await $(previewQuestion).$$("ul")[0].getText()).toBe("Yes\nNo"); + } + + it("Given I start a survey, When I view the preview page, Then all preview elements should be visible and any metadata piped answers are resolved", async () => { + await testPreview(introductionSchemaHub, IntroductionPageHub); + await testPreview(introductionSchemaLinear, IntroductionPageLinear); + }); + + it("Given I complete some of a survey and the piped answers should be being populated, Then preview answers should still be showing placeholders", async () => { + await browser.openQuestionnaire(introductionSchemaLinear); + await $(startSurveyButton).click(); + await $(noRadio).click(); + await $(submitButton).click(); + await $(answerFromDay).setValue(5); + await $(answerFromMonth).setValue(12); + await $(answerFromYear).setValue(2016); + await $(answerToDay).setValue(20); + await $(answerToMonth).setValue(12); + await $(answerToYear).setValue(2016); + await $(submitButton).click(); + expect(await $("h1").getText()).toBe("Are you sure you are able to report for the calendar month 5 December 2016 to 20 December 2016?"); + await browser.url("questionnaire/introduction/"); + await $(IntroductionPageLinear.previewQuestions()).click(); + await verifyUrlContains("questionnaire/preview"); + expect(await $(previewSectionTitle).getText()).toBe("Main section"); + expect(await $$(previewQuestion)[2].$("h3").getText()).toBe( + "Are you sure you are able to report for the calendar month {calendar_start_date} to {calendar_end_date}?", + ); + }); + + it("Given I start a survey, When I view the preview page of hub flow schema, Then the twisty button should read 'Show all' and answers should be invisible", async () => { + await browser.openQuestionnaire(introductionSchemaHub); + await $(IntroductionPageHub.previewQuestions()).click(); + await verifyUrlContains("questionnaire/preview"); + expect(await $(printButton).isClickable()).toBe(true); + expect(await $(pdfButton).isClickable()).toBe(true); + expect(await $(showButton).getText()).toBe("Show all"); + expect(await $(previewSummaryContent).isClickable()).toBe(false); + it("and if the twisty button is clicked, Then the twisty button should read 'Hide all' and the answers should be visible", async () => { + await $(showButton).click(); + expect(await $(showButton).getText()).toBe("Hide all"); + expect(await $(previewSummaryContent).isClickable()).toBe(true); + }); + }); +}); diff --git a/tests/functional/spec/question_definitions.spec.js b/tests/functional/spec/question_definitions.spec.js index 4984b9df9f..3c0ee372bc 100644 --- a/tests/functional/spec/question_definitions.spec.js +++ b/tests/functional/spec/question_definitions.spec.js @@ -2,64 +2,31 @@ import DefinitionPage from "../generated_pages/question_definition/definition-bl describe("Component: Definition", () => { describe("Given I start a survey which contains question definition", () => { - beforeEach(() => { - browser.openQuestionnaire("test_question_definition.json"); + beforeEach(async () => { + await browser.openQuestionnaire("test_question_definition.json"); }); - it('When I click the title link, then the description and "Hide this" button should be visible', () => { - expect($(DefinitionPage.definitionContent(1)).isDisplayed()).to.be.false; - expect($(DefinitionPage.definitionButton(1)).isDisplayed()).to.be.false; + it("When I click the title link, then the description should be visible", async () => { + await expect(await $(DefinitionPage.definitionContent()).getText()).toBe(""); // When - $(DefinitionPage.definitionTitle("1")).click(); + await $(DefinitionPage.definitionTitle()).click(); // Then - $(DefinitionPage.definitionContent(1)).waitForDisplayed({ timeout: 300 }); - $(DefinitionPage.definitionButton(1)).waitForDisplayed({ timeout: 300 }); + await expect(await $(DefinitionPage.definitionContent()).getText()).toContain( + "A typical photovoltaic system employs solar panels, each comprising a number of solar cells, which generate electrical power.", + ); }); - it('When I click the title link twice, then the description and "Hide this" button should not be visible', () => { - expect($(DefinitionPage.definitionContent(1)).isDisplayed()).to.be.false; - expect($(DefinitionPage.definitionButton(1)).isDisplayed()).to.be.false; + it("When I click the title link twice, then the description should not be visible", async () => { + await expect(await $(DefinitionPage.definitionContent()).getText()).toBe(""); // When - $(DefinitionPage.definitionTitle("1")).click(); - $(DefinitionPage.definitionTitle("1")).click(); + await $(DefinitionPage.definitionTitle()).click(); + await $(DefinitionPage.definitionTitle()).click(); // Then - $(DefinitionPage.definitionContent(1)).waitForDisplayed({ timeout: 300, reverse: true }); - $(DefinitionPage.definitionButton(1)).waitForDisplayed({ timeout: 300, reverse: true }); - }); - - it('When I click the title link then click "Hide this" button, then the description and button should not be visible', () => { - expect($(DefinitionPage.definitionContent(1)).isDisplayed()).to.be.false; - expect($(DefinitionPage.definitionButton(1)).isDisplayed()).to.be.false; - - // When - $(DefinitionPage.definitionTitle("1")).click(); - - // Then - $(DefinitionPage.definitionContent(1)).waitForDisplayed({ timeout: 300 }); - $(DefinitionPage.definitionButton(1)).waitForDisplayed({ timeout: 300 }); - - // When - $(DefinitionPage.definitionButton(1)).click(); - - // Then - $(DefinitionPage.definitionContent(1)).waitForDisplayed({ timeout: 300, reverse: true }); - $(DefinitionPage.definitionButton(1)).waitForDisplayed({ timeout: 300, reverse: true }); - }); - - it('When I click the second definition\'s title link then the description and "Hide this" button for the second definition should be visible', () => { - expect($(DefinitionPage.definitionContent(2)).isDisplayed()).to.be.false; - expect($(DefinitionPage.definitionButton(2)).isDisplayed()).to.be.false; - - // When - $(DefinitionPage.definitionTitle("2")).click(); - - // Then - $(DefinitionPage.definitionContent(2)).waitForDisplayed({ timeout: 300 }); - $(DefinitionPage.definitionButton(2)).waitForDisplayed({ timeout: 300 }); + await expect(await $(DefinitionPage.definitionContent()).getText()).toBe(""); }); }); }); diff --git a/tests/functional/spec/question_definitions_array_type.spec.js b/tests/functional/spec/question_definitions_array_type.spec.js new file mode 100644 index 0000000000..8e25937295 --- /dev/null +++ b/tests/functional/spec/question_definitions_array_type.spec.js @@ -0,0 +1,32 @@ +import DefinitionPage from "../generated_pages/question_definition/definition-block.page"; + +describe("Component: Definition", () => { + describe("Given I start a survey which contains question definition", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_question_definition_array_type.json"); + }); + + it("When I click the title link, then the description should be visible", async () => { + await expect(await $(DefinitionPage.definitionContent()).getText()).toBe(""); + + // When + await $(DefinitionPage.definitionTitle()).click(); + + // Then + await expect(await $(DefinitionPage.definitionContent()).getText()).toContain( + "A typical photovoltaic system employs solar panels, each comprising a number of solar cells, which generate electrical power.", + ); + }); + + it("When I click the title link twice, then the description should not be visible", async () => { + await expect(await $(DefinitionPage.definitionContent()).getText()).toBe(""); + + // When + await $(DefinitionPage.definitionTitle()).click(); + await $(DefinitionPage.definitionTitle()).click(); + + // Then + await expect(await $(DefinitionPage.definitionContent()).getText()).toBe(""); + }); + }); +}); diff --git a/tests/functional/spec/question_description.spec.js b/tests/functional/spec/question_description.spec.js index 58059f6ff2..dc901cf3f7 100644 --- a/tests/functional/spec/question_description.spec.js +++ b/tests/functional/spec/question_description.spec.js @@ -1,8 +1,36 @@ import NameBlockPage from "../generated_pages/question_description/name-block.page.js"; +import DescriptionBlockPage from "../generated_pages/optional_guidance_and_description/description-block.page"; +import RadioPage from "../generated_pages/optional_guidance_and_description/mandatory-radio.page"; +import RadioPageTwo from "../generated_pages/optional_guidance_and_description/mandatory-radio-two.page"; +import IntroductionPage from "../generated_pages/question_guidance/introduction.page"; +import GuidancePage from "../generated_pages/question_guidance/block-test-guidance-title.page"; +import { click, verifyUrlContains } from "../helpers"; describe("Question description", () => { - it("Given a question description has been set in the schema as an array, When it is rendered, Then it is displayed correctly as multiple paragraph attributes", () => { - browser.openQuestionnaire("test_question_description.json"); - expect($(NameBlockPage.questionTitle()).getHTML()).to.contain("

Answer the question

Go on

"); + it("Given a question description has been set in the schema as an array, When it is rendered, Then it is displayed correctly as multiple paragraph attributes", async () => { + await browser.openQuestionnaire("test_question_description.json"); + await expect(await $(NameBlockPage.questionTitle()).getHTML()).toContain("

Answer the question

Go on

"); + }); +}); + +describe("Optional question description and guidance", () => { + it("Given a question description has been set in the schema, When the value to be displayed is None, Then it is not rendered on the page", async () => { + await browser.openQuestionnaire("test_optional_guidance_and_description.json"); + await click(DescriptionBlockPage.submit()); + await expect(await $(RadioPage.questionTitle()).getHTML()).not.toContain("

''

"); + await expect(await $(RadioPage.guidance()).isExisting()).toBe(false); + await $(RadioPage.no()).click(); + await click(RadioPage.submit()); + await expect(await $(RadioPageTwo.questionTitle()).getHTML()).toContain("
  • List item one
  • "); + await expect(await $(RadioPageTwo.questionTitle()).getHTML()).not.toContain("
  • "); + }); +}); + +describe("Question guidance", () => { + it("Given a question guidance with multiple content items, When it is rendered, Then there should only be one guidance box", async () => { + await browser.openQuestionnaire("test_question_guidance.json"); + await click(IntroductionPage.submit()); + await verifyUrlContains(GuidancePage.pageName); + await expect(await $$("#question-guidance-question-test-guidance-title").length).toBe(1); }); }); diff --git a/tests/functional/spec/question_variants.spec.js b/tests/functional/spec/question_variants.spec.js index caf3e1313c..9a136747bf 100644 --- a/tests/functional/spec/question_variants.spec.js +++ b/tests/functional/spec/question_variants.spec.js @@ -7,55 +7,56 @@ import firstNumberBlock from "../generated_pages/variants_question/first-number- import nameBlock from "../generated_pages/variants_question/name-block.page.js"; import proxyBlock from "../generated_pages/variants_question/proxy-block.page.js"; import secondNumberBlock from "../generated_pages/variants_question/second-number-block.page.js"; +import { click } from "../helpers"; describe("QuestionVariants", () => { - beforeEach(() => { - browser.openQuestionnaire("test_new_variants_question.json"); + beforeEach(async () => { + await browser.openQuestionnaire("test_variants_question.json"); }); - it("Given I am completing the survey, then the correct questions are shown based on my previous answers", () => { - $(nameBlock.firstName()).setValue("Guido"); - $(nameBlock.lastName()).setValue("van Rossum"); - $(nameBlock.submit()).click(); + it("Given I am completing the survey, then the correct questions are shown based on my previous answers", async () => { + await $(nameBlock.firstName()).setValue("Guido"); + await $(nameBlock.lastName()).setValue("van Rossum"); + await click(nameBlock.submit()); - expect($(proxyBlock.questionText()).getText()).to.contain("Are you Guido van Rossum?"); + await expect(await $(proxyBlock.questionText()).getText()).toBe("Are you Guido van Rossum?"); - $(proxyBlock.noIAmAnsweringOnTheirBehalf()).click(); - $(proxyBlock.submit()).click(); + await $(proxyBlock.noIAmAnsweringOnTheirBehalf()).click(); + await click(proxyBlock.submit()); - expect($(ageBlock.questionText()).getText()).to.contain("What age is Guido van Rossum"); + await expect(await $(ageBlock.questionText()).getText()).toBe("What age is Guido van Rossum?"); - $(ageBlock.age()).setValue(63); - $(ageBlock.submit()).click(); + await $(ageBlock.age()).setValue(63); + await click(ageBlock.submit()); - expect($(ageConfirmationBlock.questionText()).getText()).to.contain("Guido van Rossum is over 16?"); + await expect(await $(ageConfirmationBlock.questionText()).getText()).toBe("Guido van Rossum is over 16?"); - $(ageConfirmationBlock.ageConfirmYes()).click(); - $(ageConfirmationBlock.submit()).click(); + await $(ageConfirmationBlock.ageConfirmYes()).click(); + await click(ageConfirmationBlock.submit()); - expect($(basicVariantsSummary.ageQuestion()).getText()).to.contain("What age is Guido van Rossum"); - expect($(basicVariantsSummary.ageAnswer()).getText()).to.contain("63"); + await expect(await $(basicVariantsSummary.ageQuestion()).getText()).toBe("What age is Guido van Rossum?"); + await expect(await $(basicVariantsSummary.ageAnswer()).getText()).toBe("63"); - $(basicVariantsSummary.submit()).click(); + await click(basicVariantsSummary.submit()); - $(currencyBlock.sterling()).click(); - $(currencyBlock.submit()).click(); + await $(currencyBlock.sterling()).click(); + await click(currencyBlock.submit()); - expect($(firstNumberBlock.firstNumberLabel()).getText()).to.contain("First answer in GBP"); + await expect(await $(firstNumberBlock.firstNumberLabel()).getText()).toBe("First answer in GBP"); - $(firstNumberBlock.firstNumber()).setValue(123); - $(firstNumberBlock.submit()).click(); + await $(firstNumberBlock.firstNumber()).setValue(123); + await click(firstNumberBlock.submit()); - $(secondNumberBlock.secondNumber()).setValue(321); - $(secondNumberBlock.submit()).click(); + await $(secondNumberBlock.secondNumber()).setValue(321); + await click(secondNumberBlock.submit()); - expect($(currencySectionSummary.currencyAnswer()).getText()).to.contain("Sterling"); - expect($(currencySectionSummary.firstNumberAnswer()).getText()).to.contain("ÂŖ"); + await expect(await $(currencySectionSummary.currencyAnswer()).getText()).toBe("Sterling"); + await expect(await $(currencySectionSummary.firstNumberAnswer()).getText()).toContain("ÂŖ"); - $(currencySectionSummary.currencyAnswerEdit()).click(); - $(currencyBlock.usDollars()).click(); - $(currencyBlock.submit()).click(); + await $(currencySectionSummary.currencyAnswerEdit()).click(); + await $(currencyBlock.usDollars()).click(); + await click(currencyBlock.submit()); - expect($(currencySectionSummary.firstNumberAnswer()).getText()).to.contain("$"); + await expect(await $(currencySectionSummary.firstNumberAnswer()).getText()).toContain("$"); }); }); diff --git a/tests/functional/spec/question_variants_first_item_in_list.spec.js b/tests/functional/spec/question_variants_first_item_in_list.spec.js index 4a2e5e5f31..a878f350e4 100644 --- a/tests/functional/spec/question_variants_first_item_in_list.spec.js +++ b/tests/functional/spec/question_variants_first_item_in_list.spec.js @@ -2,38 +2,39 @@ import ListCollectorPage from "../generated_pages/variants_first_item_in_list/li import ListCollectorAddPage from "../generated_pages/variants_first_item_in_list/list-collector-add.page.js"; import ListStatusQuestion from "../generated_pages/variants_first_item_in_list/list-status.page.js"; import HubPage from "../base_pages/hub.page.js"; +import { click } from "../helpers"; describe("Question Variants First Item in List", () => { - it("Given I am the first person on the list, When the when rule is set, Then I should the correct question variant", () => { - browser.openQuestionnaire("test_new_variants_first_item_in_list.json"); - $(HubPage.submit()).click(); - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("Marcus"); - $(ListCollectorAddPage.lastName()).setValue("Twin"); - $(ListCollectorAddPage.submit()).click(); - $(ListCollectorPage.no()).click(); - $(ListCollectorPage.submit()).click(); - $(HubPage.submit()).click(); - expect($(ListStatusQuestion.questionText()).getText()).to.contain("You are the first person in the list"); + it("Given I am the first person on the list, When the when rule is set, Then I should the correct question variant", async () => { + await browser.openQuestionnaire("test_variants_first_item_in_list.json"); + await click(HubPage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Marcus"); + await $(ListCollectorAddPage.lastName()).setValue("Twin"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await click(HubPage.submit()); + await expect(await $(ListStatusQuestion.questionText()).getText()).toBe("You are the first person in the list"); }); - it("Given I am the second person on the list, When the when rule is set, Then I should the correct question variant", () => { - browser.openQuestionnaire("test_new_variants_first_item_in_list.json"); - $(HubPage.submit()).click(); - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("Marcus"); - $(ListCollectorAddPage.lastName()).setValue("Twin"); - $(ListCollectorAddPage.submit()).click(); - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("John"); - $(ListCollectorAddPage.lastName()).setValue("Doe"); - $(ListCollectorAddPage.submit()).click(); - $(ListCollectorPage.no()).click(); - $(ListCollectorPage.submit()).click(); - $(HubPage.summaryRowLink("personal-details-section-2")).click(); - expect($(ListStatusQuestion.questionText()).getText()).to.contain("You are not the first person in the list"); + it("Given I am the second person on the list, When the when rule is set, Then I should the correct question variant", async () => { + await browser.openQuestionnaire("test_variants_first_item_in_list.json"); + await click(HubPage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Marcus"); + await $(ListCollectorAddPage.lastName()).setValue("Twin"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("John"); + await $(ListCollectorAddPage.lastName()).setValue("Doe"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await $(HubPage.summaryRowLink("personal-details-section-2")).click(); + await expect(await $(ListStatusQuestion.questionText()).getText()).toBe("You are not the first person in the list"); }); }); diff --git a/tests/functional/spec/radio_checkbox_descriptions.spec.js b/tests/functional/spec/radio_checkbox_descriptions.spec.js index e5f3e5a6fb..2531f1a874 100644 --- a/tests/functional/spec/radio_checkbox_descriptions.spec.js +++ b/tests/functional/spec/radio_checkbox_descriptions.spec.js @@ -1,23 +1,24 @@ import CheckboxBlockPage from "../generated_pages/radio_checkbox_descriptions/checkbox-block.page"; import RadioBlockPage from "../generated_pages/radio_checkbox_descriptions/radio-block.page"; +import { click } from "../helpers"; describe("Checkbox and Radio item descriptions", () => { describe("Given the user is presented with radio or checkbox options", () => { - before("Launch survey", () => { - browser.openQuestionnaire("test_radio_checkbox_descriptions.json"); + before("Launch survey", async () => { + await browser.openQuestionnaire("test_radio_checkbox_descriptions.json"); }); - it("When the schema defines a description for a checkbox option, then that description is displayed", () => { - expect($(CheckboxBlockPage.newMethodsOfOrganisingExternalRelationshipsWithOtherFirmsOrPublicInstitutionsLabelDescription()).getText()).to.contain( - "For example first use of alliances, partnerships, outsourcing or sub-contracting" + it("When the schema defines a description for a checkbox option, then that description is displayed", async () => { + await expect(await $(CheckboxBlockPage.newMethodsOfOrganisingExternalRelationshipsWithOtherFirmsOrPublicInstitutionsLabelDescription()).getText()).toBe( + "For example first use of alliances, partnerships, outsourcing or sub-contracting", ); }); - it("When the schema defines a description for a radio option, then that description is displayed", () => { - $(CheckboxBlockPage.newBusinessPracticesForOrganisingProcedures()).click(); - $(CheckboxBlockPage.submit()).click(); - expect($(RadioBlockPage.newMethodsOfOrganisingExternalRelationshipsWithOtherFirmsOrPublicInstitutionsLabelDescription()).getText()).to.contain( - "For example first use of alliances, partnerships, outsourcing or sub-contracting" + it("When the schema defines a description for a radio option, then that description is displayed", async () => { + await $(CheckboxBlockPage.newBusinessPracticesForOrganisingProcedures()).click(); + await click(CheckboxBlockPage.submit()); + await expect(await $(RadioBlockPage.newMethodsOfOrganisingExternalRelationshipsWithOtherFirmsOrPublicInstitutionsLabelDescription()).getText()).toBe( + "For example first use of alliances, partnerships, outsourcing or sub-contracting", ); }); }); diff --git a/tests/functional/spec/radio_optional_with_detail_answer_optional.spec.js b/tests/functional/spec/radio_optional_with_detail_answer_optional.spec.js index e4d0d6d04e..32e0647a09 100644 --- a/tests/functional/spec/radio_optional_with_detail_answer_optional.spec.js +++ b/tests/functional/spec/radio_optional_with_detail_answer_optional.spec.js @@ -1,42 +1,43 @@ import RadioNonMandatoryPage from "../generated_pages/radio_optional_with_detail_answer_optional/radio-non-mandatory.page"; import SubmitPage from "../generated_pages/radio_optional_with_detail_answer_optional/submit.page"; +import { click } from "../helpers"; describe("Checkbox and Radio item descriptions", () => { - beforeEach("load the survey", () => { - browser.openQuestionnaire("test_radio_optional_with_detail_answer_optional.json"); + beforeEach("load the survey", async () => { + await browser.openQuestionnaire("test_radio_optional_with_detail_answer_optional.json"); }); describe("Given the user is presented with an optional radio answer with optional detail answer", () => { - it("When no answer is provided, Then the expected answer is displayed", () => { - $(RadioNonMandatoryPage.submit()).click(); - expect($(SubmitPage.radioNonMandatoryAnswer()).getText()).to.contain("No answer provided"); + it("When no answer is provided, Then the expected answer is displayed", async () => { + await click(RadioNonMandatoryPage.submit()); + await expect(await $(SubmitPage.radioNonMandatoryAnswer()).getText()).toBe("No answer provided"); }); - it("When Toast is selected and no detail answer is provided, Then the expected answer is displayed", () => { - $(RadioNonMandatoryPage.toast()).click(); - $(RadioNonMandatoryPage.submit()).click(); - expect($(SubmitPage.radioNonMandatoryAnswer()).getText()).to.contain("Toast"); + it("When Toast is selected and no detail answer is provided, Then the expected answer is displayed", async () => { + await $(RadioNonMandatoryPage.toast()).click(); + await click(RadioNonMandatoryPage.submit()); + await expect(await $(SubmitPage.radioNonMandatoryAnswer()).getText()).toBe("Toast"); }); - it("When Other is selected and no detail answer is provided, Then the expected answer is displayed", () => { - $(RadioNonMandatoryPage.other()).click(); - $(RadioNonMandatoryPage.submit()).click(); - expect($(SubmitPage.radioNonMandatoryAnswer()).getText()).to.contain("Other"); + it("When Other is selected and no detail answer is provided, Then the expected answer is displayed", async () => { + await $(RadioNonMandatoryPage.other()).click(); + await click(RadioNonMandatoryPage.submit()); + await expect(await $(SubmitPage.radioNonMandatoryAnswer()).getText()).toBe("Other"); }); - it("When Other is selected and detail answer is provided, Then the expected answer is displayed", () => { - $(RadioNonMandatoryPage.other()).click(); - $(RadioNonMandatoryPage.otherDetail()).setValue("Eggs"); - $(RadioNonMandatoryPage.submit()).click(); - expect($(SubmitPage.radioNonMandatoryAnswer()).getText()).to.contain("Eggs"); + it("When Other is selected and detail answer is provided, Then the expected answer is displayed", async () => { + await $(RadioNonMandatoryPage.other()).click(); + await $(RadioNonMandatoryPage.otherDetail()).setValue("Eggs"); + await click(RadioNonMandatoryPage.submit()); + await expect(await $(SubmitPage.radioNonMandatoryAnswer()).getText()).toContain("Eggs"); }); - it("When Other is selected and detail answer is provided and the answer is changed, Then the expected answer is displayed", () => { - $(RadioNonMandatoryPage.other()).click(); - $(RadioNonMandatoryPage.otherDetail()).setValue("Eggs"); - $(RadioNonMandatoryPage.toast()).click(); - $(RadioNonMandatoryPage.submit()).click(); - expect($(SubmitPage.radioNonMandatoryAnswer()).getText()).to.contain("Toast"); + it("When Other is selected and detail answer is provided and the answer is changed, Then the expected answer is displayed", async () => { + await $(RadioNonMandatoryPage.other()).click(); + await $(RadioNonMandatoryPage.otherDetail()).setValue("Eggs"); + await $(RadioNonMandatoryPage.toast()).click(); + await click(RadioNonMandatoryPage.submit()); + await expect(await $(SubmitPage.radioNonMandatoryAnswer()).getText()).toBe("Toast"); }); }); }); diff --git a/tests/functional/spec/relationships-unrelated.spec.js b/tests/functional/spec/relationships-unrelated.spec.js deleted file mode 100644 index 1acd783484..0000000000 --- a/tests/functional/spec/relationships-unrelated.spec.js +++ /dev/null @@ -1,94 +0,0 @@ -import ListCollectorPage from "../generated_pages/relationships_unrelated/list-collector.page.js"; -import ListCollectorAddPage from "../generated_pages/relationships_unrelated/list-collector-add.page.js"; -import RelationshipsPage from "../generated_pages/relationships_unrelated/relationships.page.js"; -import RelatedToAnyoneElsePage from "../generated_pages/relationships_unrelated/related-to-anyone-else.page.js"; -import RelationshipsInterstitialPage from "../generated_pages/relationships_unrelated/relationship-interstitial.page.js"; - -describe("Unrelated Relationships", () => { - const schema = "test_relationships_unrelated.json"; - - describe("Given I am completing the test_relationships_unrelated survey,", () => { - before("load the survey", () => { - browser.openQuestionnaire(schema); - }); - - describe("And I add six people", () => { - before("add people", () => { - addPerson("Andrew", "Austin"); - addPerson("Betty", "Burns"); - addPerson("Carla", "Clark"); - addPerson("Daniel", "Davis"); - addPerson("Eve", "Elliot"); - $(ListCollectorPage.no()).click(); - $(ListCollectorPage.submit()).click(); - }); - - it("When I answer 'Unrelated' twice, Then I will be asked if anyone else is related with a list of the remaining people", () => { - $(RelationshipsPage.unrelated()).click(); - $(RelationshipsPage.submit()).click(); - $(RelationshipsPage.unrelated()).click(); - $(RelationshipsPage.submit()).click(); - expect($(RelatedToAnyoneElsePage.questionText()).getText()).to.contain("Are any of these people related to you?"); - expect($(RelatedToAnyoneElsePage.listLabel(1)).getText()).to.equal("Daniel Davis"); - expect($(RelatedToAnyoneElsePage.listLabel(2)).getText()).to.equal("Eve Elliot"); - }); - - it("When I click previous, Then I will go back to the previous relationship", () => { - $(RelatedToAnyoneElsePage.previous()).click(); - expect($(RelationshipsPage.questionText()).getText()).to.contain("Carla Clark is unrelated to Andrew Austin"); - }); - - it("When I return to the 'related to anyone else' question and select 'Yes', Then I will be taken to the next relationship for the first person", () => { - $(RelationshipsPage.submit()).click(); - $(RelatedToAnyoneElsePage.yes()).click(); - $(RelatedToAnyoneElsePage.submit()).click(); - expect($(RelationshipsPage.questionText()).getText()).to.contain("Thinking about Andrew Austin, Daniel Davis is their"); - }); - - it("When I click previous, Then I will go back to the 'related to anyone else' question", () => { - $(RelationshipsPage.previous()).click(); - expect($(RelatedToAnyoneElsePage.questionText()).getText()).to.contain("Are any of these people related to you?"); - expect($(RelatedToAnyoneElsePage.yes()).isSelected()).to.be.true; - }); - - it("When I select 'No' to the 'related to anyone else' question, Then I will be taken to the first relationship for the second person", () => { - $(RelatedToAnyoneElsePage.noNoneOfThesePeopleAreRelatedToMe()).click(); - $(RelatedToAnyoneElsePage.submit()).click(); - expect($(RelationshipsPage.questionText()).getText()).to.contain("Thinking about Betty Burns, Carla Clark is their"); - }); - - it("When I click previous, Then I will go back to the 'related to anyone else' question for the first person", () => { - $(RelationshipsPage.previous()).click(); - expect($(RelatedToAnyoneElsePage.questionText()).getText()).to.contain("Are any of these people related to you?"); - expect($(RelatedToAnyoneElsePage.listLabel(1)).getText()).to.equal("Daniel Davis"); - expect($(RelatedToAnyoneElsePage.listLabel(2)).getText()).to.equal("Eve Elliot"); - expect($(RelatedToAnyoneElsePage.noNoneOfThesePeopleAreRelatedToMe()).isSelected()).to.be.true; - }); - - it("When I click complete the remaining relationships, Then I will go to the relationships section complete page", () => { - $(RelatedToAnyoneElsePage.submit()).click(); - $(RelationshipsPage.unrelated()).click(); - $(RelationshipsPage.submit()).click(); - $(RelationshipsPage.unrelated()).click(); - $(RelationshipsPage.submit()).click(); - $(RelationshipsPage.unrelated()).click(); - $(RelationshipsPage.submit()).click(); - $(RelationshipsPage.unrelated()).click(); - $(RelationshipsPage.submit()).click(); - $(RelationshipsPage.unrelated()).click(); - $(RelationshipsPage.submit()).click(); - $(RelationshipsPage.unrelated()).click(); - $(RelationshipsPage.submit()).click(); - expect(browser.getUrl()).to.contain(RelationshipsInterstitialPage.pageName); - }); - }); - - function addPerson(firstName, lastName) { - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue(firstName); - $(ListCollectorAddPage.lastName()).setValue(lastName); - $(ListCollectorAddPage.submit()).click(); - } - }); -}); diff --git a/tests/functional/spec/relationships.spec.js b/tests/functional/spec/relationships.spec.js deleted file mode 100644 index adaa47d276..0000000000 --- a/tests/functional/spec/relationships.spec.js +++ /dev/null @@ -1,207 +0,0 @@ -import ListCollectorPage from "../generated_pages/relationships/list-collector.page.js"; -import ListCollectorAddPage from "../generated_pages/relationships/list-collector-add.page.js"; -import ListCollectorRemovePage from "../generated_pages/relationships/list-collector-remove.page.js"; -import RelationshipsPage from "../generated_pages/relationships/relationships.page.js"; -import RelationshipsInterstitialPage from "../generated_pages/relationships/relationship-interstitial.page.js"; -import SectionSummaryPage from "../generated_pages/relationships/section-summary.page.js"; - -describe("Relationships", () => { - const schema = "test_relationships.json"; - - describe("Given I am completing the test_relationships survey,", () => { - beforeEach("load the survey", () => { - browser.openQuestionnaire(schema); - }); - - it("When I have one household member, Then I will be not be asked about relationships", () => { - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("Marcus"); - $(ListCollectorAddPage.lastName()).setValue("Twin"); - $(ListCollectorAddPage.submit()).click(); - $(ListCollectorPage.no()).click(); - $(ListCollectorPage.submit()).click(); - expect(browser.getUrl()).to.contain("/sections/section/"); - }); - - it("When I add two household members, Then I will be asked about one relationship", () => { - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("Marcus"); - $(ListCollectorAddPage.lastName()).setValue("Twin"); - $(ListCollectorAddPage.submit()).click(); - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("Samuel"); - $(ListCollectorAddPage.lastName()).setValue("Clemens"); - $(ListCollectorAddPage.submit()).click(); - $(ListCollectorPage.no()).click(); - $(ListCollectorPage.submit()).click(); - expect(browser.getUrl()).to.contain(RelationshipsPage.pageName); - $(RelationshipsPage.husbandOrWife()).click(); - $(RelationshipsPage.submit()).click(); - $(RelationshipsInterstitialPage.submit()).click(); - expect(browser.getUrl()).to.contain("/sections/section/"); - }); - - describe("When I add three household members,", () => { - beforeEach("add three people", () => { - addThreePeople(); - }); - - it("Then I will be asked about all relationships", () => { - $(ListCollectorPage.no()).click(); - $(ListCollectorPage.submit()).click(); - $(RelationshipsPage.husbandOrWife()).click(); - $(RelationshipsPage.submit()).click(); - $(RelationshipsPage.legallyRegisteredCivilPartner()).click(); - $(RelationshipsPage.submit()).click(); - $(RelationshipsPage.husbandOrWife()).click(); - $(RelationshipsPage.submit()).click(); - $(RelationshipsInterstitialPage.submit()).click(); - expect(browser.getUrl()).to.contain("/sections/section/"); - }); - - it("And go to the first relationship, Then the previous link should return to the list collector", () => { - $(ListCollectorPage.no()).click(); - $(ListCollectorPage.submit()).click(); - $(RelationshipsPage.previous()).click(); - expect(browser.getUrl()).to.contain("/questionnaire/list-collector/"); - }); - - it("And go to the first relationship, Then the 'Brother or Sister' option should have the text 'Including half brother or half sister'", () => { - $(ListCollectorPage.no()).click(); - $(ListCollectorPage.submit()).click(); - expect($(RelationshipsPage.brotherOrSisterLabelDescription()).getText()).to.contain("Including half brother or half sister"); - }); - - it("And go to the second relationship, Then the previous link should return to the first relationship", () => { - $(ListCollectorPage.no()).click(); - $(ListCollectorPage.submit()).click(); - $(RelationshipsPage.husbandOrWife()).click(); - $(RelationshipsPage.submit()).click(); - $(RelationshipsPage.previous()).click(); - $(RelationshipsInterstitialPage.submit()).click(); - expect(browser.getUrl()).to.contain(RelationshipsPage.pageName); - expect($(RelationshipsPage.questionText()).getText()).to.contain("Marcus"); - }); - - it("And go to the section summary, Then the previous link should return to the last relationship Interstitial", () => { - $(ListCollectorPage.no()).click(); - $(ListCollectorPage.submit()).click(); - $(RelationshipsPage.husbandOrWife()).click(); - $(RelationshipsPage.submit()).click(); - $(RelationshipsPage.legallyRegisteredCivilPartner()).click(); - $(RelationshipsPage.submit()).click(); - $(RelationshipsPage.husbandOrWife()).click(); - $(RelationshipsPage.submit()).click(); - $(RelationshipsInterstitialPage.submit()).click(); - expect(browser.getUrl()).to.contain("/sections/section/"); - $(SectionSummaryPage.previous()).click(); - $(RelationshipsInterstitialPage.previous()).click(); - expect(browser.getUrl()).to.contain(RelationshipsPage.pageName); - expect($(RelationshipsPage.questionText()).getText()).to.contain("Olivia"); - }); - - it("When I add all relationships and return to the relationships, Then the relationships should be populated", () => { - $(ListCollectorPage.no()).click(); - $(ListCollectorPage.submit()).click(); - $(RelationshipsPage.husbandOrWife()).click(); - $(RelationshipsPage.submit()).click(); - $(RelationshipsPage.legallyRegisteredCivilPartner()).click(); - $(RelationshipsPage.submit()).click(); - $(RelationshipsPage.husbandOrWife()).click(); - $(RelationshipsPage.submit()).click(); - $(RelationshipsInterstitialPage.submit()).click(); - expect(browser.getUrl()).to.contain("/sections/section/"); - $(SectionSummaryPage.previous()).click(); - $(RelationshipsInterstitialPage.previous()).click(); - expect($(RelationshipsPage.husbandOrWife()).isSelected()).to.be.true; - $(RelationshipsPage.previous()).click(); - expect($(RelationshipsPage.legallyRegisteredCivilPartner()).isSelected()).to.be.true; - }); - - it("And go to the first relationship, Then the person's name should be in the question title and playback text", () => { - $(ListCollectorPage.no()).click(); - $(ListCollectorPage.submit()).click(); - expect($(ListCollectorPage.questionText()).getText()).to.contain("Marcus Twin"); - expect($(RelationshipsPage.playback()).getText()).to.contain("Marcus Twin"); - }); - - it("And go to the first relationship and submit without selecting an option, Then an error should be displayed", () => { - $(ListCollectorPage.no()).click(); - $(ListCollectorPage.submit()).click(); - $(RelationshipsPage.submit()).click(); - expect($(RelationshipsPage.error()).isDisplayed()).to.be.true; - }); - - it("And go to the first relationship and click 'Save and sign out', Then I should be signed out", () => { - $(ListCollectorPage.no()).click(); - $(ListCollectorPage.submit()).click(); - $(RelationshipsPage.husbandOrWife()).click(); - $(RelationshipsPage.saveSignOut()).click(); - expect(browser.getUrl()).to.not.contain("questionnaire"); - }); - - it("And go to the first relationship, select a relationship and click 'Save and sign out', Then I should be signed out", () => { - $(ListCollectorPage.no()).click(); - $(ListCollectorPage.submit()).click(); - $(RelationshipsPage.saveSignOut()).click(); - expect(browser.getUrl()).to.not.contain("questionnaire"); - }); - }); - - describe("When I have added one or more household members after answering the relationships question,", () => { - beforeEach("add three people and complete their relationships", () => { - addThreePeopleAndCompleteRelationships(); - }); - - it("Then I delete one of the original household members I will not be asked for the original members relationships again", () => { - $(SectionSummaryPage.peopleListRemoveLink(1)).click(); - $(ListCollectorRemovePage.yes()).click(); - $(ListCollectorRemovePage.submit()).click(); - expect(browser.getUrl()).to.contain("/sections/section/"); - }); - - it("Then I add another household member I will be redirected to parent list collector", () => { - $(SectionSummaryPage.peopleListAddLink()).click(); - $(ListCollectorAddPage.firstName()).setValue("Tom"); - $(ListCollectorAddPage.lastName()).setValue("Bowden"); - $(ListCollectorAddPage.submit()).click(); - expect(browser.getUrl()).to.contain("/questionnaire/list-collector/"); - }); - }); - - function addThreePeopleAndCompleteRelationships() { - addThreePeople(); - - $(ListCollectorPage.no()).click(); - $(ListCollectorPage.submit()).click(); - $(RelationshipsPage.husbandOrWife()).click(); - $(RelationshipsPage.submit()).click(); - $(RelationshipsPage.legallyRegisteredCivilPartner()).click(); - $(RelationshipsPage.submit()).click(); - $(RelationshipsPage.husbandOrWife()).click(); - $(RelationshipsPage.submit()).click(); - $(RelationshipsInterstitialPage.submit()).click(); - } - - function addThreePeople() { - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("Marcus"); - $(ListCollectorAddPage.lastName()).setValue("Twin"); - $(ListCollectorAddPage.submit()).click(); - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("Samuel"); - $(ListCollectorAddPage.lastName()).setValue("Clemens"); - $(ListCollectorAddPage.submit()).click(); - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("Olivia"); - $(ListCollectorAddPage.lastName()).setValue("Clemens"); - $(ListCollectorAddPage.submit()).click(); - } - }); -}); diff --git a/tests/functional/spec/relationships_primary.spec.js b/tests/functional/spec/relationships_primary.spec.js deleted file mode 100644 index 3f61c79342..0000000000 --- a/tests/functional/spec/relationships_primary.spec.js +++ /dev/null @@ -1,86 +0,0 @@ -import PrimaryPersonListCollectorPage from "../generated_pages/relationships_primary/primary-person-list-collector.page.js"; -import PrimaryPersonListCollectorAddPage from "../generated_pages/relationships_primary/primary-person-list-collector-add.page.js"; -import ListCollectorPage from "../generated_pages/relationships_primary/list-collector.page.js"; -import ListCollectorAddPage from "../generated_pages/relationships_primary/list-collector-add.page.js"; -import RelationshipsPage from "../generated_pages/relationships_primary/relationships.page.js"; - -describe("Relationships - Primary Person", () => { - const schema = "test_relationships_primary.json"; - - describe("Given I am completing the test_relationships_primary survey", () => { - beforeEach(() => { - browser.openQuestionnaire(schema); - }); - - it("When I add household members, Then I will be asked my relationships as a primary person", () => { - addPrimaryAndTwoOthers(); - - $(ListCollectorPage.no()).click(); - $(ListCollectorPage.submit()).click(); - expect($(RelationshipsPage.questionText()).getText()).to.contain("is your"); - }); - - it("When I add household members, Then non-primary relationships will be asked as a non primary person", () => { - addPrimaryAndTwoOthers(); - - $(ListCollectorPage.no()).click(); - $(ListCollectorPage.submit()).click(); - $(RelationshipsPage.relationshipBrotherOrSister()).click(); - $(RelationshipsPage.submit()).click(); - $(RelationshipsPage.relationshipSonOrDaughter()).click(); - $(RelationshipsPage.submit()).click(); - expect($(RelationshipsPage.questionText()).getText()).to.contain("is their"); - }); - - it("When I add household members And add thir relationships And remove the primary person And add a new primary person then I will be asked for the relationships again", () => { - addPrimaryAndTwoOthersAndCompleteRelationships(); - - browser.url("/questionnaire/primary-person-list-collector"); - - $(PrimaryPersonListCollectorPage.no()).click(); - $(PrimaryPersonListCollectorPage.submit()).click(); - - browser.url("/questionnaire/primary-person-list-collector"); - - $(PrimaryPersonListCollectorPage.yes()).click(); - $(PrimaryPersonListCollectorPage.submit()).click(); - $(PrimaryPersonListCollectorAddPage.firstName()).setValue("Marcus"); - $(PrimaryPersonListCollectorAddPage.lastName()).setValue("Twin"); - $(PrimaryPersonListCollectorAddPage.submit()).click(); - $(ListCollectorPage.no()).click(); - $(ListCollectorPage.submit()).click(); - - expect($(RelationshipsPage.questionText()).getText()).to.contain("Samuel Clemens is your"); - }); - - function addPrimaryAndTwoOthersAndCompleteRelationships() { - addPrimaryAndTwoOthers(); - - $(ListCollectorPage.no()).click(); - $(ListCollectorPage.submit()).click(); - $(RelationshipsPage.relationshipBrotherOrSister()).click(); - $(RelationshipsPage.submit()).click(); - $(RelationshipsPage.relationshipSonOrDaughter()).click(); - $(RelationshipsPage.submit()).click(); - $(RelationshipsPage.relationshipBrotherOrSister()).click(); - } - - function addPrimaryAndTwoOthers() { - $(PrimaryPersonListCollectorPage.yes()).click(); - $(PrimaryPersonListCollectorPage.submit()).click(); - $(PrimaryPersonListCollectorAddPage.firstName()).setValue("Marcus"); - $(PrimaryPersonListCollectorAddPage.lastName()).setValue("Twin"); - $(PrimaryPersonListCollectorAddPage.submit()).click(); - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("Samuel"); - $(ListCollectorAddPage.lastName()).setValue("Clemens"); - $(ListCollectorAddPage.submit()).click(); - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("Olivia"); - $(ListCollectorAddPage.lastName()).setValue("Clemens"); - $(ListCollectorAddPage.submit()).click(); - } - }); -}); diff --git a/tests/functional/spec/routing_and_skipping_section_dependencies.spec.js b/tests/functional/spec/routing_and_skipping_section_dependencies.spec.js deleted file mode 100644 index 4e509545a9..0000000000 --- a/tests/functional/spec/routing_and_skipping_section_dependencies.spec.js +++ /dev/null @@ -1,240 +0,0 @@ -import AgePage from "../generated_pages/new_routing_and_skipping_section_dependencies/age.page"; -import HouseHoldPersonalDetailsSectionSummaryPage from "../generated_pages/new_routing_and_skipping_section_dependencies/household-personal-details-section-summary.page"; -import HouseholdSectionSummaryPage from "../generated_pages/new_routing_and_skipping_section_dependencies/household-section-summary.page"; -import ListCollectorAddPage from "../generated_pages/new_routing_and_skipping_section_dependencies/list-collector-add.page"; -import ListCollectorPage from "../generated_pages/new_routing_and_skipping_section_dependencies/list-collector.page"; -import NamePage from "../generated_pages/new_routing_and_skipping_section_dependencies/name-block.page"; -import PrimaryPersonSummaryPage from "../generated_pages/new_routing_and_skipping_section_dependencies/primary-person-summary.page"; -import ReasonNoConfirmationPage from "../generated_pages/new_routing_and_skipping_section_dependencies/reason-no-confirmation.page"; -import RepeatingAgePage from "../generated_pages/new_routing_and_skipping_section_dependencies/repeating-age.page"; -import RepeatingSexPage from "../generated_pages/new_routing_and_skipping_section_dependencies/repeating-sex.page"; -import SecurityPage from "../generated_pages/new_routing_and_skipping_section_dependencies/security.page"; -import SkipAgePage from "../generated_pages/new_routing_and_skipping_section_dependencies/skip-age.page"; -import SkipConfirmationPage from "../generated_pages/new_routing_and_skipping_section_dependencies/skip-confirmation.page"; -import SkipConfirmationSectionSummaryPage from "../generated_pages/new_routing_and_skipping_section_dependencies/skip-confirmation-section-summary.page"; -import SkipSectionSummaryPage from "../generated_pages/new_routing_and_skipping_section_dependencies/skip-section-summary.page"; - -import HubPage from "../base_pages/hub.page"; - -describe("Routing and skipping section dependencies", () => { - beforeEach("Load the survey", () => { - browser.openQuestionnaire("test_new_routing_and_skipping_section_dependencies.json"); - }); - - describe("Given the routing and skipping section dependencies questionnaire", () => { - it("When I answer 'No' to skipping the age question, Then in the Primary Person section I am asked my name, age and why I didn't confirm skipping", () => { - answerNoToSkipAgeQuestion(); - - selectPrimaryPerson(); - answerAndSubmitNameQuestion(); - answerAndSubmitAgeQuestion(); - answerAndSubmitReasonForNoConfirmationQuestion(); - - expectPersonalDetailsName(); - expectPersonalDetailsAge(); - expectReasonNoConfirmationAnswer(); - }); - - it("When I answer 'Yes' to skipping the age question, Then in the Primary Person section I am only asked my name and why I didn't confirm skipping", () => { - answerYesToSkipAgeQuestion(); - - selectPrimaryPerson(); - answerAndSubmitNameQuestion(); - answerAndSubmitReasonForNoConfirmationQuestion(); - - expectPersonalDetailsName(); - expectReasonNoConfirmationAnswer(); - expectPersonalDetailsAgeExistingFalse(); - }); - - it("When I answer 'Yes' to skipping the age question and 'Yes' to are you sure in skip question confirmation section, Then in the Primary Person section I am just asked my name", () => { - answerYesToSkipAgeQuestion(); - - selectConfirmationSectionAndAnswerSecurityQuestion(); - answerYesToSkipConfirmationQuestion(); - - selectPrimaryPerson(); - answerAndSubmitNameQuestion(); - - expectPersonalDetailsName(); - expectPersonalDetailsAgeExistingFalse(); - expectReasonNoConfirmationExistingFalse(); - }); - - it("When I answer 'Yes' to skipping the age question but 'No' to are you sure in skip question confirmation section, Then in the Primary Person section I am only asked my name and age", () => { - answerYesToSkipAgeQuestion(); - - selectConfirmationSectionAndAnswerSecurityQuestion(); - answerNoToSkipConfirmationQuestion(); - - selectPrimaryPerson(); - answerAndSubmitNameQuestion(); - answerAndSubmitAgeQuestion(); - - expectPersonalDetailsName(); - expectPersonalDetailsAge(); - expectReasonNoConfirmationExistingFalse(); - }); - - it("When I answer 'No' to skipping the age question and populate the household, Then in each repeating section I am not asked their age", () => { - answerNoToSkipAgeQuestion(); - - addHouseholdMembers(); - - $(HubPage.summaryRowLink("household-personal-details-section-1")).click(); - $(RepeatingSexPage.female()).click(); - $(RepeatingSexPage.submit()).click(); - $(RepeatingAgePage.answer()).setValue("45"); - $(RepeatingAgePage.submit()).click(); - - expect($(HouseHoldPersonalDetailsSectionSummaryPage.repeatingSexAnswer()).getText()).to.contain("Female"); - expect($(HouseHoldPersonalDetailsSectionSummaryPage.repeatingAgeAnswer()).getText()).to.contain("45"); - - $(HouseHoldPersonalDetailsSectionSummaryPage.submit()).click(); - $(HubPage.summaryRowLink("household-personal-details-section-2")).click(); - $(RepeatingSexPage.male()).click(); - $(RepeatingSexPage.submit()).click(); - $(RepeatingAgePage.answer()).setValue("10"); - $(RepeatingAgePage.submit()).click(); - - expect($(HouseHoldPersonalDetailsSectionSummaryPage.repeatingSexAnswer()).getText()).to.contain("Male"); - expect($(HouseHoldPersonalDetailsSectionSummaryPage.repeatingAgeAnswer()).getText()).to.contain("10"); - }); - - it("When I answer 'Yes' to skipping the age question and populate the household, Then in each repeating section I am not asked their age", () => { - answerYesToSkipAgeQuestion(); - - addHouseholdMembers(); - - $(HubPage.summaryRowLink("household-personal-details-section-1")).click(); - $(RepeatingSexPage.female()).click(); - $(RepeatingSexPage.submit()).click(); - expect($(HouseHoldPersonalDetailsSectionSummaryPage.repeatingSexAnswer()).getText()).to.contain("Female"); - expect($(HouseHoldPersonalDetailsSectionSummaryPage.repeatingAgeAnswer()).isExisting()).to.be.false; - - $(HouseHoldPersonalDetailsSectionSummaryPage.submit()).click(); - $(HubPage.summaryRowLink("household-personal-details-section-2")).click(); - $(RepeatingSexPage.male()).click(); - $(RepeatingAgePage.submit()).click(); - - expect($(HouseHoldPersonalDetailsSectionSummaryPage.repeatingSexAnswer()).getText()).to.contain("Male"); - expect($(HouseHoldPersonalDetailsSectionSummaryPage.repeatingAgeAnswer()).isExisting()).to.be.false; - }); - }); - - describe("Given the routing and skipping section dependencies questionnaire and I answered 'Yes' to skipping the age question but 'No' to are you sure in skip question confirmation section", () => { - it("When I change my answer to skipping age to 'No', removing the 'are you sure' question from the path, Then in the Primary Person section I am asked my name, age and why I didn't confirm skipping", () => { - answerYesToSkipAgeQuestion(); - - selectConfirmationSectionAndAnswerSecurityQuestion(); - answerNoToSkipConfirmationQuestion(); - - $(HubPage.summaryRowLink("skip-section")).click(); - $(SkipSectionSummaryPage.skipAgeAnswerEdit()).click(); - $(SkipAgePage.no()).click(); - $(SkipAgePage.submit()).click(); - $(SkipSectionSummaryPage.submit()).click(); - - selectPrimaryPerson(); - answerAndSubmitNameQuestion(); - answerAndSubmitAgeQuestion(); - - $(ReasonNoConfirmationPage.iDidButItWasRemovedFromThePathAsIChangedMyAnswerToNoOnTheSkipQuestion()).click(); - $(ReasonNoConfirmationPage.submit()).click(); - - expectPersonalDetailsName(); - expectPersonalDetailsAge(); - expect($(PrimaryPersonSummaryPage.reasonNoConfirmationAnswer()).getText()).to.contain( - "I did, but it was removed from the path as I changed my answer to No on the skip question" - ); - }); - }); -}); - -const addHouseholdMembers = () => { - $(HubPage.summaryRowLink("household-section")).click(); - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("Sarah"); - $(ListCollectorAddPage.lastName()).setValue("Smith"); - $(ListCollectorAddPage.submit()).click(); - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("Marcus"); - $(ListCollectorAddPage.lastName()).setValue("Smith"); - $(ListCollectorAddPage.submit()).click(); - $(ListCollectorPage.no()).click(); - $(ListCollectorPage.submit()).click(); - $(HouseholdSectionSummaryPage.submit()).click(); -}; - -const selectPrimaryPerson = () => { - $(HubPage.summaryRowLink("primary-person")).click(); -}; - -const selectConfirmationSectionAndAnswerSecurityQuestion = () => { - $(HubPage.summaryRowLink("skip-confirmation-section")).click(); - $(SecurityPage.yes()).click(); - $(SecurityPage.submit()).click(); -}; - -const answerYesToSkipAgeQuestion = () => { - $(HubPage.summaryRowLink("skip-section")).click(); - $(SkipAgePage.yes()).click(); - $(SkipAgePage.submit()).click(); - $(SkipSectionSummaryPage.submit()).click(); -}; - -const answerNoToSkipAgeQuestion = () => { - $(HubPage.summaryRowLink("skip-section")).click(); - $(SkipAgePage.no()).click(); - $(SkipAgePage.submit()).click(); - $(SkipSectionSummaryPage.submit()).click(); -}; - -const answerNoToSkipConfirmationQuestion = () => { - $(SkipConfirmationPage.no()).click(); - $(SkipConfirmationPage.submit()).click(); - $(SkipConfirmationSectionSummaryPage.submit()).click(); -}; - -const answerYesToSkipConfirmationQuestion = () => { - $(SkipConfirmationPage.yes()).click(); - $(SkipConfirmationPage.submit()).click(); - $(SkipConfirmationSectionSummaryPage.submit()).click(); -}; - -const answerAndSubmitNameQuestion = () => { - $(NamePage.name()).setValue("John Smith"); - $(NamePage.submit()).click(); -}; - -const answerAndSubmitAgeQuestion = () => { - $(AgePage.answer()).setValue("50"); - $(AgePage.submit()).click(); -}; - -const answerAndSubmitReasonForNoConfirmationQuestion = () => { - $(ReasonNoConfirmationPage.iDidNotVisitSection2SoConfirmationWasNotNeeded()).click(); - $(ReasonNoConfirmationPage.submit()).click(); -}; - -const expectPersonalDetailsName = () => { - expect($(PrimaryPersonSummaryPage.nameAnswer()).getText()).to.contain("John Smith"); -}; - -const expectPersonalDetailsAge = () => { - expect($(PrimaryPersonSummaryPage.ageAnswer()).getText()).to.contain("50"); -}; - -const expectReasonNoConfirmationAnswer = () => { - expect($(PrimaryPersonSummaryPage.reasonNoConfirmationAnswer()).getText()).to.contain("I did not visit section 2, so confirmation was not needed"); -}; - -const expectPersonalDetailsAgeExistingFalse = () => { - expect($(PrimaryPersonSummaryPage.ageAnswer()).isExisting()).to.be.false; -}; - -const expectReasonNoConfirmationExistingFalse = () => { - expect($(PrimaryPersonSummaryPage.reasonNoConfirmationAnswer()).isExisting()).to.be.false; -}; diff --git a/tests/functional/spec/routing_checkbox_contains.spec.js b/tests/functional/spec/routing_checkbox_contains.spec.js deleted file mode 100644 index 62efa083a7..0000000000 --- a/tests/functional/spec/routing_checkbox_contains.spec.js +++ /dev/null @@ -1,80 +0,0 @@ -import RoutingCheckboxContains from "../generated_pages/routing_checkbox_contains/country-checkbox.page"; -import ContainsAllPage from "../generated_pages/routing_checkbox_contains/country-interstitial-all.page"; -import ContainsAnyPage from "../generated_pages/routing_checkbox_contains/country-interstitial-any.page"; -import SubmitPage from "../generated_pages/routing_checkbox_contains/submit.page"; - -describe("Routing Checkbox Contains Condition.", () => { - beforeEach(() => { - browser.openQuestionnaire("test_routing_checkbox_contains.json"); - }); - - it('Given a list of checkbox options, when I have don\'t select "Liechtenstein" and select the option "India" or the option "Azerbaijan" or both then I should be routed to the "contains any" condition page', () => { - // When - expect($(RoutingCheckboxContains.liechtenstein()).isSelected()).to.be.false; - - $(RoutingCheckboxContains.india()).click(); - $(RoutingCheckboxContains.submit()).click(); - // Then - expect(browser.getUrl()).to.contain(ContainsAnyPage.pageName); - - // Or - $(ContainsAnyPage.previous()).click(); - - // When - $(RoutingCheckboxContains.india()).click(); - $(RoutingCheckboxContains.azerbaijan()).click(); - $(RoutingCheckboxContains.submit()).click(); - - // Then - expect(browser.getUrl()).to.contain(ContainsAnyPage.pageName); - - // Or - $(ContainsAnyPage.previous()).click(); - - // When - $(RoutingCheckboxContains.india()).click(); - $(RoutingCheckboxContains.submit()).click(); - - // Then - expect(browser.getUrl()).to.contain(ContainsAnyPage.pageName); - }); - - it('Given a list of checkbox options, when I select the option "Malta" or the option "Liechtenstein" or both then I should be routed to the summary condition page', () => { - // When - $(RoutingCheckboxContains.liechtenstein()).click(); - $(RoutingCheckboxContains.submit()).click(); - // Then - expect(browser.getUrl()).to.contain(SubmitPage.pageName); - - // Or - $(ContainsAnyPage.previous()).click(); - - // When - $(RoutingCheckboxContains.liechtenstein()).click(); - $(RoutingCheckboxContains.malta()).click(); - $(RoutingCheckboxContains.submit()).click(); - - // Then - expect(browser.getUrl()).to.contain(SubmitPage.pageName); - - // Or - $(ContainsAnyPage.previous()).click(); - - // When - $(RoutingCheckboxContains.liechtenstein()).click(); - $(RoutingCheckboxContains.submit()).click(); - - // Then - expect(browser.getUrl()).to.contain(SubmitPage.pageName); - }); - - it('Given a list of checkbox options, when I select the options "India", "Azerbaijan" and "Liechtenstein" then I should be routed to the "contains all" condition page', () => { - // When - $(RoutingCheckboxContains.india()).click(); - $(RoutingCheckboxContains.azerbaijan()).click(); - $(RoutingCheckboxContains.liechtenstein()).click(); - $(RoutingCheckboxContains.submit()).click(); - // Then - expect(browser.getUrl()).to.contain(ContainsAllPage.pageName); - }); -}); diff --git a/tests/functional/spec/save_sign_out.spec.js b/tests/functional/spec/save_sign_out.spec.js index a9f8b82cb0..67765c1afb 100644 --- a/tests/functional/spec/save_sign_out.spec.js +++ b/tests/functional/spec/save_sign_out.spec.js @@ -5,71 +5,91 @@ import SubmitPage from "../generated_pages/numbers/submit.page"; import IntroductionPage from "../generated_pages/introduction/introduction.page"; import IntroInterstitialPage from "../generated_pages/introduction/general-business-information-completed.page"; import IntroThankYouPagePage from "../base_pages/thank-you.page"; -import HouseHolderConfirmationPage from "../generated_pages/thank_you_census_household/household-confirmation.page"; +import currencyBlock from "../generated_pages/variants_question/currency-block.page.js"; +import firstNumberBlock from "../generated_pages/variants_question/first-number-block.page.js"; +import secondNumberBlock from "../generated_pages/variants_question/second-number-block.page.js"; +import currencySectionSummary from "../generated_pages/variants_question/currency-section-summary.page.js"; import { getRandomString } from "../jwt_helper"; - +import { click, verifyUrlContains } from "../helpers"; describe("Save sign out / Exit", () => { const responseId = getRandomString(16); - it("Given I am on an introduction page, when I click the exit button, then I am redirected to sign out page and my session is cleared", () => { - browser.openQuestionnaire("test_introduction.json"); - $(IntroductionPage.exitButton()).click(); + it("Given I am on an introduction page, when I click the exit button, then I am redirected to sign out page and my session is cleared", async () => { + await browser.openQuestionnaire("test_introduction.json"); + await $(IntroductionPage.exitButton()).click(); - expect(browser.getUrl()).to.contain("/surveys/todo"); + await verifyUrlContains("/surveys/todo"); - browser.back(); - expect($("body").getHTML()).to.contain("Sorry, you need to sign in again"); + await browser.back(); + await expect(await $("body").getHTML()).toContain("Sorry, you need to sign in again"); }); - it("Given I am completing a questionnaire, when I select save and sign out, then I am redirected to sign out page and my session is cleared", () => { - browser.openQuestionnaire("test_numbers.json", { userId: "test_user", responseId }); - $(SetMinMax.setMinimum()).setValue("10"); - $(SetMinMax.setMaximum()).setValue("1020"); - $(SetMinMax.submit()).click(); - $(TestMinMax.saveSignOut()).click(); + it("Given I am completing a questionnaire, when I select save and sign out, then I am redirected to the signed out page", async () => { + await browser.openQuestionnaire("test_numbers.json", { userId: "test_user", responseId }); + await $(SetMinMax.setMinimum()).setValue("10"); + await $(SetMinMax.setMaximum()).setValue("1020"); + await click(SetMinMax.submit()); + await $(TestMinMax.saveSignOut()).click(); - expect(browser.getUrl()).to.contain("/surveys/todo"); + await verifyUrlContains("/signed-out"); - browser.back(); - expect($("body").getHTML()).to.contain("Sorry, you need to sign in again"); + await browser.back(); + await expect(await $("body").getHTML()).toContain("Sorry, you need to sign in again"); }); - it("Given I have started a questionnaire, when I return to the questionnaire, then I am returned to the page I was on and can then complete the questionnaire", () => { - browser.openQuestionnaire("test_numbers.json", { userId: "test_user", responseId }); - - $(TestMinMax.testRange()).setValue("10"); - $(TestMinMax.testMin()).setValue("123"); - $(TestMinMax.testMax()).setValue("1000"); - $(TestMinMax.testPercent()).setValue("100"); - $(TestMinMax.submit()).click(); - $(DetailAnswer.answer1()).click(); - $(DetailAnswer.submit()).click(); - - $(SubmitPage.submit()).click(); - expect(browser.getUrl()).to.contain("thank-you"); + it("Given I have started a questionnaire, when I return to the questionnaire, then I am returned to the page I was on and can then complete the questionnaire", async () => { + await browser.openQuestionnaire("test_numbers.json", { userId: "test_user", responseId }); + + await $(TestMinMax.testRange()).setValue("10"); + await $(TestMinMax.testMin()).setValue("123"); + await $(TestMinMax.testMax()).setValue("1000"); + await $(TestMinMax.testPercent()).setValue("100"); + await click(TestMinMax.submit()); + await $(DetailAnswer.answer1()).click(); + await click(DetailAnswer.submit()); + await $(currencyBlock.usDollars()).click(); + await click(currencyBlock.submit()); + await $(firstNumberBlock.firstNumber()).setValue(50); + await click(firstNumberBlock.submit()); + await $(secondNumberBlock.secondNumber()).setValue(321); + await click(secondNumberBlock.submit()); + await click(currencySectionSummary.submit()); + + await click(SubmitPage.submit()); + await verifyUrlContains("thank-you"); }); - it("Given a business questionnaire, when I navigate the questionnaire, then I see the correct sign out buttons", () => { - browser.openQuestionnaire("test_introduction.json"); - - expect($(IntroductionPage.exitButton()).getText()).to.contain("Exit"); - $(IntroductionPage.getStarted()).click(); + it("Given a I have started a social questionnaire, when I select save and sign out, then I am redirected to the signed out page and the correct access code link is shown", async () => { + await browser.openQuestionnaire("test_theme_social.json", { theme: "social" }); + await $(SubmitPage.saveSignOut()).click(); + await verifyUrlContains("/signed-out"); + await expect(await $("body").getHTML()).toContain("Your progress has been saved"); + await expect(await $("body").getHTML()).toContain("To resume the survey,"); + await expect(await $("body").getHTML()).toContain("/en/start"); + }); - expect($(IntroInterstitialPage.saveSignOut()).getText()).to.contain("Save and exit survey"); - $(IntroInterstitialPage.submit()).click(); + it("Given a I have started a business questionnaire, when I select save and sign out, then I am redirected to the signed out page and the correct access code link is shown", async () => { + await browser.openQuestionnaire("test_introduction.json"); + await $(IntroductionPage.getStarted()).click(); + await $(IntroInterstitialPage.saveSignOut()).click(); + await verifyUrlContains("/signed-out"); + await expect(await $("body").getHTML()).toContain("Your progress has been saved"); + await expect(await $("body").getHTML()).toContain("To find further information or resume the survey,"); + await expect(await $("body").getHTML()).toContain("/surveys/todo"); + }); - expect($(SubmitPage.saveSignOut()).getText()).to.contain("Save and exit survey"); - $(SubmitPage.submit()).click(); + it("Given a business questionnaire, when I navigate the questionnaire, then I see the correct sign out buttons", async () => { + await browser.openQuestionnaire("test_introduction.json"); - expect($(IntroThankYouPagePage.exitButton()).isExisting()).to.be.false; - }); + await expect(await $(IntroductionPage.exitButton()).getText()).toBe("Exit"); + await $(IntroductionPage.getStarted()).click(); - it("Given a Census questionnaire, when I navigate the questionnaire, then I see the correct sign out buttons", () => { - browser.openQuestionnaire("test_thank_you_census_household.json"); + await expect(await $(IntroInterstitialPage.saveSignOut()).getText()).toBe("Save and exit survey"); + await click(IntroInterstitialPage.submit()); - expect($(HouseHolderConfirmationPage.saveSignOut()).getText()).to.contain("Save and complete later"); - $(HouseHolderConfirmationPage.submit()).click(); + await expect(await $(SubmitPage.saveSignOut()).getText()).toBe("Save and exit survey"); + await click(SubmitPage.submit()); - expect($(SubmitPage.saveSignOut()).getText()).to.contain("Save and complete later"); + await expect(await $(IntroThankYouPagePage.exitButton()).isExisting()).toBe(false); }); }); diff --git a/tests/functional/spec/skip_condition_block.spec.js b/tests/functional/spec/skip_condition_block.spec.js deleted file mode 100644 index f2ca43a24e..0000000000 --- a/tests/functional/spec/skip_condition_block.spec.js +++ /dev/null @@ -1,25 +0,0 @@ -import QuestionPage from "../generated_pages/skip_condition_block/do-you-want-to-skip.page"; -import SkipPage from "../generated_pages/skip_condition_block/should-skip.page"; -import SubmitPage from "../generated_pages/skip_condition_block/submit.page"; - -describe("Skip Conditions - Block", () => { - const schema = "test_new_skip_condition_block.json"; - - describe("Given I am completing the test skip condition block survey,", () => { - beforeEach("load the survey", () => { - browser.openQuestionnaire(schema); - }); - - it("When I choose to skip on the first page, Then I should see the summary page", () => { - $(QuestionPage.yes()).click(); - $(QuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); - }); - - it("When I choose not to skip on the first page, Then I should see the should-skip page", () => { - $(QuestionPage.no()).click(); - $(QuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(SkipPage.pageName); - }); - }); -}); diff --git a/tests/functional/spec/skip_condition_group.spec.js b/tests/functional/spec/skip_condition_group.spec.js deleted file mode 100644 index b070009ee1..0000000000 --- a/tests/functional/spec/skip_condition_group.spec.js +++ /dev/null @@ -1,25 +0,0 @@ -import QuestionPage from "../generated_pages/skip_condition_group/do-you-want-to-skip.page"; -import SkipPage from "../generated_pages/skip_condition_group/should-skip.page"; -import SubmitPage from "../generated_pages/skip_condition_group/submit.page"; - -describe("Skip Conditions - Group", () => { - const schema = "test_new_skip_condition_group.json"; - - describe("Given I am completing the test skip condition group survey,", () => { - beforeEach("load the survey", () => { - browser.openQuestionnaire(schema); - }); - - it("When I choose to skip on the first page, Then I should see the summary page", () => { - $(QuestionPage.yes()).click(); - $(QuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); - }); - - it("When I choose not to skip on the first page, Then I should see the should-skip page", () => { - $(QuestionPage.no()).click(); - $(QuestionPage.submit()).click(); - expect(browser.getUrl()).to.contain(SkipPage.pageName); - }); - }); -}); diff --git a/tests/functional/spec/skip_condition_list.spec.js b/tests/functional/spec/skip_condition_list.spec.js deleted file mode 100644 index 58b1d93209..0000000000 --- a/tests/functional/spec/skip_condition_list.spec.js +++ /dev/null @@ -1,67 +0,0 @@ -import ListCollectorPage from "../generated_pages/skip_condition_list/list-collector.page.js"; -import ListCollectorAddPage from "../generated_pages/skip_condition_list/list-collector-add.page.js"; -import LessThanTwoInterstitialPage from "../generated_pages/skip_condition_list/less-than-two-interstitial.page.js"; -import TwoInterstitialPage from "../generated_pages/skip_condition_list/two-interstitial.page.js"; -import MoreThanTwoInterstitialPage from "../generated_pages/skip_condition_list/more-than-two-interstitial.page.js"; - -describe("Feature: Routing on lists", () => { - describe("Given I start skip condition list survey", () => { - beforeEach(() => { - browser.openQuestionnaire("test_skip_condition_list.json"); - }); - - it("When I don't add a person to the list, Then the less than two people skippable page should be shown", () => { - $(ListCollectorPage.no()).click(); - $(ListCollectorPage.submit()).click(); - expect(browser.getUrl()).to.contain(LessThanTwoInterstitialPage.pageName); - }); - - it("When I add one person to the list, Then the less than two people skippable page should be shown", () => { - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("Marcus"); - $(ListCollectorAddPage.lastName()).setValue("Twin"); - $(ListCollectorAddPage.submit()).click(); - $(ListCollectorPage.no()).click(); - $(ListCollectorPage.submit()).click(); - expect(browser.getUrl()).to.contain(LessThanTwoInterstitialPage.pageName); - }); - - it("When I add two people to the list, Then the two people skippable page should be shown", () => { - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("Marcus"); - $(ListCollectorAddPage.lastName()).setValue("Twin"); - $(ListCollectorAddPage.submit()).click(); - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("Samuel"); - $(ListCollectorAddPage.lastName()).setValue("Clemens"); - $(ListCollectorAddPage.submit()).click(); - $(ListCollectorPage.no()).click(); - $(ListCollectorPage.submit()).click(); - expect(browser.getUrl()).to.contain(TwoInterstitialPage.pageName); - }); - - it("When I add three people to the list, Then the more than two people skippable page should be shown", () => { - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("Marcus"); - $(ListCollectorAddPage.lastName()).setValue("Twin"); - $(ListCollectorAddPage.submit()).click(); - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("Samuel"); - $(ListCollectorAddPage.lastName()).setValue("Clemens"); - $(ListCollectorAddPage.submit()).click(); - $(ListCollectorPage.yes()).click(); - $(ListCollectorPage.submit()).click(); - $(ListCollectorAddPage.firstName()).setValue("Olivia"); - $(ListCollectorAddPage.lastName()).setValue("Clemens"); - $(ListCollectorAddPage.submit()).click(); - $(ListCollectorPage.no()).click(); - $(ListCollectorPage.submit()).click(); - expect(browser.getUrl()).to.contain(MoreThanTwoInterstitialPage.pageName); - }); - }); -}); diff --git a/tests/functional/spec/skip_conditions_not_set.spec.js b/tests/functional/spec/skip_conditions_not_set.spec.js deleted file mode 100644 index 50d808861c..0000000000 --- a/tests/functional/spec/skip_conditions_not_set.spec.js +++ /dev/null @@ -1,20 +0,0 @@ -import FoodPage from "../generated_pages/new_skip_condition_not_set/food-block.page"; -import DrinkPage from "../generated_pages/new_skip_condition_not_set/drink-block.page"; -import SubmitPage from "../generated_pages/new_skip_condition_not_set/submit.page"; - -describe("Skip Conditions - Not Set", () => { - beforeEach("Load the survey", () => { - browser.openQuestionnaire("test_new_skip_condition_not_set.json"); - }); - - it("Given I do not complete the first page, Then I should see the summary page", () => { - $(FoodPage.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); - }); - - it("Given I complete the first page, Then I should see the drink page", () => { - $(FoodPage.bacon()).click(); - $(FoodPage.submit()).click(); - expect(browser.getUrl()).to.contain(DrinkPage.pageName); - }); -}); diff --git a/tests/functional/spec/skip_conditions_set.spec.js b/tests/functional/spec/skip_conditions_set.spec.js deleted file mode 100644 index 69b0aba68f..0000000000 --- a/tests/functional/spec/skip_conditions_set.spec.js +++ /dev/null @@ -1,20 +0,0 @@ -import FoodPage from "../generated_pages/new_skip_condition_set/food-block.page"; -import DrinkPage from "../generated_pages/new_skip_condition_set/drink-block.page"; -import SubmitPage from "../generated_pages/new_skip_condition_set/submit.page"; - -describe("Skip Conditions - Set", () => { - beforeEach("Load the survey", () => { - browser.openQuestionnaire("test_new_skip_condition_set.json"); - }); - - it("Given I complete the first page, Then I should see the summary page", () => { - $(FoodPage.bacon()).click(); - $(FoodPage.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); - }); - - it("Given I do not complete the first page, Then I should see the drink page", () => { - $(FoodPage.submit()).click(); - expect(browser.getUrl()).to.contain(DrinkPage.pageName); - }); -}); diff --git a/tests/functional/spec/submit_with_custom_submission_text.spec.js b/tests/functional/spec/submit_with_custom_submission_text.spec.js index 3740db36e5..569bb90c8a 100644 --- a/tests/functional/spec/submit_with_custom_submission_text.spec.js +++ b/tests/functional/spec/submit_with_custom_submission_text.spec.js @@ -1,18 +1,18 @@ import BreakfastPage from "../generated_pages/submit_with_custom_submission_text/breakfast.page.js"; import IntroductionPage from "../generated_pages/submit_with_custom_submission_text/introduction.page.js"; import { SubmitPage } from "../base_pages/submit.page.js"; - +import { click } from "../helpers"; describe("Submit with custom submission text", () => { - beforeEach("Load the questionnaire", () => { - browser.openQuestionnaire("test_submit_with_custom_submission_text.json"); + beforeEach("Load the questionnaire", async () => { + await browser.openQuestionnaire("test_submit_with_custom_submission_text.json"); }); - it("Given a questionnaire with custom submission content has been started, when it is completed to the submit page, then the correct submission content should be displayed", () => { - $(IntroductionPage.getStarted()).click(); - $(BreakfastPage.answer()).setValue("Eggs"); - $(BreakfastPage.submit()).click(); - expect($(SubmitPage.heading()).getText()).to.contain("Submit your questionnaire"); - expect($(SubmitPage.warning()).getText()).to.contain("You cannot view your answers after submission"); - expect($(SubmitPage.guidance()).getText()).to.contain("Thank you for your answers, submit this to complete it"); + it("Given a questionnaire with custom submission content has been started, when it is completed to the submit page, then the correct submission content should be displayed", async () => { + await $(IntroductionPage.getStarted()).click(); + await $(BreakfastPage.answer()).setValue("Eggs"); + await click(BreakfastPage.submit()); + await expect(await $(SubmitPage.heading()).getText()).toBe("Submit your questionnaire"); + await expect(await $(SubmitPage.warning()).getText()).toBe("You cannot view your answers after submission"); + await expect(await $(SubmitPage.guidance()).getText()).toBe("Thank you for your answers, submit this to complete it"); }); }); diff --git a/tests/functional/spec/summaries/calculated_summary/calculated_summary.spec.js b/tests/functional/spec/summaries/calculated_summary/calculated_summary.spec.js new file mode 100644 index 0000000000..cee8c0f62c --- /dev/null +++ b/tests/functional/spec/summaries/calculated_summary/calculated_summary.spec.js @@ -0,0 +1,108 @@ +import { CalculatedSummaryTestCase } from "./calculated_summary_test_case.js"; + +describe("Feature: Calculated Summary", () => { + describe("Given I have a Calculated Summary", () => { + CalculatedSummaryTestCase.testCase("test_calculated_summary.json"); + }); + describe("Given I have a Calculated Summary with the new format", () => { + CalculatedSummaryTestCase.testCase("test_new_calculated_summary.json"); + }); + + describe("Given I have a Calculated Summary", () => { + CalculatedSummaryTestCase.testCrossSectionDependencies("test_calculated_summary_cross_section_dependencies.json"); + }); + describe("Given I have a Calculated Summary with the new format", () => { + CalculatedSummaryTestCase.testCrossSectionDependencies("test_new_calculated_summary_cross_section_dependencies.json"); + }); +}); + +describe("Feature: Calculated Summary with negative values", () => { + describe("Given I enter a negative value in the first section", () => { + CalculatedSummaryTestCase.testNegative("test_calculated_summary.json", -1, 2, 3, 0, "ÂŖ4.00", [ + "-ÂŖ1.00", + "ÂŖ2.00", + "ÂŖ0.00", + "ÂŖ3.00", + "ÂŖ0.00", + "ÂŖ0.00", + "ÂŖ4.00", + ]); + }); + describe("Given I enter a negative value in the second section ", () => { + CalculatedSummaryTestCase.testNegative("test_calculated_summary.json", 12, -2, 1, 0, "ÂŖ11.00", [ + "ÂŖ12.00", + "-ÂŖ2.00", + "ÂŖ0.00", + "ÂŖ1.00", + "ÂŖ0.00", + "ÂŖ0.00", + "ÂŖ11.00", + ]); + }); + describe("Given I enter a negative value in the third section ", () => { + CalculatedSummaryTestCase.testNegative("test_calculated_summary.json", 3, 2, -6, 0, "-ÂŖ1.00", [ + "ÂŖ3.00", + "ÂŖ2.00", + "ÂŖ0.00", + "-ÂŖ6.00", + "ÂŖ0.00", + "ÂŖ0.00", + "-ÂŖ1.00", + ]); + }); + describe("Given I enter negative values in every currency field ", () => { + CalculatedSummaryTestCase.testNegative("test_calculated_summary.json", -1, -2, -3, 0, "-ÂŖ6.00", [ + "-ÂŖ1.00", + "-ÂŖ2.00", + "ÂŖ0.00", + "-ÂŖ3.00", + "ÂŖ0.00", + "ÂŖ0.00", + "-ÂŖ6.00", + ]); + }); + describe("Given I enter a negative value in the first section", () => { + CalculatedSummaryTestCase.testNegative("test_new_calculated_summary.json", -1, 2, 3, 0, "ÂŖ4.00", [ + "-ÂŖ1.00", + "ÂŖ2.00", + "ÂŖ0.00", + "ÂŖ3.00", + "ÂŖ0.00", + "ÂŖ0.00", + "ÂŖ4.00", + ]); + }); + describe("Given I enter a negative value in the second section ", () => { + CalculatedSummaryTestCase.testNegative("test_new_calculated_summary.json", 12, -2, 1, 0, "ÂŖ11.00", [ + "ÂŖ12.00", + "-ÂŖ2.00", + "ÂŖ0.00", + "ÂŖ1.00", + "ÂŖ0.00", + "ÂŖ0.00", + "ÂŖ11.00", + ]); + }); + describe("Given I enter a negative value in the third section ", () => { + CalculatedSummaryTestCase.testNegative("test_new_calculated_summary.json", 3, 2, -6, 0, "-ÂŖ1.00", [ + "ÂŖ3.00", + "ÂŖ2.00", + "ÂŖ0.00", + "-ÂŖ6.00", + "ÂŖ0.00", + "ÂŖ0.00", + "-ÂŖ1.00", + ]); + }); + describe("Given I enter negative values in every currency field ", () => { + CalculatedSummaryTestCase.testNegative("test_new_calculated_summary.json", -1, -2, -3, 0, "-ÂŖ6.00", [ + "-ÂŖ1.00", + "-ÂŖ2.00", + "ÂŖ0.00", + "-ÂŖ3.00", + "ÂŖ0.00", + "ÂŖ0.00", + "-ÂŖ6.00", + ]); + }); +}); diff --git a/tests/functional/spec/summaries/calculated_summary/calculated_summary_test_case.js b/tests/functional/spec/summaries/calculated_summary/calculated_summary_test_case.js new file mode 100644 index 0000000000..789b7925de --- /dev/null +++ b/tests/functional/spec/summaries/calculated_summary/calculated_summary_test_case.js @@ -0,0 +1,462 @@ +import CurrencyTotalPlaybackPage from "../../../generated_pages/calculated_summary/currency-total-playback.page"; +import UnitTotalPlaybackPage from "../../../generated_pages/calculated_summary/unit-total-playback.page"; +import NumberTotalPlaybackPage from "../../../generated_pages/calculated_summary/number-total-playback.page"; +import ThirdNumberBlockPage from "../../../generated_pages/calculated_summary/third-number-block.page"; +import FourthNumberBlockPage from "../../../generated_pages/calculated_summary/fourth-number-block.page"; +import FourthAndAHalfNumberBlockPage from "../../../generated_pages/calculated_summary/fourth-and-a-half-number-block.page"; +import SixthNumberBlockPage from "../../../generated_pages/calculated_summary/sixth-number-block.page"; +import FifthNumberBlockPage from "../../../generated_pages/calculated_summary/fifth-number-block.page"; +import SkipFourthBlockPage from "../../../generated_pages/calculated_summary/skip-fourth-block.page"; +import PercentageTotalPlaybackPage from "../../../generated_pages/calculated_summary/percentage-total-playback.page"; +import CalculatedSummaryTotalConfirmation from "../../../generated_pages/calculated_summary/calculated-summary-total-confirmation.page"; +import SetMinMaxBlockPage from "../../../generated_pages/calculated_summary/set-min-max-block.page"; +import SubmitPage from "../../../generated_pages/calculated_summary/submit.page"; +import ThirdAndAHalfNumberBlockPage from "../../../generated_pages/calculated_summary/third-and-a-half-number-block.page"; +import ThankYouPage from "../../../base_pages/thank-you.page"; +import FirstNumberBlockPage from "../../../generated_pages/calculated_summary/first-number-block.page"; +import SecondNumberBlockPage from "../../../generated_pages/calculated_summary/second-number-block.page"; +import HubPage from "../../../base_pages/hub.page"; +import SkipFirstNumberBlockPageSectionOne from "../../../generated_pages/calculated_summary_cross_section_dependencies/skip-first-block.page"; +import FirstNumberBlockPageSectionOne from "../../../generated_pages/calculated_summary_cross_section_dependencies/first-number-block.page"; +import FirstAndAHalfNumberBlockPageSectionOne from "../../../generated_pages/calculated_summary_cross_section_dependencies/first-and-a-half-number-block.page"; +import SecondNumberBlockPageSectionOne from "../../../generated_pages/calculated_summary_cross_section_dependencies/second-number-block.page"; +import CalculatedSummarySectionOne from "../../../generated_pages/calculated_summary_cross_section_dependencies/currency-total-playback-1.page"; +import CalculatedSummarySectionTwo from "../../../generated_pages/calculated_summary_cross_section_dependencies/currency-total-playback-2.page"; +import ThirdNumberBlockPageSectionTwo from "../../../generated_pages/calculated_summary_cross_section_dependencies/third-number-block.page"; +import SectionSummarySectionOne from "../../../generated_pages/calculated_summary_cross_section_dependencies/questions-section-summary.page"; +import SectionSummarySectionTwo from "../../../generated_pages/calculated_summary_cross_section_dependencies/calculated-summary-section-summary.page"; +import DependencyQuestionSectionTwo from "../../../generated_pages/calculated_summary_cross_section_dependencies/mutually-exclusive-checkbox.page"; +import MinMaxSectionTwo from "../../../generated_pages/calculated_summary_cross_section_dependencies/set-min-max-block.page"; +import { assertSummaryValues, click, verifyUrlContains } from "../../../helpers"; +import { expect } from "@wdio/globals"; + +class TestCase { + testCase(schema) { + before("Get to Calculated Summary", async () => { + await browser.openQuestionnaire(schema); + + await $(FirstNumberBlockPage.firstNumber()).setValue(1.23); + await click(FirstNumberBlockPage.submit()); + + await $(SecondNumberBlockPage.secondNumber()).setValue(4.56); + await $(SecondNumberBlockPage.secondNumberUnitTotal()).setValue(789); + await $(SecondNumberBlockPage.secondNumberAlsoInTotal()).setValue(0.12); + await click(SecondNumberBlockPage.submit()); + + await $(ThirdNumberBlockPage.thirdNumber()).setValue(3.45); + await click(ThirdNumberBlockPage.submit()); + await $(ThirdAndAHalfNumberBlockPage.thirdAndAHalfNumberUnitTotal()).setValue(678); + await click(ThirdAndAHalfNumberBlockPage.submit()); + + await $(SkipFourthBlockPage.no()).click(); + await click(SkipFourthBlockPage.submit()); + + await $(FourthNumberBlockPage.fourthNumber()).setValue(9.01); + await click(FourthNumberBlockPage.submit()); + await $(FourthAndAHalfNumberBlockPage.fourthAndAHalfNumberAlsoInTotal()).setValue(2.34); + await click(FourthAndAHalfNumberBlockPage.submit()); + + await $(FifthNumberBlockPage.fifthPercent()).setValue(56); + await $(FifthNumberBlockPage.fifthNumber()).setValue(78.91); + await click(FifthNumberBlockPage.submit()); + + await $(SixthNumberBlockPage.sixthPercent()).setValue(23); + await $(SixthNumberBlockPage.sixthNumber()).setValue(45.67); + await click(SixthNumberBlockPage.submit()); + + await verifyUrlContains(CurrencyTotalPlaybackPage.pageName); + }); + + it("Given I have completed all questions, When I am on the calculated summary, Then the page title should use the calculation's title", async () => { + await expect(await browser.getTitle()).toBe("Grand total of previous values - A test schema to demo Calculated Summary"); + }); + + it("Given I complete every question, When I get to the currency summary, Then I should see the correct total", async () => { + // Totals and titles should be shown + await expect(await $(CurrencyTotalPlaybackPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total of currency values entered to be ÂŖ20.71. Is this correct?", + ); + await expect(await $(CurrencyTotalPlaybackPage.calculatedSummaryQuestion()).getText()).toBe("Grand total of previous values"); + await expect(await $(CurrencyTotalPlaybackPage.calculatedSummaryAnswer()).getText()).toBe("ÂŖ20.71"); + + // Answers included in calculation should be shown + await expect(await $(CurrencyTotalPlaybackPage.firstNumberAnswerLabel()).getText()).toBe("First answer label"); + await expect(await $(CurrencyTotalPlaybackPage.firstNumberAnswer()).getText()).toBe("ÂŖ1.23"); + await expect(await $(CurrencyTotalPlaybackPage.secondNumberAnswerLabel()).getText()).toBe("Second answer in currency label"); + await expect(await $(CurrencyTotalPlaybackPage.secondNumberAnswer()).getText()).toBe("ÂŖ4.56"); + await expect(await $(CurrencyTotalPlaybackPage.secondNumberAnswerAlsoInTotalLabel()).getText()).toBe( + "Second answer label also in currency total (optional)", + ); + await expect(await $(CurrencyTotalPlaybackPage.secondNumberAnswerAlsoInTotal()).getText()).toBe("ÂŖ0.12"); + await expect(await $(CurrencyTotalPlaybackPage.thirdNumberAnswerLabel()).getText()).toBe("Third answer label"); + await expect(await $(CurrencyTotalPlaybackPage.thirdNumberAnswer()).getText()).toBe("ÂŖ3.45"); + await expect(await $(CurrencyTotalPlaybackPage.fourthNumberAnswerLabel()).getText()).toBe("Fourth answer label (optional)"); + await expect(await $(CurrencyTotalPlaybackPage.fourthNumberAnswer()).getText()).toBe("ÂŖ9.01"); + await expect(await $(CurrencyTotalPlaybackPage.fourthAndAHalfNumberAnswerAlsoInTotalLabel()).getText()).toBe( + "Fourth answer label also in total (optional)", + ); + await expect(await $(CurrencyTotalPlaybackPage.fourthAndAHalfNumberAnswerAlsoInTotal()).getText()).toBe("ÂŖ2.34"); + + // Answers not included in calculation should not be shown + await expect(await $$(UnitTotalPlaybackPage.secondNumberAnswerUnitTotal())).toHaveLength(0); + await expect(await $$(UnitTotalPlaybackPage.thirdAndAHalfNumberAnswerUnitTotal())).toHaveLength(0); + await expect(await $$(NumberTotalPlaybackPage.fifthNumberAnswer())).toHaveLength(0); + await expect(await $$(NumberTotalPlaybackPage.sixthNumberAnswer())).toHaveLength(0); + }); + + it("Given I reach the calculated summary page, Then the Change link url should contain return_to, return_to_answer_id and return_to_block_id query params", async () => { + await expect(await $(CurrencyTotalPlaybackPage.firstNumberAnswerEdit()).getAttribute("href")).toContain( + "/questionnaire/first-number-block/?return_to=calculated-summary&return_to_answer_id=first-number-answer&return_to_block_id=currency-total-playback#first-number-answer", + ); + }); + + it("Given I edit an answer from the calculated summary page and click the Previous button, Then I am taken to the calculated summary page that I clicked the change link from and the browser url should contain an anchor referencing the answer id of the answer I am changing", async () => { + await $(CurrencyTotalPlaybackPage.thirdNumberAnswerEdit()).click(); + await $(ThirdNumberBlockPage.previous()).click(); + await verifyUrlContains("currency-total-playback/#third-number-answer"); + }); + + it("Given I edit an answer from the calculated summary page and click the Submit button, Then I am taken to the calculated summary page that I clicked the change link from and the browser url should contain an anchor referencing the answer id of the answer I am changing", async () => { + await $(CurrencyTotalPlaybackPage.thirdNumberAnswerEdit()).click(); + await click(ThirdNumberBlockPage.submit()); + await verifyUrlContains("currency-total-playback/#third-number-answer"); + }); + + it("Given I change an answer, When I get to the currency summary, Then I should see the new total", async () => { + await $(CurrencyTotalPlaybackPage.fourthNumberAnswerEdit()).click(); + await $(FourthNumberBlockPage.fourthNumber()).setValue(19.01); + await click(FourthNumberBlockPage.submit()); + + await verifyUrlContains(CurrencyTotalPlaybackPage.pageName); + await expect(await $(CurrencyTotalPlaybackPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total of currency values entered to be ÂŖ30.71. Is this correct?", + ); + await expect(await $(CurrencyTotalPlaybackPage.calculatedSummaryAnswer()).getText()).toBe("ÂŖ30.71"); + }); + + it("Given I leave an answer empty, When I get to the currency summary, Then I should see no answer provided and new total", async () => { + await $(CurrencyTotalPlaybackPage.fourthAndAHalfNumberAnswerAlsoInTotalEdit()).click(); + await $(FourthAndAHalfNumberBlockPage.fourthAndAHalfNumberAlsoInTotal()).setValue(""); + await click(FourthAndAHalfNumberBlockPage.submit()); + + await verifyUrlContains(CurrencyTotalPlaybackPage.pageName); + await expect(await $(CurrencyTotalPlaybackPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total of currency values entered to be ÂŖ28.37. Is this correct?", + ); + await expect(await $(CurrencyTotalPlaybackPage.calculatedSummaryAnswer()).getText()).toBe("ÂŖ28.37"); + await expect(await $(CurrencyTotalPlaybackPage.fourthAndAHalfNumberAnswerAlsoInTotal()).getText()).toBe("No answer provided"); + }); + + it("Given I skip the fourth page, When I get to the playback, Then I can should not see it in the total", async () => { + await $(CurrencyTotalPlaybackPage.previous()).click(); + await $(SixthNumberBlockPage.previous()).click(); + await $(FifthNumberBlockPage.previous()).click(); + await $(FourthAndAHalfNumberBlockPage.previous()).click(); + await $(FourthNumberBlockPage.previous()).click(); + + await $(SkipFourthBlockPage.yes()).click(); + await click(SkipFourthBlockPage.submit()); + + await click(FifthNumberBlockPage.submit()); + await click(SixthNumberBlockPage.submit()); + + await verifyUrlContains(CurrencyTotalPlaybackPage.pageName); + await expect(await $$(CurrencyTotalPlaybackPage.fourthNumberAnswer())).toHaveLength(0); + await expect(await $$(CurrencyTotalPlaybackPage.fourthAndAHalfNumberAnswerAlsoInTotal())).toHaveLength(0); + await expect(await $(CurrencyTotalPlaybackPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total of currency values entered to be ÂŖ9.36. Is this correct?", + ); + await expect(await $(CurrencyTotalPlaybackPage.calculatedSummaryAnswer()).getText()).toBe("ÂŖ9.36"); + }); + + it("Given I complete every question, When I get to the unit summary, Then I should see the correct total", async () => { + // Totals and titles should be shown + await click(CurrencyTotalPlaybackPage.submit()); + await expect(await $(UnitTotalPlaybackPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total of unit values entered to be 1,467 cm. Is this correct?", + ); + await expect(await $(UnitTotalPlaybackPage.calculatedSummaryQuestion()).getText()).toBe("Grand total of previous values"); + await expect(await $(UnitTotalPlaybackPage.calculatedSummaryAnswer()).getText()).toBe("1,467 cm"); + + // Answers included in calculation should be shown + await expect(await $(UnitTotalPlaybackPage.secondNumberAnswerUnitTotalLabel()).getText()).toBe("Second answer label in unit total"); + await expect(await $(UnitTotalPlaybackPage.secondNumberAnswerUnitTotal()).getText()).toBe("789 cm"); + await expect(await $(UnitTotalPlaybackPage.thirdAndAHalfNumberAnswerUnitTotalLabel()).getText()).toBe("Third answer label in unit total"); + await expect(await $(UnitTotalPlaybackPage.thirdAndAHalfNumberAnswerUnitTotal()).getText()).toBe("678 cm"); + }); + + it("Given the calculated summary has a custom title, When I am on the unit calculated summary, Then the page title should use the custom title", async () => { + await expect(await browser.getTitle()).toBe("Total Unit Values - A test schema to demo Calculated Summary"); + }); + + it("Given I complete every question, When I get to the percentage summary, Then I should see the correct total", async () => { + // Totals and titles should be shown + await click(UnitTotalPlaybackPage.submit()); + await expect(await $(UnitTotalPlaybackPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total of percentage values entered to be 79%. Is this correct?", + ); + await expect(await $(UnitTotalPlaybackPage.calculatedSummaryQuestion()).getText()).toBe("Grand total of previous values"); + await expect(await $(UnitTotalPlaybackPage.calculatedSummaryAnswer()).getText()).toBe("79%"); + + // Answers included in calculation should be shown + await expect(await $(PercentageTotalPlaybackPage.fifthPercentAnswerLabel()).getText()).toBe("Fifth answer label percentage total"); + await expect(await $(PercentageTotalPlaybackPage.fifthPercentAnswer()).getText()).toBe("56%"); + await expect(await $(PercentageTotalPlaybackPage.sixthPercentAnswerLabel()).getText()).toBe("Sixth answer label percentage total"); + await expect(await $(PercentageTotalPlaybackPage.sixthPercentAnswer()).getText()).toBe("23%"); + }); + + it("Given I complete every question, When I get to the number summary, Then I should see the correct total", async () => { + // Totals and titles should be shown + await click(UnitTotalPlaybackPage.submit()); + await expect(await $(UnitTotalPlaybackPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total of number values entered to be 124.58. Is this correct?", + ); + await expect(await $(UnitTotalPlaybackPage.calculatedSummaryQuestion()).getText()).toBe("Grand total of previous values"); + await expect(await $(UnitTotalPlaybackPage.calculatedSummaryAnswer()).getText()).toBe("124.58"); + + // Answers included in calculation should be shown + await expect(await $(NumberTotalPlaybackPage.fifthNumberAnswerLabel()).getText()).toBe("Fifth answer label number total"); + await expect(await $(NumberTotalPlaybackPage.fifthNumberAnswer()).getText()).toBe("78.91"); + await expect(await $(NumberTotalPlaybackPage.sixthNumberAnswerLabel()).getText()).toBe("Sixth answer label number total"); + await expect(await $(NumberTotalPlaybackPage.sixthNumberAnswer()).getText()).toBe("45.67"); + }); + + it("Given I complete every calculated summary, When I go to a page with calculated summary piping, Then I should the see the piped calculated summary total for each summary", async () => { + await click(NumberTotalPlaybackPage.submit()); + + const content = await $("h1 + ul").getText(); + const textsToAssert = [ + "Total currency values: ÂŖ9.36", + "Total unformatted unit values: 1,467", + "Total formatted unit values: 1,467 cm", + "Total unformatted percentage values: 79", + "Total formatted percentage values: 79%", + "Total number values: 124.58", + ]; + + for (const text of textsToAssert) { + await expect(content).toContain(text); + } + }); + + it("Given I have an answer minimum based on a calculated summary total, When I enter an invalid answer, Then I should see an error message on the page", async () => { + await click(CalculatedSummaryTotalConfirmation.submit()); + await verifyUrlContains(SetMinMaxBlockPage.pageName); + await $(SetMinMaxBlockPage.setMinimum()).setValue(8.0); + await click(SetMinMaxBlockPage.submit()); + await expect(await $(SetMinMaxBlockPage.errorNumber(1)).getText()).toBe("Enter an answer more than or equal to ÂŖ9.36"); + await $(SetMinMaxBlockPage.setMinimum()).setValue(10.0); + await click(SetMinMaxBlockPage.submit()); + }); + + it("Given I have an answer maximum based on a calculated summary total, When I enter an invalid answer, Then I should see an error message on the page", async () => { + await click(SubmitPage.submit()); + await verifyUrlContains(SetMinMaxBlockPage.pageName); + await $(SetMinMaxBlockPage.setMaximum()).setValue(10.0); + await click(SetMinMaxBlockPage.submit()); + await expect(await $(SetMinMaxBlockPage.errorNumber(1)).getText()).toBe("Enter an answer less than or equal to ÂŖ9.36"); + await $(SetMinMaxBlockPage.setMaximum()).setValue(7.0); + await click(SetMinMaxBlockPage.submit()); + }); + + it("Given I confirm the totals and am on the summary, When I edit and change an answer, Then I must re-confirm the dependant calculated summary page and min max question page before I can return to the summary", async () => { + await verifyUrlContains(SubmitPage.pageName); + await $(SubmitPage.thirdNumberAnswerEdit()).click(); + await $(ThirdNumberBlockPage.thirdNumber()).setValue(3.5); + await click(ThirdNumberBlockPage.submit()); + + // first incomplete block + await verifyUrlContains(CurrencyTotalPlaybackPage.pageName); + await expect(await $(CurrencyTotalPlaybackPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total of currency values entered to be ÂŖ9.41. Is this correct?", + ); + await click(CurrencyTotalPlaybackPage.submit()); + + // second incomplete block + await verifyUrlContains(SetMinMaxBlockPage.pageName); + await $(SetMinMaxBlockPage.setMinimum()).setValue(10.0); + await $(SetMinMaxBlockPage.setMaximum()).setValue(9.0); + await click(SetMinMaxBlockPage.submit()); + + // back to summary + await verifyUrlContains(SubmitPage.pageName); + }); + + it("Given I confirm the totals and am on the summary, When I edit and change an answer that has a dependent minimum value from a calculated summary total, And the minimum value has been changed, Then I must re-validate before I get to the summary", async () => { + await verifyUrlContains(SubmitPage.pageName); + await $(SubmitPage.thirdNumberAnswerEdit()).click(); + await $(ThirdNumberBlockPage.thirdNumber()).setValue(10.0); + await click(ThirdNumberBlockPage.submit()); + await verifyUrlContains(CurrencyTotalPlaybackPage.pageName); + await expect(await $(CurrencyTotalPlaybackPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total of currency values entered to be ÂŖ15.91. Is this correct?", + ); + await click(CurrencyTotalPlaybackPage.submit()); + await verifyUrlContains(SetMinMaxBlockPage.pageName); + await click(SetMinMaxBlockPage.submit()); + await expect(await $(SetMinMaxBlockPage.errorNumber(1)).getText()).toBe("Enter an answer more than or equal to ÂŖ15.91"); + await $(SetMinMaxBlockPage.setMinimum()).setValue(16.0); + await click(SetMinMaxBlockPage.submit()); + await verifyUrlContains(SubmitPage.pageName); + }); + + it("Given I confirm the totals and am on the summary, When I edit and change an answer that has a dependent maximum value from a calculated summary total, And the maximum value has been changed, Then I must re-validate before I get to the summary", async () => { + await verifyUrlContains(SubmitPage.pageName); + await $(SubmitPage.thirdNumberAnswerEdit()).click(); + await $(ThirdNumberBlockPage.thirdNumber()).setValue(1.0); + await click(ThirdNumberBlockPage.submit()); + await verifyUrlContains(CurrencyTotalPlaybackPage.pageName); + await expect(await $(CurrencyTotalPlaybackPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total of currency values entered to be ÂŖ6.91. Is this correct?", + ); + await click(CurrencyTotalPlaybackPage.submit()); + await verifyUrlContains(SetMinMaxBlockPage.pageName); + await click(SetMinMaxBlockPage.submit()); + await expect(await $(SetMinMaxBlockPage.errorNumber(1)).getText()).toBe("Enter an answer less than or equal to ÂŖ6.91"); + await $(SetMinMaxBlockPage.setMaximum()).setValue(6.0); + await click(SetMinMaxBlockPage.submit()); + await verifyUrlContains(SubmitPage.pageName); + }); + + it("Given I am on a page with a placeholder containing a calculated summary value, When I have updated the calculated summary so that additional answers are on the path, Then the placeholder should display the updated value", async () => { + await $(SubmitPage.skipFourthBlockAnswerEdit()).click(); + await $(SkipFourthBlockPage.no()).click(); + await click(SkipFourthBlockPage.submit()); + await $(SubmitPage.skipFourthBlockAnswerEdit()).click(); + await browser.url(CalculatedSummaryTotalConfirmation.url()); + await verifyUrlContains(CalculatedSummaryTotalConfirmation.pageName); + const content = await $("h1 + ul").getText(); + const textsToAssert = [ + "Total currency values: ÂŖ25.92", + "Total unformatted unit values: 1,467", + "Total formatted unit values: 1,467 cm", + "Total unformatted percentage values: 79", + "Total formatted percentage values: 79%", + "Total number values: 124.58", + ]; + + for (const text of textsToAssert) { + await expect(content).toContain(text); + } + await browser.url(SubmitPage.url()); + }); + + it("Given I am on a page with a dependent question based on a calculated summary value, When I have updated the calculated summary so that additional answers are on the path, Then the question should display the updated value", async () => { + await $(SubmitPage.setMinimumAnswerEdit()).click(); + await verifyUrlContains(SetMinMaxBlockPage.pageName); + await expect(await $(SetMinMaxBlockPage.questionTitle()).getText()).toContain( + "Set minimum and maximum values based on your calculated summary total of ÂŖ25.92", + ); + await click(SetMinMaxBlockPage.submit()); + await expect(await $(SetMinMaxBlockPage.errorNumber(1)).getText()).toBe("Enter an answer more than or equal to ÂŖ25.92"); + await $(SetMinMaxBlockPage.setMinimum()).setValue(30.0); + await $(SetMinMaxBlockPage.setMaximum()).setValue(6.0); + await click(SetMinMaxBlockPage.submit()); + }); + + it("Given I am on the summary, When I submit the questionnaire, Then I should see the thank you page", async () => { + await click(SubmitPage.submit()); + await verifyUrlContains(ThankYouPage.pageName); + }); + } + + testCrossSectionDependencies(schema) { + before("Get to the question containing calculated summary values with cross section dependencies", async () => { + await browser.openQuestionnaire(schema); + await click(HubPage.submit()); + await $(SkipFirstNumberBlockPageSectionOne.no()).click(); + await click(SkipFirstNumberBlockPageSectionOne.submit()); + await $(FirstNumberBlockPageSectionOne.firstNumber()).setValue(10); + await click(FirstNumberBlockPageSectionOne.submit()); + await $(FirstAndAHalfNumberBlockPageSectionOne.firstAndAHalfNumberAlsoInTotal()).setValue(20); + await click(FirstAndAHalfNumberBlockPageSectionOne.submit()); + await $(SecondNumberBlockPageSectionOne.secondNumberAlsoInTotal()).setValue(30); + await click(SecondNumberBlockPageSectionOne.submit()); + await click(CalculatedSummarySectionOne.submit()); + await click(SectionSummarySectionOne.submit()); + await click(HubPage.submit()); + await $(ThirdNumberBlockPageSectionTwo.thirdNumber()).setValue(20); + await $(ThirdNumberBlockPageSectionTwo.thirdNumberAlsoInTotal()).setValue(20); + await click(ThirdNumberBlockPageSectionTwo.submit()); + await click(CalculatedSummarySectionTwo.submit()); + }); + + it("Given I have a placeholder displaying a calculated summary value source, When the calculated summary value is from a previous section, Then the value displayed should be correct", async () => { + await verifyUrlContains(DependencyQuestionSectionTwo.pageName); + await expect(await $(DependencyQuestionSectionTwo.checkboxAnswerCalcValue1Label()).getText()).toBe("60 - calculated summary answer (previous section)"); + await expect(await $(DependencyQuestionSectionTwo.checkboxAnswerCalcValue2Label()).getText()).toBe("40 - calculated summary answer (current section)"); + }); + + it("Given I have validation using a calculated summary value source, When the calculated summary value is from a previous section, Then the value used to validate should be correct", async () => { + await $(DependencyQuestionSectionTwo.checkboxAnswerCalcValue1()).click(); + await click(DependencyQuestionSectionTwo.submit()); + await verifyUrlContains(MinMaxSectionTwo.pageName); + await $(MinMaxSectionTwo.setMinimum()).setValue(59.0); + await $(MinMaxSectionTwo.setMaximum()).setValue(1.0); + await click(MinMaxSectionTwo.submit()); + await expect(await $(MinMaxSectionTwo.errorNumber(1)).getText()).toBe("Enter an answer more than or equal to ÂŖ60.00"); + await $(MinMaxSectionTwo.setMinimum()).setValue(61.0); + await $(MinMaxSectionTwo.setMaximum()).setValue(40.0); + await click(MinMaxSectionTwo.submit()); + }); + + it("Given I remove answers from the path for a calculated summary in a previous section by changing an answer, When I return to the question with the calculated summary value source, Then the value displayed should be correct", async () => { + await click(SectionSummarySectionTwo.submit()); + await $(HubPage.summaryRowLink("questions-section")).click(); + await $(SectionSummarySectionOne.skipFirstBlockAnswerEdit()).click(); + await $(SkipFirstNumberBlockPageSectionOne.yes()).click(); + await click(SkipFirstNumberBlockPageSectionOne.submit()); + await click(SectionSummarySectionOne.submit()); + await $(HubPage.summaryRowLink("calculated-summary-section")).click(); + await expect(await $("body").getText()).toContain("30 - calculated summary answer (previous section)"); + await $(SectionSummarySectionTwo.checkboxAnswerEdit()).click(); + await expect(await $(DependencyQuestionSectionTwo.checkboxAnswerCalcValue1Label()).getText()).toBe("30 - calculated summary answer (previous section)"); + await expect(await $(DependencyQuestionSectionTwo.checkboxAnswerCalcValue2Label()).getText()).toBe("40 - calculated summary answer (current section)"); + }); + } + + testNegative(schema, firstAnswerValue, secondAnswerValue, thirdAnswerValue, fourthAnswerValue, expectedTotalValue, expectedAnswerValues) { + before("Get to Calculated Summary", async () => { + await browser.openQuestionnaire(schema); + + await $(FirstNumberBlockPage.firstNumber()).setValue(firstAnswerValue); + await click(FirstNumberBlockPage.submit()); + + await $(SecondNumberBlockPage.secondNumber()).setValue(secondAnswerValue); + await $(SecondNumberBlockPage.secondNumberUnitTotal()).setValue(789); + await $(SecondNumberBlockPage.secondNumberAlsoInTotal()).setValue(0); + await click(SecondNumberBlockPage.submit()); + + await $(ThirdNumberBlockPage.thirdNumber()).setValue(thirdAnswerValue); + await click(ThirdNumberBlockPage.submit()); + await $(ThirdAndAHalfNumberBlockPage.thirdAndAHalfNumberUnitTotal()).setValue(678); + await click(ThirdAndAHalfNumberBlockPage.submit()); + + await $(SkipFourthBlockPage.no()).click(); + await click(SkipFourthBlockPage.submit()); + + await $(FourthNumberBlockPage.fourthNumber()).setValue(fourthAnswerValue); + await click(FourthNumberBlockPage.submit()); + await $(FourthAndAHalfNumberBlockPage.fourthAndAHalfNumberAlsoInTotal()).setValue(0); + await click(FourthAndAHalfNumberBlockPage.submit()); + + await $(FifthNumberBlockPage.fifthPercent()).setValue(56); + await $(FifthNumberBlockPage.fifthNumber()).setValue(78.91); + await click(FifthNumberBlockPage.submit()); + + await $(SixthNumberBlockPage.sixthPercent()).setValue(23); + await $(SixthNumberBlockPage.sixthNumber()).setValue(45); + await click(SixthNumberBlockPage.submit()); + + await verifyUrlContains(CurrencyTotalPlaybackPage.pageName); + }); + it("Given I have entered a range of positive and negative values, When I reach the calculated summary, Then the total is correct", async () => { + await assertSummaryValues(expectedAnswerValues); + await expect(await $(CurrencyTotalPlaybackPage.calculatedSummaryTitle()).getText()).toBe( + `We calculate the total of currency values entered to be ${expectedTotalValue}. Is this correct?`, + ); + }); + } +} + +export const CalculatedSummaryTestCase = new TestCase(); diff --git a/tests/functional/spec/summaries/calculated_summary/new_calculated_summary_repeating_and_static_answers.spec.js b/tests/functional/spec/summaries/calculated_summary/new_calculated_summary_repeating_and_static_answers.spec.js new file mode 100644 index 0000000000..badc21d14a --- /dev/null +++ b/tests/functional/spec/summaries/calculated_summary/new_calculated_summary_repeating_and_static_answers.spec.js @@ -0,0 +1,201 @@ +import HubPage from "../../../base_pages/hub.page"; +import AnySupermarketPage from "../../../generated_pages/new_calculated_summary_repeating_and_static_answers/any-supermarket.page.js"; +import ListCollectorPage from "../../../generated_pages/new_calculated_summary_repeating_and_static_answers/list-collector.page.js"; +import ExtraSpendingBlockPage from "../../../generated_pages/new_calculated_summary_repeating_and_static_answers/extra-spending-block.page.js"; +import CalculatedSummarySpendingPage from "../../../generated_pages/new_calculated_summary_repeating_and_static_answers/calculated-summary-spending.page.js"; +import CalculatedSummaryVisitsPage from "../../../generated_pages/new_calculated_summary_repeating_and_static_answers/calculated-summary-visits.page.js"; +import ListCollectorAddPage from "../../../generated_pages/new_calculated_summary_repeating_and_static_answers/list-collector-add.page"; +import DynamicAnswerPage from "../../../generated_pages/new_calculated_summary_repeating_and_static_answers/dynamic-answer.page"; +import SummaryPage from "../../../generated_pages/new_calculated_summary_repeating_and_static_answers/section-1-summary.page"; +import ExtraSpendingMethodBlockPage from "../../../generated_pages/new_calculated_summary_repeating_and_static_answers/extra-spending-method-block.page"; +import ListCollectorRemovePage from "../../../generated_pages/new_calculated_summary_repeating_and_static_answers/list-collector-remove.page"; +import SupermarketTransportPage from "../../../generated_pages/new_calculated_summary_repeating_and_static_answers/supermarket-transport.page"; +import SupermarketTransportCostPage from "../../../generated_pages/new_calculated_summary_repeating_and_static_answers/supermarket-transport-cost.page"; +import CalculatedSummaryPipingPage from "../../../generated_pages/new_calculated_summary_repeating_and_static_answers/calculated-summary-piping.page"; +import { assertSummaryValues, click, verifyUrlContains } from "../../../helpers"; +import { expect } from "@wdio/globals"; + +describe("Calculated summary with repeating answers", () => { + const summaryActions = 'dd[class="ons-summary__actions"]'; + const dynamicAnswerChangeLink = (answerIndex) => $$(summaryActions)[answerIndex].$("a"); + + before("Completing the list collector and dynamic answer", async () => { + await browser.openQuestionnaire("test_new_calculated_summary_repeating_and_static_answers.json"); + await $(HubPage.acceptCookies()).click(); + await $(AnySupermarketPage.yes()).click(); + await click(AnySupermarketPage.submit()); + await $(ListCollectorAddPage.supermarketName()).setValue("Tesco"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.supermarketName()).setValue("Lidl"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await $$(DynamicAnswerPage.inputs())[0].setValue(300); + await $$(DynamicAnswerPage.inputs())[1].setValue(200); + await $$(DynamicAnswerPage.inputs())[2].setValue(30); + await $$(DynamicAnswerPage.inputs())[3].setValue(15); + await $$(DynamicAnswerPage.inputs())[4].setValue(4); + await $$(DynamicAnswerPage.inputs())[5].setValue(2); + await $(DynamicAnswerPage.extraStatic()).setValue(5); + await click(DynamicAnswerPage.submit()); + await $(ExtraSpendingBlockPage.extraSpending()).setValue(0); + }); + + it("Given I complete all list collector dynamic answers for two calculated summaries one of which also has static answers, I'm taken to each one in turn, showing the correct answers", async () => { + await click(ExtraSpendingBlockPage.submit()); + await expect(await $(CalculatedSummarySpendingPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total cost of your weekly shopping to be ÂŖ550.00. Is this correct?", + ); + await expect(await $(CalculatedSummarySpendingPage.calculatedSummaryAnswer()).getText()).toBe("ÂŖ550.00"); + await assertSummaryValues(["ÂŖ300.00", "ÂŖ200.00", "ÂŖ30.00", "ÂŖ15.00", "ÂŖ5.00", "ÂŖ0.00", "ÂŖ550.00"]); + await click(CalculatedSummarySpendingPage.submit()); + await expect(await $(CalculatedSummaryVisitsPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total visits to the shop to be 6. Is this correct?", + ); + await assertSummaryValues(["4", "2", "6"]); + }); + + it("Given I click on a change link, when I use the previous button, I return to the calculated summary", async () => { + await dynamicAnswerChangeLink(1).click(); + await verifyUrlContains(DynamicAnswerPage.pageName); + await $(DynamicAnswerPage.previous()).click(); + await verifyUrlContains(CalculatedSummaryVisitsPage.pageName); + }); + + it("Given I click on a change link, edit an answer and continue, I return to the calculated summary to reconfirm it", async () => { + await dynamicAnswerChangeLink(0).click(); + await $$(DynamicAnswerPage.inputs())[5].setValue(3); + await click(DynamicAnswerPage.submit()); + await verifyUrlContains(CalculatedSummaryVisitsPage.pageName); + await expect(await $(CalculatedSummaryVisitsPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total visits to the shop to be 7. Is this correct?", + ); + await assertSummaryValues(["4", "3", "7"]); + await click(CalculatedSummaryVisitsPage.submit()); + }); + + it("Given I go back and change an answer that opens up a new question before the calculated summary, I am taken to the new question, and then the calculated summary", async () => { + await $(SummaryPage.extraSpendingAnswerEdit()).click(); + await $(ExtraSpendingBlockPage.extraSpending()).setValue(50); + await click(ExtraSpendingBlockPage.submit()); + + // new question + await verifyUrlContains(ExtraSpendingMethodBlockPage.pageName); + await $(ExtraSpendingMethodBlockPage.yes()).click(); + await click(ExtraSpendingMethodBlockPage.submit()); + + // then calculated summary + await verifyUrlContains(CalculatedSummarySpendingPage.pageName); + await expect(await $(CalculatedSummarySpendingPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total cost of your weekly shopping to be ÂŖ600.00. Is this correct?", + ); + + // then jump straight back to section summary (as other calculated summary is unchanged + await click(CalculatedSummarySpendingPage.submit()); + await verifyUrlContains(SummaryPage.pageName); + }); + + it("Given I add a new item to the list, I return to the list collector block, then the dynamic answers, then both calculated summaries to confirm newly added answers", async () => { + await $(SummaryPage.supermarketsListAddLink()).click(); + await $(ListCollectorAddPage.supermarketName()).setValue("Sainsburys"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + + // return to dynamic answer + await verifyUrlContains(DynamicAnswerPage.pageName); + await $$(DynamicAnswerPage.inputs())[2].setValue(100); + await $$(DynamicAnswerPage.inputs())[5].setValue(10); + await $$(DynamicAnswerPage.inputs())[8].setValue(7); + await click(DynamicAnswerPage.submit()); + + // first calc summary + await verifyUrlContains(CalculatedSummarySpendingPage.pageName); + await expect(await $(CalculatedSummarySpendingPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total cost of your weekly shopping to be ÂŖ710.00. Is this correct?", + ); + await assertSummaryValues(["ÂŖ300.00", "ÂŖ200.00", "ÂŖ100.00", "ÂŖ30.00", "ÂŖ15.00", "ÂŖ10.00", "ÂŖ5.00", "ÂŖ0.00", "ÂŖ710.00"]); + + // second calculated summary + await click(CalculatedSummarySpendingPage.submit()); + await expect(await $(CalculatedSummaryVisitsPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total visits to the shop to be 14. Is this correct?", + ); + await assertSummaryValues(["4", "3", "2", "14"]); + await click(CalculatedSummaryVisitsPage.submit()); + await verifyUrlContains(SummaryPage.pageName); + }); + + it("Given I remove an item from the list which changes the calculated summaries, I return to each incomplete block only to confirm new dynamic answers and totals with answers removed", async () => { + await expect(await $(SummaryPage.supermarketsListLabel(1)).getText()).toBe("Tesco"); + await expect(await $(SummaryPage.supermarketsListLabel(2)).getText()).toBe("Lidl"); + await expect(await $(SummaryPage.supermarketsListLabel(3)).getText()).toBe("Sainsburys"); + await expect(await $(SummaryPage.supermarketsListLabel(4)).isExisting()).toBe(false); + await $(SummaryPage.supermarketsListRemoveLink(1)).click(); + + await verifyUrlContains(ListCollectorRemovePage.pageName); + await $(ListCollectorRemovePage.yes()).click(); + await click(ListCollectorRemovePage.submit()); + + // section is now incomplete as dynamic answers and calculated summary depend on the removed item - step through each incomplete block only + await verifyUrlContains(DynamicAnswerPage.pageName); + await click(DynamicAnswerPage.submit()); + + // Tesco is now gone + await expect(await $(CalculatedSummarySpendingPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total cost of your weekly shopping to be ÂŖ380.00. Is this correct?", + ); + await assertSummaryValues(["ÂŖ200.00", "ÂŖ100.00", "ÂŖ15.00", "ÂŖ10.00", "ÂŖ5.00", "ÂŖ50.00", "ÂŖ380.00"]); + await click(CalculatedSummarySpendingPage.submit()); + await expect(await $(CalculatedSummaryVisitsPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total visits to the shop to be 10. Is this correct?", + ); + await assertSummaryValues(["3", "7", "10"]); + await click(CalculatedSummaryVisitsPage.submit()); + + await expect(await $(SummaryPage.supermarketsListLabel(1)).getText()).toBe("Lidl"); + await expect(await $(SummaryPage.supermarketsListLabel(2)).getText()).toBe("Sainsburys"); + await expect(await $(SummaryPage.supermarketsListLabel(3)).isExisting()).toBe(false); + }); + + it("Given I proceed to the second section and enter a value greater than the calculated summary from the previous section, the correct error message is displayed", async () => { + await click(SummaryPage.submit()); + await click(HubPage.submit()); + await $(SupermarketTransportPage.weeklyCarTrips()).setValue(11); + await click(SupermarketTransportPage.submit()); + await expect(await $(SupermarketTransportPage.singleErrorLink()).getText()).toBe("Enter an answer less than or equal to 10"); + }); + + it("Given I change my answer to a value less than the calculated summary from the previous section, I am able to proceed", async () => { + await $(SupermarketTransportPage.weeklyCarTrips()).setValue(9); + await click(SupermarketTransportPage.submit()); + await verifyUrlContains(SupermarketTransportCostPage.pageName); + }); + + it("Given I reach the final block, the calculated summary of dynamic answers is piped in correctly", async () => { + await $(SupermarketTransportCostPage.weeklyTripsCost()).setValue(30); + await click(SupermarketTransportCostPage.submit()); + await verifyUrlContains(CalculatedSummaryPipingPage.pageName); + await expect(await $("body").getText()).toContain("Total weekly supermarket spending: ÂŖ380.00"); + await expect(await $("body").getText()).toContain("Total weekly supermarket visits: 10"); + await expect(await $("body").getText()).toContain("Total of supermarket visits by car: 9"); + await expect(await $("body").getText()).toContain("Total spending on parking: ÂŖ30.00"); + await click(CalculatedSummaryPipingPage.submit()); + }); + + it("Given I return to section 1 and update the calculated summary used in section 2 validation, the progress of section 2 is updated", async () => { + await expect(await $(HubPage.summaryRowState("section-1")).getText()).toBe("Completed"); + await expect(await $(HubPage.summaryRowState("section-2")).getText()).toBe("Completed"); + await $(HubPage.summaryRowLink("section-1")).click(); + await dynamicAnswerChangeLink(8).click(); + await $$(DynamicAnswerPage.inputs())[5].setValue(1); + await click(DynamicAnswerPage.submit()); + await verifyUrlContains(CalculatedSummaryVisitsPage.pageName); + await click(CalculatedSummaryVisitsPage.submit()); + await click(SummaryPage.submit()); + await expect(await $(HubPage.summaryRowState("section-1")).getText()).toBe("Completed"); + await expect(await $(HubPage.summaryRowState("section-2")).getText()).toBe("Partially completed"); + }); +}); diff --git a/tests/functional/spec/summaries/calculated_summary/new_calculated_summary_repeating_blocks.spec.js b/tests/functional/spec/summaries/calculated_summary/new_calculated_summary_repeating_blocks.spec.js new file mode 100644 index 0000000000..7311aae531 --- /dev/null +++ b/tests/functional/spec/summaries/calculated_summary/new_calculated_summary_repeating_blocks.spec.js @@ -0,0 +1,237 @@ +import SectionOnePage from "../../../generated_pages/new_calculated_summary_repeating_blocks/section-1-summary.page.js"; +import SectionTwoPage from "../../../generated_pages/new_calculated_summary_repeating_blocks/section-2-summary.page.js"; +import BlockCarPage from "../../../generated_pages/new_calculated_summary_repeating_blocks/block-car.page.js"; +import AddTransportPage from "../../../generated_pages/new_calculated_summary_repeating_blocks/list-collector-add.page.js"; +import RemoveTransportPage from "../../../generated_pages/new_calculated_summary_repeating_blocks/list-collector-remove.page.js"; +import TransportRepeatingBlock1Page from "../../../generated_pages/new_calculated_summary_repeating_blocks/transport-repeating-block-1-repeating-block.page.js"; +import TransportRepeatingBlock2Page from "../../../generated_pages/new_calculated_summary_repeating_blocks/transport-repeating-block-2-repeating-block.page.js"; +import ListCollectorPage from "../../../generated_pages/new_calculated_summary_repeating_blocks/list-collector.page.js"; +import CalculatedSummarySpendingPage from "../../../generated_pages/new_calculated_summary_repeating_blocks/calculated-summary-spending.page.js"; +import CalculatedSummaryCountPage from "../../../generated_pages/new_calculated_summary_repeating_blocks/calculated-summary-count.page.js"; +import HubPage from "../../../base_pages/hub.page"; +import FamilyJourneysPage from "../../../generated_pages/new_calculated_summary_repeating_blocks/family-journeys.page"; +import BlockSkipPage from "../../../generated_pages/new_calculated_summary_repeating_blocks/block-skip.page"; +import { assertSummaryValues, repeatingAnswerChangeLink, click, verifyUrlContains } from "../../../helpers"; +import { expect } from "@wdio/globals"; + +describe("Feature: Calculated Summary using Repeating Blocks", () => { + before("Reaching the first calculated summary", async () => { + await browser.openQuestionnaire("test_new_calculated_summary_repeating_blocks.json"); + await $(BlockCarPage.car()).setValue(100); + await click(BlockCarPage.submit()); + await $(BlockSkipPage.no()).click(); + await click(BlockSkipPage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(AddTransportPage.transportName()).selectByAttribute("value", "Bus"); + await click(AddTransportPage.submit()); + await $(TransportRepeatingBlock1Page.transportCompany()).setValue("First"); + await $(TransportRepeatingBlock1Page.transportCost()).setValue(30); + await $(TransportRepeatingBlock1Page.transportAdditionalCost()).setValue(5); + await click(TransportRepeatingBlock1Page.submit()); + await $(TransportRepeatingBlock2Page.transportCount()).setValue(10); + await click(TransportRepeatingBlock2Page.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(AddTransportPage.transportName()).selectByAttribute("value", "Plane"); + await click(AddTransportPage.submit()); + await $(TransportRepeatingBlock1Page.transportCompany()).setValue("EasyJet"); + await $(TransportRepeatingBlock1Page.transportCost()).setValue(0); + await $(TransportRepeatingBlock1Page.transportAdditionalCost()).setValue(265); + await click(TransportRepeatingBlock1Page.submit()); + await $(TransportRepeatingBlock2Page.transportCount()).setValue(2); + await click(TransportRepeatingBlock2Page.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + }); + + it("Given I have a calculated summary using both list repeating block and static answers, When I reach the calculated summary page, Then I see the correct items and total.", async () => { + await expect(await $(CalculatedSummarySpendingPage.calculatedSummaryTitle()).getText()).toContain( + "We calculate the total monthly expenditure on transport to be ÂŖ400.00. Is this correct?", + ); + await assertSummaryValues(["ÂŖ100.00", "ÂŖ30.00", "ÂŖ5.00", "ÂŖ0.00", "ÂŖ265.00", "ÂŖ400.00"]); + await expect(await $(CalculatedSummarySpendingPage.summaryItems()).getText()).toContain("Monthly expenditure travelling by car"); + await expect(await $(CalculatedSummarySpendingPage.summaryItems()).getText()).toContain("Monthly season ticket expenditure for travel by Bus"); + await expect(await $(CalculatedSummarySpendingPage.summaryItems()).getText()).toContain("Additional monthly expenditure for travel by Bus"); + await expect(await $(CalculatedSummarySpendingPage.summaryItems()).getText()).toContain("Monthly season ticket expenditure for travel by Plane"); + await expect(await $(CalculatedSummarySpendingPage.summaryItems()).getText()).toContain("Additional monthly expenditure for travel by Plane"); + await click(CalculatedSummarySpendingPage.submit()); + }); + + it("Given I have a calculated summary using a single answer from a repeating block, When I reach the calculated summary page, Then I see the correct items and total", async () => { + await expect(await $(CalculatedSummaryCountPage.calculatedSummaryTitle()).getText()).toContain( + "We calculate the total journeys made per month to be 12. Is this correct?", + ); + await assertSummaryValues(["10", "2", "12"]); + await expect(await $(CalculatedSummaryCountPage.summaryItems()).getText()).toContain("Monthly journeys by Bus"); + await expect(await $(CalculatedSummaryCountPage.summaryItems()).getText()).toContain("Monthly journeys by Plane"); + await click(CalculatedSummaryCountPage.submit()); + }); + + it("Given I add a new item to the list, When I complete the repeating blocks and press continue, Then I see the first calculated summary page which the updated total", async () => { + await $(SectionOnePage.transportListAddLink()).click(); + await $(AddTransportPage.transportName()).selectByAttribute("value", "Train"); + await click(AddTransportPage.submit()); + await $(TransportRepeatingBlock1Page.transportCompany()).setValue("Great Western Railway"); + await $(TransportRepeatingBlock1Page.transportCost()).setValue(100); + await $(TransportRepeatingBlock1Page.transportAdditionalCost()).setValue(50); + await click(TransportRepeatingBlock1Page.submit()); + await $(TransportRepeatingBlock2Page.transportCount()).setValue(6); + await click(TransportRepeatingBlock2Page.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await verifyUrlContains(CalculatedSummarySpendingPage.pageName); + await expect(await $(CalculatedSummarySpendingPage.calculatedSummaryTitle()).getText()).toContain( + "We calculate the total monthly expenditure on transport to be ÂŖ550.00. Is this correct?", + ); + await assertSummaryValues(["ÂŖ100.00", "ÂŖ30.00", "ÂŖ5.00", "ÂŖ0.00", "ÂŖ265.00", "ÂŖ100.00", "ÂŖ50.00", "ÂŖ550.00"]); + }); + + it("Given I am on the first calculated summary, When I confirm the total, Then I see the second calculated summary with an updated total", async () => { + await click(CalculatedSummarySpendingPage.submit()); + await verifyUrlContains(CalculatedSummaryCountPage.pageName); + await expect(await $(CalculatedSummaryCountPage.calculatedSummaryTitle()).getText()).toContain( + "We calculate the total journeys made per month to be 18. Is this correct?", + ); + await assertSummaryValues(["10", "2", "6", "18"]); + await $(CalculatedSummaryCountPage.previous()).click(); + }); + + it("Given I am on the first calculated summary, When I use one of the change links, Then I see the correct repeating block", async () => { + await repeatingAnswerChangeLink(1).click(); + await verifyUrlContains(TransportRepeatingBlock1Page.pageName); + }); + + it("Given I have used a change link on a calculated summary to go back to the first repeating block, When I press continue, Then I see the calculated summary I came from", async () => { + await click(TransportRepeatingBlock1Page.submit()); + await verifyUrlContains(CalculatedSummarySpendingPage.pageName); + }); + + it("Given I am on a calculated summary with change links for repeating blocks, When I use a change link and click previous, Then I see the calculated summary I came from", async () => { + await repeatingAnswerChangeLink(1).click(); + await $(TransportRepeatingBlock1Page.previous()).click(); + await verifyUrlContains(CalculatedSummarySpendingPage.pageName); + }); + + it("Given I use a repeating block change link on the first calculated summary, When I edit my answer and press continue, Then I see the first calculated summary with a new correct total", async () => { + await repeatingAnswerChangeLink(1).click(); + await $(TransportRepeatingBlock1Page.transportCost()).setValue(60); + await click(TransportRepeatingBlock1Page.submit()); + await verifyUrlContains(CalculatedSummarySpendingPage.pageName); + await expect(await $(CalculatedSummarySpendingPage.calculatedSummaryTitle()).getText()).toContain( + "We calculate the total monthly expenditure on transport to be ÂŖ580.00. Is this correct?", + ); + await assertSummaryValues(["ÂŖ100.00", "ÂŖ60.00", "ÂŖ5.00", "ÂŖ0.00", "ÂŖ265.00", "ÂŖ100.00", "ÂŖ50.00", "ÂŖ580.00"]); + await click(CalculatedSummarySpendingPage.submit()); + }); + + it("Given I use a repeating block change link on the second calculated summary, When I edit my answer and press continue, Then I see the second calculated summary with a new correct total", async () => { + await repeatingAnswerChangeLink(2).click(); + await $(TransportRepeatingBlock2Page.transportCount()).setValue(12); + await click(TransportRepeatingBlock2Page.submit()); + await verifyUrlContains(CalculatedSummaryCountPage.pageName); + await expect(await $(CalculatedSummaryCountPage.calculatedSummaryTitle()).getText()).toContain( + "We calculate the total journeys made per month to be 24. Is this correct?", + ); + await assertSummaryValues(["10", "2", "12", "24"]); + await click(CalculatedSummaryCountPage.submit()); + }); + + it("Given I use a remove link for on the summary page, When I press yes to confirm deleting the item, Then I see see the first calculated summary where I'm asked to reconfirm the total", async () => { + await $(SectionOnePage.transportListRemoveLink(1)).click(); + await $(RemoveTransportPage.yes()).click(); + await click(RemoveTransportPage.submit()); + await verifyUrlContains(CalculatedSummarySpendingPage.pageName); + await expect(await $(CalculatedSummarySpendingPage.calculatedSummaryTitle()).getText()).toContain( + "We calculate the total monthly expenditure on transport to be ÂŖ515.00. Is this correct?", + ); + await assertSummaryValues(["ÂŖ100.00", "ÂŖ0.00", "ÂŖ265.00", "ÂŖ100.00", "ÂŖ50.00", "ÂŖ515.00"]); + }); + + it("Given I have confirmed the first updated total, When I press continue, Then I see the next calculated summary to confirm that total too", async () => { + await click(CalculatedSummarySpendingPage.submit()); + await verifyUrlContains(CalculatedSummaryCountPage.pageName); + await expect(await $(CalculatedSummaryCountPage.calculatedSummaryTitle()).getText()).toContain( + "We calculate the total journeys made per month to be 14. Is this correct?", + ); + await assertSummaryValues(["2", "12", "14"]); + }); + + it("Given I have a second section, When I begin and answer the first question with a total higher than the calculated summary, Then I see an error message preventing me from continuing", async () => { + await click(CalculatedSummaryCountPage.submit()); + await click(SectionOnePage.submit()); + await click(HubPage.submit()); + await expect(await $(FamilyJourneysPage.questionTitle()).getText()).toContain("How many of your 14 journeys are to visit family?"); + await $(FamilyJourneysPage.answer()).setValue(15); + await click(FamilyJourneysPage.submit()); + await expect(await $(FamilyJourneysPage.singleErrorLink()).getText()).toContain("Enter an answer less than or equal to 14"); + }); + + it("Given I enter a value below the calculated summary from section 1, When I press Continue, Then I see my answer displayed on the next page", async () => { + await $(FamilyJourneysPage.answer()).setValue(10); + await click(FamilyJourneysPage.submit()); + await expect(await $(SectionTwoPage.familyJourneysQuestion()).getText()).toContain("How many of your 14 journeys are to visit family?"); + await expect(await $(SectionTwoPage.familyJourneysAnswer()).getText()).toContain("10"); + await click(SectionTwoPage.submit()); + }); + + it("Given I use the add list item link, When I add a new item and return to the Hub, Then I see the progress of section 2 has reverted to Partially Complete", async () => { + await expect(await $(HubPage.summaryRowState("section-1")).getText()).toBe("Completed"); + await expect(await $(HubPage.summaryRowState("section-2")).getText()).toBe("Completed"); + await $(HubPage.summaryRowLink("section-1")).click(); + await $(SectionOnePage.transportListAddLink()).click(); + await $(AddTransportPage.transportName()).selectByAttribute("value", "Tube"); + await click(AddTransportPage.submit()); + await click(TransportRepeatingBlock1Page.submit()); + await $(TransportRepeatingBlock2Page.transportCount()).setValue(2); + await click(TransportRepeatingBlock2Page.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await click(CalculatedSummarySpendingPage.submit()); + await click(CalculatedSummaryCountPage.submit()); + await browser.url(HubPage.url()); + await expect(await $(HubPage.summaryRowState("section-1")).getText()).toBe("Completed"); + await expect(await $(HubPage.summaryRowState("section-2")).getText()).toBe("Partially completed"); + }); + + it("Given I complete section-2 again, When I remove a list item and return to the Hub, Then I see the progress of section 2 has reverted to Partially Complete", async () => { + await click(HubPage.submit()); + await $(FamilyJourneysPage.answer()).setValue(16); + await click(FamilyJourneysPage.submit()); + await click(SectionTwoPage.submit()); + await expect(await $(HubPage.summaryRowState("section-1")).getText()).toBe("Completed"); + await expect(await $(HubPage.summaryRowState("section-2")).getText()).toBe("Completed"); + await $(HubPage.summaryRowLink("section-1")).click(); + await $(SectionOnePage.transportListRemoveLink(3)).click(); + await $(RemoveTransportPage.yes()).click(); + await click(RemoveTransportPage.submit()); + await click(CalculatedSummarySpendingPage.submit()); + await click(CalculatedSummaryCountPage.submit()); + await click(SectionOnePage.submit()); + await expect(await $(HubPage.summaryRowState("section-1")).getText()).toBe("Completed"); + await expect(await $(HubPage.summaryRowState("section-2")).getText()).toBe("Partially completed"); + }); + + it("Given I have a question which removes the list collector from the path, When I change my answer to the question removing the list collector and route backwards from the summary, Then I see the first calculated summary with an updated total", async () => { + await $(HubPage.summaryRowLink("section-1")).click(); + await $(SectionOnePage.answerSkipEdit()).click(); + await $(BlockSkipPage.yes()).click(); + await click(BlockSkipPage.submit()); + // calculated summary progress is not altered by removing the list collector from the path so next location is summary page + await verifyUrlContains(SectionOnePage.pageName); + await $(SectionOnePage.previous()).click(); + // other calculated summary should not be on the path, so go straight back to the spending one which now has none of the list items + await verifyUrlContains(CalculatedSummarySpendingPage.pageName); + await expect(await $(CalculatedSummarySpendingPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total monthly expenditure on transport to be ÂŖ100.00. Is this correct?", + ); + await assertSummaryValues(["ÂŖ100.00", "ÂŖ100.00"]); + }); + + it("Given I confirm the calculated summary and finish the section, When I return to the Hub, Then I see that section 2 is no longer available", async () => { + await click(CalculatedSummarySpendingPage.submit()); + await click(SectionOnePage.submit()); + // section 2 is now gone + await expect(await $$(HubPage.summaryItems()).length).toBe(1); + }); +}); diff --git a/tests/functional/spec/summaries/calculated_summary/new_calculated_summary_repeating_section.spec.js b/tests/functional/spec/summaries/calculated_summary/new_calculated_summary_repeating_section.spec.js new file mode 100644 index 0000000000..348df818c6 --- /dev/null +++ b/tests/functional/spec/summaries/calculated_summary/new_calculated_summary_repeating_section.spec.js @@ -0,0 +1,489 @@ +import FirstNumberBlockPage from "../../../generated_pages/new_calculated_summary_repeating_section/first-number-block.page.js"; +import SecondNumberBlockPage from "../../../generated_pages/new_calculated_summary_repeating_section/second-number-block.page.js"; +import ThirdNumberBlockPage from "../../../generated_pages/new_calculated_summary_repeating_section/third-number-block.page.js"; +import ThirdAndAHalfNumberBlockPage from "../../../generated_pages/new_calculated_summary_repeating_section/third-and-a-half-number-block.page.js"; +import SkipFourthBlockPage from "../../../generated_pages/new_calculated_summary_repeating_section/skip-fourth-block.page.js"; +import FourthNumberBlockPage from "../../../generated_pages/new_calculated_summary_repeating_section/fourth-number-block.page.js"; +import FourthAndAHalfNumberBlockPage from "../../../generated_pages/new_calculated_summary_repeating_section/fourth-and-a-half-number-block.page.js"; +import FifthNumberBlockPage from "../../../generated_pages/new_calculated_summary_repeating_section/fifth-number-block.page.js"; +import SixthNumberBlockPage from "../../../generated_pages/new_calculated_summary_repeating_section/sixth-number-block.page.js"; +import CurrencyTotalPlaybackPage from "../../../generated_pages/new_calculated_summary_repeating_section/currency-total-playback.page.js"; +import SetMinMaxBlockPage from "../../../generated_pages/new_calculated_summary_repeating_section/set-min-max-block.page.js"; +import UnitTotalPlaybackPage from "../../../generated_pages/new_calculated_summary_repeating_section/unit-total-playback.page.js"; +import PercentageTotalPlaybackPage from "../../../generated_pages/new_calculated_summary_repeating_section/percentage-total-playback.page.js"; +import NumberTotalPlaybackPage from "../../../generated_pages/new_calculated_summary_repeating_section/number-total-playback.page.js"; +import BreakdownPage from "../../../generated_pages/new_calculated_summary_repeating_section/breakdown.page.js"; +import SecondCurrencyTotalPlaybackPage from "../../../generated_pages/new_calculated_summary_repeating_section/second-currency-total-playback.page.js"; +import CalculatedSummaryTotalConfirmation from "../../../generated_pages/new_calculated_summary_repeating_section/calculated-summary-total-confirmation.page.js"; +import SubmitPage from "../../../generated_pages/new_calculated_summary_repeating_section/personal-details-section-summary.page.js"; +import ThankYouPage from "../../../base_pages/thank-you.page.js"; +import HubPage from "../../../base_pages/hub.page.js"; +import PrimaryPersonListCollectorPage from "../../../generated_pages/new_calculated_summary_repeating_section/primary-person-list-collector.page"; +import PrimaryPersonListCollectorAddPage from "../../../generated_pages/new_calculated_summary_repeating_section/primary-person-list-collector-add.page.js"; +import ListCollectorPage from "../../../generated_pages/new_calculated_summary_repeating_section/list-collector.page"; +import ListCollectorAddPage from "../../../generated_pages/new_calculated_summary_repeating_section/list-collector-add.page"; +import SkipFirstNumberBlockPageSectionOne from "../../../generated_pages/new_calculated_summary_cross_section_dependencies_repeating/skip-first-block.page"; +import FirstNumberBlockPageSectionOne from "../../../generated_pages/new_calculated_summary_cross_section_dependencies_repeating/first-number-block.page"; +import FirstAndAHalfNumberBlockPageSectionOne from "../../../generated_pages/new_calculated_summary_cross_section_dependencies_repeating/first-and-a-half-number-block.page"; +import SecondNumberBlockPageSectionOne from "../../../generated_pages/new_calculated_summary_cross_section_dependencies_repeating/second-number-block.page"; +import CalculatedSummarySectionOne from "../../../generated_pages/new_calculated_summary_cross_section_dependencies_repeating/currency-total-playback-1.page"; +import CalculatedSummarySectionTwo from "../../../generated_pages/new_calculated_summary_cross_section_dependencies_repeating/currency-total-playback-2.page"; +import ThirdNumberBlockPageSectionTwo from "../../../generated_pages/new_calculated_summary_cross_section_dependencies_repeating/third-number-block.page"; +import SectionSummarySectionOne from "../../../generated_pages/new_calculated_summary_cross_section_dependencies_repeating/questions-section-summary.page"; +import SectionSummarySectionTwo from "../../../generated_pages/new_calculated_summary_cross_section_dependencies_repeating/calculated-summary-section-summary.page"; +import DependencyQuestionSectionTwo from "../../../generated_pages/new_calculated_summary_cross_section_dependencies_repeating/mutually-exclusive-checkbox.page"; +import MinMaxSectionTwo from "../../../generated_pages/new_calculated_summary_cross_section_dependencies_repeating/set-min-max-block.page"; +import { click, verifyUrlContains } from "../../../helpers"; +import { expect } from "@wdio/globals"; + +describe("Feature: Calculated Summary Repeating Section", () => { + describe("Given I have a Calculated Summary in a Repeating Section", () => { + before("Get to Calculated Summary", async () => { + await browser.openQuestionnaire("test_new_calculated_summary_repeating_section.json"); + await click(HubPage.submit()); + await $(PrimaryPersonListCollectorPage.yes()).click(); + await click(PrimaryPersonListCollectorPage.submit()); + await $(PrimaryPersonListCollectorAddPage.firstName()).setValue("Marcus"); + await $(PrimaryPersonListCollectorAddPage.lastName()).setValue("Twin"); + await click(PrimaryPersonListCollectorAddPage.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await click(HubPage.submit()); + + await getToFirstCalculatedSummary(); + + await verifyUrlContains(CurrencyTotalPlaybackPage.pageName); + }); + + it("Given I have completed all questions, When I am on the calculated summary and there is no custom page title, Then the page title should use the calculation's title", async () => { + await expect(await browser.getTitle()).toBe("Grand total of previous values - A test schema to demo Calculated Summary"); + }); + + it("Given I complete every question, When I get to the currency summary, Then I should see the correct total", async () => { + // Totals and titles should be shown + await expect(await $(CurrencyTotalPlaybackPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total of currency values entered to be ÂŖ20.71. Is this correct?", + ); + await expect(await $(CurrencyTotalPlaybackPage.calculatedSummaryQuestion()).getText()).toBe("Grand total of previous values"); + await expect(await $(CurrencyTotalPlaybackPage.calculatedSummaryAnswer()).getText()).toBe("ÂŖ20.71"); + + // Answers included in calculation should be shown + await expect(await $(CurrencyTotalPlaybackPage.firstNumberAnswerLabel()).getText()).toBe("First answer label"); + await expect(await $(CurrencyTotalPlaybackPage.firstNumberAnswer()).getText()).toBe("ÂŖ1.23"); + await expect(await $(CurrencyTotalPlaybackPage.secondNumberAnswerLabel()).getText()).toBe("Second answer in currency label"); + await expect(await $(CurrencyTotalPlaybackPage.secondNumberAnswer()).getText()).toBe("ÂŖ4.56"); + await expect(await $(CurrencyTotalPlaybackPage.secondNumberAnswerAlsoInTotalLabel()).getText()).toBe( + "Second answer label also in currency total (optional)", + ); + await expect(await $(CurrencyTotalPlaybackPage.secondNumberAnswerAlsoInTotal()).getText()).toBe("ÂŖ0.12"); + await expect(await $(CurrencyTotalPlaybackPage.thirdNumberAnswerLabel()).getText()).toBe("Third answer label"); + await expect(await $(CurrencyTotalPlaybackPage.thirdNumberAnswer()).getText()).toBe("ÂŖ3.45"); + await expect(await $(CurrencyTotalPlaybackPage.fourthNumberAnswerLabel()).getText()).toBe("Fourth answer label (optional)"); + await expect(await $(CurrencyTotalPlaybackPage.fourthNumberAnswer()).getText()).toBe("ÂŖ9.01"); + await expect(await $(CurrencyTotalPlaybackPage.fourthAndAHalfNumberAnswerAlsoInTotalLabel()).getText()).toBe( + "Fourth answer label also in total (optional)", + ); + await expect(await $(CurrencyTotalPlaybackPage.fourthAndAHalfNumberAnswerAlsoInTotal()).getText()).toBe("ÂŖ2.34"); + + // Answers not included in calculation should not be shown + await expect(await $$(UnitTotalPlaybackPage.secondNumberAnswerUnitTotal())).toHaveLength(0); + await expect(await $$(UnitTotalPlaybackPage.thirdAndAHalfNumberAnswerUnitTotal())).toHaveLength(0); + await expect(await $$(NumberTotalPlaybackPage.fifthNumberAnswer())).toHaveLength(0); + await expect(await $$(NumberTotalPlaybackPage.sixthNumberAnswer())).toHaveLength(0); + }); + + it("Given I reach the calculated summary page, Then the Change link url should contain return_to, return_to_answer_id and return_to_block_id query params", async () => { + await expect(await $(CurrencyTotalPlaybackPage.firstNumberAnswerEdit()).getAttribute("href")).toContain( + "first-number-block/?return_to=calculated-summary&return_to_answer_id=first-number-answer&return_to_block_id=currency-total-playback#first-number-answer", + ); + }); + + it("Given I edit an answer from the calculated summary page and click the Previous button, Then I am taken to the calculated summary page that I clicked the change link from and the browser url should contain an anchor referencing the answer id of the answer I am changing", async () => { + await $(CurrencyTotalPlaybackPage.thirdNumberAnswerEdit()).click(); + await $(ThirdNumberBlockPage.previous()).click(); + await verifyUrlContains("currency-total-playback/#third-number-answer"); + }); + + it("Given I edit an answer from the calculated summary page and click the Submit button, Then I am taken to the calculated summary page that I clicked the change link from and the browser url should contain an anchor referencing the answer id of the answer I am changing", async () => { + await $(CurrencyTotalPlaybackPage.thirdNumberAnswerEdit()).click(); + await click(ThirdNumberBlockPage.submit()); + await verifyUrlContains("currency-total-playback/#third-number-answer"); + }); + + it("Given I change an answer, When I get to the currency summary, Then I should see the new total", async () => { + await $(CurrencyTotalPlaybackPage.fourthNumberAnswerEdit()).click(); + await $(FourthNumberBlockPage.fourthNumber()).setValue(19.01); + await click(FourthNumberBlockPage.submit()); + + await verifyUrlContains(CurrencyTotalPlaybackPage.pageName); + await expect(await $(CurrencyTotalPlaybackPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total of currency values entered to be ÂŖ30.71. Is this correct?", + ); + await expect(await $(CurrencyTotalPlaybackPage.calculatedSummaryAnswer()).getText()).toBe("ÂŖ30.71"); + }); + + it("Given I leave an answer empty, When I get to the currency summary, Then I should see no answer provided and new total", async () => { + await $(CurrencyTotalPlaybackPage.fourthAndAHalfNumberAnswerAlsoInTotalEdit()).click(); + await $(FourthAndAHalfNumberBlockPage.fourthAndAHalfNumberAlsoInTotal()).setValue(""); + await click(FourthAndAHalfNumberBlockPage.submit()); + + await verifyUrlContains(CurrencyTotalPlaybackPage.pageName); + await expect(await $(CurrencyTotalPlaybackPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total of currency values entered to be ÂŖ28.37. Is this correct?", + ); + await expect(await $(CurrencyTotalPlaybackPage.calculatedSummaryAnswer()).getText()).toBe("ÂŖ28.37"); + await expect(await $(CurrencyTotalPlaybackPage.fourthAndAHalfNumberAnswerAlsoInTotal()).getText()).toBe("No answer provided"); + }); + + it("Given I skip the fourth page, When I get to the playback, Then I can should not see it in the total", async () => { + await $(CurrencyTotalPlaybackPage.previous()).click(); + await $(SixthNumberBlockPage.previous()).click(); + await $(FifthNumberBlockPage.previous()).click(); + await $(FourthAndAHalfNumberBlockPage.previous()).click(); + await $(FourthNumberBlockPage.previous()).click(); + + await $(SkipFourthBlockPage.yes()).click(); + await click(SkipFourthBlockPage.submit()); + + await click(FifthNumberBlockPage.submit()); + await click(SixthNumberBlockPage.submit()); + + await verifyUrlContains(CurrencyTotalPlaybackPage.pageName); + await expect(await $$(CurrencyTotalPlaybackPage.fourthNumberAnswer())).toHaveLength(0); + await expect(await $$(CurrencyTotalPlaybackPage.fourthAndAHalfNumberAnswerAlsoInTotal())).toHaveLength(0); + await expect(await $(CurrencyTotalPlaybackPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total of currency values entered to be ÂŖ9.36. Is this correct?", + ); + await expect(await $(CurrencyTotalPlaybackPage.calculatedSummaryAnswer()).getText()).toBe("ÂŖ9.36"); + }); + + it("Given I complete every question, When I get to the unit summary, Then I should see the correct total", async () => { + // Totals and titles should be shown + await click(CurrencyTotalPlaybackPage.submit()); + await expect(await $(UnitTotalPlaybackPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total of unit values entered to be 1,467 cm. Is this correct?", + ); + await expect(await $(UnitTotalPlaybackPage.calculatedSummaryQuestion()).getText()).toBe("Grand total of previous values"); + await expect(await $(UnitTotalPlaybackPage.calculatedSummaryAnswer()).getText()).toBe("1,467 cm"); + + // Answers included in calculation should be shown + await expect(await $(UnitTotalPlaybackPage.secondNumberAnswerUnitTotalLabel()).getText()).toBe("Second answer label in unit total"); + await expect(await $(UnitTotalPlaybackPage.secondNumberAnswerUnitTotal()).getText()).toBe("789 cm"); + await expect(await $(UnitTotalPlaybackPage.thirdAndAHalfNumberAnswerUnitTotalLabel()).getText()).toBe("Third answer label in unit total"); + await expect(await $(UnitTotalPlaybackPage.thirdAndAHalfNumberAnswerUnitTotal()).getText()).toBe("678 cm"); + }); + + it("Given the calculated summary has a custom title, When I am on the unit calculated summary, Then the page title should use the custom title", async () => { + await expect(await browser.getTitle()).toBe("Total Unit Values - A test schema to demo Calculated Summary"); + }); + + it("Given I complete every question, When I get to the percentage summary, Then I should see the correct total", async () => { + // Totals and titles should be shown + await click(UnitTotalPlaybackPage.submit()); + await expect(await $(UnitTotalPlaybackPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total of percentage values entered to be 79%. Is this correct?", + ); + await expect(await $(UnitTotalPlaybackPage.calculatedSummaryQuestion()).getText()).toBe("Grand total of previous values"); + await expect(await $(UnitTotalPlaybackPage.calculatedSummaryAnswer()).getText()).toBe("79%"); + + // Answers included in calculation should be shown + await expect(await $(PercentageTotalPlaybackPage.fifthPercentAnswerLabel()).getText()).toBe("Fifth answer label percentage total"); + await expect(await $(PercentageTotalPlaybackPage.fifthPercentAnswer()).getText()).toBe("56%"); + await expect(await $(PercentageTotalPlaybackPage.sixthPercentAnswerLabel()).getText()).toBe("Sixth answer label percentage total"); + await expect(await $(PercentageTotalPlaybackPage.sixthPercentAnswer()).getText()).toBe("23%"); + }); + + it("Given the calculated summary has a custom title with the list item position, When I am on the percentage calculated summary, Then the page title should use the custom title with the list item position", async () => { + await expect(await browser.getTitle()).toBe("Percentage Calculated Summary: Person 1 - A test schema to demo Calculated Summary"); + }); + + it("Given I complete every question, When I get to the number summary, Then I should see the correct total", async () => { + // Totals and titles should be shown + await click(UnitTotalPlaybackPage.submit()); + await expect(await $(UnitTotalPlaybackPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total of number values entered to be 124.58. Is this correct?", + ); + await expect(await $(UnitTotalPlaybackPage.calculatedSummaryQuestion()).getText()).toBe("Grand total of previous values"); + await expect(await $(UnitTotalPlaybackPage.calculatedSummaryAnswer()).getText()).toBe("124.58"); + + // Answers included in calculation should be shown + await expect(await $(NumberTotalPlaybackPage.fifthNumberAnswerLabel()).getText()).toBe("Fifth answer label number total"); + await expect(await $(NumberTotalPlaybackPage.fifthNumberAnswer()).getText()).toBe("78.91"); + await expect(await $(NumberTotalPlaybackPage.sixthNumberAnswerLabel()).getText()).toBe("Sixth answer label number total"); + await expect(await $(NumberTotalPlaybackPage.sixthNumberAnswer()).getText()).toBe("45.67"); + }); + + it("Given I have a calculated summary total that is used as a placeholder in another calculated summary, When I get to the calculated summary page displaying the placeholder, Then I should see the correct total", async () => { + await click(NumberTotalPlaybackPage.submit()); + await verifyUrlContains(BreakdownPage.pageName); + await $(BreakdownPage.answer1()).setValue(100.0); + await $(BreakdownPage.answer2()).setValue(24.58); + await click(BreakdownPage.submit()); + await verifyUrlContains(SecondCurrencyTotalPlaybackPage.pageName); + await expect(await $(SecondCurrencyTotalPlaybackPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total of number values entered to be ÂŖ124.58. Is this correct?", + ); + await expect(await $("body").getText()).toContain("Enter two values that add up to the previous calculated summary total of ÂŖ124.58"); + await expect(await $(SecondCurrencyTotalPlaybackPage.calculatedSummaryAnswer()).getText()).toBe("ÂŖ124.58"); + }); + + it("Given I complete every calculated summary, When I go to a page with calculated summary piping, Then I should the see the piped calculated summary total for each summary", async () => { + await click(SecondCurrencyTotalPlaybackPage.submit()); + + const content = $("h1 + ul").getText(); + const textsToAssert = ["Total currency values: ÂŖ9.36", "Total unit values: 1,467", "Total percentage values: 79", "Total number values: 124.58"]; + + textsToAssert.forEach(async (text) => await expect(content).toBe(text)); + }); + + it("Given I have an answer minimum based on a calculated summary total, When I enter an invalid answer, Then I should see an error message on the page", async () => { + await click(CalculatedSummaryTotalConfirmation.submit()); + await verifyUrlContains(SetMinMaxBlockPage.pageName); + await $(SetMinMaxBlockPage.setMinimum()).setValue(8.0); + await click(SetMinMaxBlockPage.submit()); + await expect(await $(SetMinMaxBlockPage.errorNumber(1)).getText()).toBe("Enter an answer more than or equal to ÂŖ9.36"); + await $(SetMinMaxBlockPage.setMinimum()).setValue(10.0); + }); + + it("Given I have an answer maximum based on a calculated summary total, When I enter an invalid answer, Then I should see an error message on the page", async () => { + await $(SetMinMaxBlockPage.setMaximum()).setValue(10.0); + await click(SetMinMaxBlockPage.submit()); + await expect(await $(SetMinMaxBlockPage.errorNumber(1)).getText()).toBe("Enter an answer less than or equal to ÂŖ9.36"); + await $(SetMinMaxBlockPage.setMaximum()).setValue(7.0); + await click(SetMinMaxBlockPage.submit()); + }); + + it("Given I confirm the totals and am on the summary, When I edit and change an answer, Then I go to each incomplete page in turn before I return to the summary", async () => { + await verifyUrlContains(SubmitPage.pageName); + await $(SubmitPage.thirdNumberAnswerEdit()).click(); + await $(ThirdNumberBlockPage.thirdNumber()).setValue(3.5); + await click(ThirdNumberBlockPage.submit()); + + // first incomplete block + await verifyUrlContains(CurrencyTotalPlaybackPage.pageName); + await expect(await $(CurrencyTotalPlaybackPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total of currency values entered to be ÂŖ9.41. Is this correct?", + ); + await click(CurrencyTotalPlaybackPage.submit()); + + // second incomplete block + await verifyUrlContains(SetMinMaxBlockPage.pageName); + await $(SetMinMaxBlockPage.setMinimum()).setValue(10.0); + await $(SetMinMaxBlockPage.setMaximum()).setValue(9.0); + await click(SetMinMaxBlockPage.submit()); + + // back to summary + await verifyUrlContains(SubmitPage.pageName); + }); + + it("Given I confirm the totals and am on the summary, When I edit and change an answer that has a dependent minimum value from a calculated summary total, And the minimum value has been changed, Then I must re-validate before I get to the summary", async () => { + await verifyUrlContains(SubmitPage.pageName); + await $(SubmitPage.thirdNumberAnswerEdit()).click(); + await $(ThirdNumberBlockPage.thirdNumber()).setValue(10.0); + await click(ThirdNumberBlockPage.submit()); + await verifyUrlContains(CurrencyTotalPlaybackPage.pageName); + await expect(await $(CurrencyTotalPlaybackPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total of currency values entered to be ÂŖ15.91. Is this correct?", + ); + await click(CurrencyTotalPlaybackPage.submit()); + await verifyUrlContains(SetMinMaxBlockPage.pageName); + await click(SetMinMaxBlockPage.submit()); + await expect(await $(SetMinMaxBlockPage.errorNumber(1)).getText()).toBe("Enter an answer more than or equal to ÂŖ15.91"); + await $(SetMinMaxBlockPage.setMinimum()).setValue(16.0); + await click(SetMinMaxBlockPage.submit()); + await verifyUrlContains(SubmitPage.pageName); + }); + + it("Given I confirm the totals and am on the summary, When I edit and change an answer that has a dependent maximum value from a calculated summary total, And the maximum value has been changed, Then I must re-validate before I get to the summary", async () => { + await verifyUrlContains(SubmitPage.pageName); + await $(SubmitPage.thirdNumberAnswerEdit()).click(); + await $(ThirdNumberBlockPage.thirdNumber()).setValue(1.0); + await click(ThirdNumberBlockPage.submit()); + await verifyUrlContains(CurrencyTotalPlaybackPage.pageName); + await expect(await $(CurrencyTotalPlaybackPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total of currency values entered to be ÂŖ6.91. Is this correct?", + ); + await click(CurrencyTotalPlaybackPage.submit()); + await verifyUrlContains(SetMinMaxBlockPage.pageName); + await click(SetMinMaxBlockPage.submit()); + await expect(await $(SetMinMaxBlockPage.errorNumber(1)).getText()).toBe("Enter an answer less than or equal to ÂŖ6.91"); + await $(SetMinMaxBlockPage.setMaximum()).setValue(6.0); + await click(SetMinMaxBlockPage.submit()); + await verifyUrlContains(SubmitPage.pageName); + }); + + it("Given I am on the summary, When I submit the questionnaire, Then I should see the thank you page", async () => { + await click(SubmitPage.submit()); + await click(HubPage.submit()); + await verifyUrlContains(ThankYouPage.pageName); + }); + }); + + describe("Given I have a Calculated Summary in a Repeating Section", () => { + before("Get to Final Summary", async () => { + await browser.openQuestionnaire("test_new_calculated_summary_repeating_section.json"); + await click(HubPage.submit()); + await $(PrimaryPersonListCollectorPage.no()).click(); + await click(PrimaryPersonListCollectorPage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Jean"); + await $(ListCollectorAddPage.lastName()).setValue("Clemens"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(ListCollectorAddPage.firstName()).setValue("Jane"); + await $(ListCollectorAddPage.lastName()).setValue("Doe"); + await click(ListCollectorAddPage.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await click(HubPage.submit()); + await getToFirstCalculatedSummary(); + await getToSubmitPage(); + await click(SubmitPage.submit()); + await click(HubPage.submit()); + await getToFirstCalculatedSummary(); + await getToSubmitPage(); + await click(SubmitPage.submit()); + }); + + it("Given I am on the submit page, When I have completed two repeating sections containing a calculated summary, Then the section status for both repeating sections should be complete", async () => { + await verifyUrlContains(HubPage.pageName); + await expect(await $(HubPage.summaryRowState("personal-details-section-1")).getText()).toBe("Completed"); + await expect(await $(HubPage.summaryRowState("personal-details-section-2")).getText()).toBe("Completed"); + }); + + it("Given I change an answer with a dependent calculated summary question, When I return to the hub, Then only the section status for the repeating section I updated should be incomplete", async () => { + await verifyUrlContains(HubPage.pageName); + await $(HubPage.summaryRowLink("personal-details-section-1")).click(); + await verifyUrlContains(SubmitPage.pageName); + await $(SubmitPage.skipFourthBlockAnswerEdit()).click(); + await $(SkipFourthBlockPage.yes()).click(); + await click(SkipFourthBlockPage.submit()); + await browser.url(HubPage.url()); + await expect(await $(HubPage.summaryRowState("personal-details-section-1")).getText()).toBe("Partially completed"); + await expect(await $(HubPage.summaryRowState("personal-details-section-2")).getText()).toBe("Completed"); + }); + + it("Given I return to a partially completed section with a calculated summary, When I answer the dependent questions and return to the hub, Then the section status for the repeating section I updated should be complete", async () => { + await verifyUrlContains(HubPage.pageName); + await expect(await $(HubPage.summaryRowState("personal-details-section-1")).getText()).toBe("Partially completed"); + await $(HubPage.summaryRowLink("personal-details-section-1")).click(); + await verifyUrlContains(SetMinMaxBlockPage.pageName); + await $(SetMinMaxBlockPage.setMinimum()).setValue(10.0); + await $(SetMinMaxBlockPage.setMaximum()).setValue(6.0); + await click(SetMinMaxBlockPage.submit()); + await click(SubmitPage.submit()); + await verifyUrlContains(HubPage.pageName); + await expect(await $(HubPage.summaryRowState("personal-details-section-1")).getText()).toBe("Completed"); + await expect(await $(HubPage.summaryRowState("personal-details-section-2")).getText()).toBe("Completed"); + }); + }); + + describe("Given I have a Calculated Summary in a Repeating Section with a Dependency based on a calculated summary in another section", () => { + before("Get to the Dependent question page", async () => { + await browser.openQuestionnaire("test_new_calculated_summary_cross_section_dependencies_repeating.json"); + await click(HubPage.submit()); + await $(PrimaryPersonListCollectorPage.yes()).click(); + await click(PrimaryPersonListCollectorPage.submit()); + await $(PrimaryPersonListCollectorAddPage.firstName()).setValue("Marcus"); + await $(PrimaryPersonListCollectorAddPage.lastName()).setValue("Twin"); + await click(PrimaryPersonListCollectorAddPage.submit()); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await click(HubPage.submit()); + + await $(SkipFirstNumberBlockPageSectionOne.no()).click(); + await click(SkipFirstNumberBlockPageSectionOne.submit()); + await $(FirstNumberBlockPageSectionOne.firstNumber()).setValue(10); + await click(FirstNumberBlockPageSectionOne.submit()); + await $(FirstAndAHalfNumberBlockPageSectionOne.firstAndAHalfNumberAlsoInTotal()).setValue(20); + await click(FirstAndAHalfNumberBlockPageSectionOne.submit()); + await $(SecondNumberBlockPageSectionOne.secondNumberAlsoInTotal()).setValue(30); + await click(SecondNumberBlockPageSectionOne.submit()); + await click(CalculatedSummarySectionOne.submit()); + await click(SectionSummarySectionOne.submit()); + await click(HubPage.submit()); + await $(ThirdNumberBlockPageSectionTwo.thirdNumber()).setValue(20); + await $(ThirdNumberBlockPageSectionTwo.thirdNumberAlsoInTotal()).setValue(20); + await click(ThirdNumberBlockPageSectionTwo.submit()); + await click(CalculatedSummarySectionTwo.submit()); + }); + + it("Given I have a placeholder displaying a calculated summary value source, When the calculated summary value is from a previous section, Then the value displayed should be correct", async () => { + await verifyUrlContains(DependencyQuestionSectionTwo.pageName); + await expect(await $(DependencyQuestionSectionTwo.checkboxAnswerCalcValue1Label()).getText()).toBe("60 - calculated summary answer (previous section)"); + await expect(await $(DependencyQuestionSectionTwo.checkboxAnswerCalcValue2Label()).getText()).toBe("40 - calculated summary answer (current section)"); + }); + + it("Given I have validation using a calculated summary value source, When the calculated summary value is from a previous section, Then the value used to validate should be correct", async () => { + await $(DependencyQuestionSectionTwo.checkboxAnswerCalcValue1()).click(); + await click(DependencyQuestionSectionTwo.submit()); + await verifyUrlContains(MinMaxSectionTwo.pageName); + await $(MinMaxSectionTwo.setMinimum()).setValue(59.0); + await $(MinMaxSectionTwo.setMaximum()).setValue(1.0); + await click(MinMaxSectionTwo.submit()); + await expect(await $(MinMaxSectionTwo.errorNumber(1)).getText()).toBe("Enter an answer more than or equal to ÂŖ60.00"); + await $(MinMaxSectionTwo.setMinimum()).setValue(61.0); + await $(MinMaxSectionTwo.setMaximum()).setValue(40.0); + await click(MinMaxSectionTwo.submit()); + }); + + it("Given I remove answers from the path for a calculated summary in a previous section by changing an answer, When I return to the question with the calculated summary value source, Then the value displayed should be correct", async () => { + await click(SectionSummarySectionTwo.submit()); + await $(HubPage.summaryRowLink("questions-section")).click(); + await $(SectionSummarySectionOne.skipFirstBlockAnswerEdit()).click(); + await $(SkipFirstNumberBlockPageSectionOne.yes()).click(); + await click(SkipFirstNumberBlockPageSectionOne.submit()); + await click(SectionSummarySectionOne.submit()); + await $(HubPage.summaryRowLink("calculated-summary-section-1")).click(); + await expect(await $("body").getText()).toContain("30 - calculated summary answer (previous section)"); + await $(SectionSummarySectionTwo.checkboxAnswerEdit()).click(); + await expect(await $(DependencyQuestionSectionTwo.checkboxAnswerCalcValue1Label()).getText()).toBe("30 - calculated summary answer (previous section)"); + await expect(await $(DependencyQuestionSectionTwo.checkboxAnswerCalcValue2Label()).getText()).toBe("40 - calculated summary answer (current section)"); + }); + }); +}); + +const getToFirstCalculatedSummary = async () => { + await $(FirstNumberBlockPage.firstNumber()).setValue(1.23); + await click(FirstNumberBlockPage.submit()); + + await $(SecondNumberBlockPage.secondNumber()).setValue(4.56); + await $(SecondNumberBlockPage.secondNumberUnitTotal()).setValue(789); + await $(SecondNumberBlockPage.secondNumberAlsoInTotal()).setValue(0.12); + await click(SecondNumberBlockPage.submit()); + + await $(ThirdNumberBlockPage.thirdNumber()).setValue(3.45); + await click(ThirdNumberBlockPage.submit()); + await $(ThirdAndAHalfNumberBlockPage.thirdAndAHalfNumberUnitTotal()).setValue(678); + await click(ThirdAndAHalfNumberBlockPage.submit()); + + await $(SkipFourthBlockPage.no()).click(); + await click(SkipFourthBlockPage.submit()); + + await $(FourthNumberBlockPage.fourthNumber()).setValue(9.01); + await click(FourthNumberBlockPage.submit()); + await $(FourthAndAHalfNumberBlockPage.fourthAndAHalfNumberAlsoInTotal()).setValue(2.34); + await click(FourthAndAHalfNumberBlockPage.submit()); + + await $(FifthNumberBlockPage.fifthPercent()).setValue(56); + await $(FifthNumberBlockPage.fifthNumber()).setValue(78.91); + await click(FifthNumberBlockPage.submit()); + + await $(SixthNumberBlockPage.sixthPercent()).setValue(23); + await $(SixthNumberBlockPage.sixthNumber()).setValue(45.67); + await click(SixthNumberBlockPage.submit()); +}; + +const getToSubmitPage = async () => { + await click(CurrencyTotalPlaybackPage.submit()); + await click(UnitTotalPlaybackPage.submit()); + await click(PercentageTotalPlaybackPage.submit()); + await click(NumberTotalPlaybackPage.submit()); + await $(BreakdownPage.answer1()).setValue(100.0); + await $(BreakdownPage.answer2()).setValue(24.58); + await click(BreakdownPage.submit()); + await click(SecondCurrencyTotalPlaybackPage.submit()); + await click(CalculatedSummaryTotalConfirmation.submit()); +}; diff --git a/tests/functional/spec/summaries/grand_calculated_summary/grand_calculated_summary_cross_section_dependencies.spec.js b/tests/functional/spec/summaries/grand_calculated_summary/grand_calculated_summary_cross_section_dependencies.spec.js new file mode 100644 index 0000000000..c7db1f1bfa --- /dev/null +++ b/tests/functional/spec/summaries/grand_calculated_summary/grand_calculated_summary_cross_section_dependencies.spec.js @@ -0,0 +1,140 @@ +import SkipFirstBlockPage from "../../../generated_pages/grand_calculated_summary_cross_section_dependencies/skip-first-block.page"; +import SecondNumberBlockPage from "../../../generated_pages/grand_calculated_summary_cross_section_dependencies/second-number-block.page"; +import HubPage from "../../../base_pages/hub.page"; +import CurrencySection1Page from "../../../generated_pages/grand_calculated_summary_cross_section_dependencies/currency-section-1.page"; +import QuestionsSectionSummaryPage from "../../../generated_pages/grand_calculated_summary_cross_section_dependencies/questions-section-summary.page"; +import ThirdNumberBlockPage from "../../../generated_pages/grand_calculated_summary_cross_section_dependencies/third-number-block.page"; +import SkipCalculatedSummaryPage from "../../../generated_pages/grand_calculated_summary_cross_section_dependencies/skip-calculated-summary.page"; +import CalculatedSummarySectionSummaryPage from "../../../generated_pages/grand_calculated_summary_cross_section_dependencies/calculated-summary-section-summary.page"; +import CurrencyQuestion3Page from "../../../generated_pages/grand_calculated_summary_cross_section_dependencies/currency-question-3.page"; +import CurrencyAllPage from "../../../generated_pages/grand_calculated_summary_cross_section_dependencies/currency-all.page"; +import FirstNumberBlockPartAPage from "../../../generated_pages/grand_calculated_summary_cross_section_dependencies/first-number-block-part-a.page"; +import FourthNumberBlockPage from "../../../generated_pages/grand_calculated_summary_cross_section_dependencies/fourth-number-block.page"; +import tvChoiceBlockPage from "../../../generated_pages/grand_calculated_summary_cross_section_dependencies/tv-choice-block.page"; +import { click, verifyUrlContains } from "../../../helpers"; +import { expect } from "@wdio/globals"; + +describe("Feature: Grand Calculated Summary", () => { + describe("Given I have a Grand Calculated Summary", () => { + before("Getting to the second calculated summary", async () => { + await browser.openQuestionnaire("test_grand_calculated_summary_cross_section_dependencies.json"); + await click(HubPage.submit()); + await $(SkipFirstBlockPage.no()).click(); + await click(SkipFirstBlockPage.submit()); + await $(FirstNumberBlockPartAPage.firstNumberA()).setValue(300); + await click(FirstNumberBlockPartAPage.submit()); + await $(SecondNumberBlockPage.secondNumberA()).setValue(10); + await $(SecondNumberBlockPage.secondNumberB()).setValue(5); + await $(SecondNumberBlockPage.secondNumberC()).setValue(15); + await click(SecondNumberBlockPage.submit()); + await click(CurrencySection1Page.submit()); + await click(QuestionsSectionSummaryPage.submit()); + // section 2 + await click(HubPage.submit()); + await $(ThirdNumberBlockPage.thirdNumberPartA()).setValue(70); + await click(ThirdNumberBlockPage.submit()); + }); + it("Given I don't skip the second calculated summary, it is included in the grand calculated summary", async () => { + await $(SkipCalculatedSummaryPage.no()).click(); + await click(SkipCalculatedSummaryPage.submit()); + await click(CurrencyQuestion3Page.submit()); + await $(tvChoiceBlockPage.television()).click(); + await click(tvChoiceBlockPage.submit()); + await click(CalculatedSummarySectionSummaryPage.submit()); + await click(HubPage.submit()); + await expect(await $(CurrencyAllPage.currencySection1()).getText()).toBe("ÂŖ330.00"); + await expect(await $(CurrencyAllPage.currencyQuestion3()).getText()).toBe("ÂŖ70.00"); + await expect(await $(CurrencyAllPage.grandCalculatedSummaryTitle()).getText()).toBe( + "The grand calculated summary is calculated to be ÂŖ400.00. Is this correct?", + ); + await click(CurrencyAllPage.submit()); + }); + it("Given I go back and skip the second calculated summary, it is not included in the grand calculated summary", async () => { + await $(HubPage.summaryRowLink("calculated-summary-section")).click(); + await $(CalculatedSummarySectionSummaryPage.skipAnswer2Edit()).click(); + await $(SkipCalculatedSummaryPage.yes()).click(); + await click(SkipCalculatedSummaryPage.submit()); + await click(CalculatedSummarySectionSummaryPage.submit()); + // Currently the grand calculated summary remains 'Completed' because none of the answers have changed + await expect(await $(HubPage.summaryRowState("grand-calculated-summary-section")).getText()).toBe("Completed"); + await $(HubPage.summaryRowLink("grand-calculated-summary-section")).click(); + await expect(await $(CurrencyAllPage.grandCalculatedSummaryTitle()).getText()).toBe( + "The grand calculated summary is calculated to be ÂŖ330.00. Is this correct?", + ); + await expect(await $(CurrencyAllPage.currencyQuestion3()).isExisting()).toBe(false); + }); + it("Given I confirm the grand calculated summary, then edit an answer for question 3, the grand calculated summary updates to be incomplete, because this is a dependency", async () => { + await click(CurrencyAllPage.submit()); + await $(HubPage.summaryRowLink("calculated-summary-section")).click(); + await $(CalculatedSummarySectionSummaryPage.thirdNumberAnswerPartAEdit()).click(); + await $(ThirdNumberBlockPage.thirdNumberPartA()).setValue(130); + await click(ThirdNumberBlockPage.submit()); + await click(CalculatedSummarySectionSummaryPage.submit()); + // Although the calculated summary is not on the path, the answer is still a grand calculated summary dependency, so it updates progress + await expect(await $(HubPage.summaryRowState("grand-calculated-summary-section")).getText()).toBe("Partially completed"); + await $(HubPage.summaryRowLink("grand-calculated-summary-section")).click(); + await expect(await $(CurrencyAllPage.grandCalculatedSummaryTitle()).getText()).toBe( + "The grand calculated summary is calculated to be ÂŖ330.00. Is this correct?", + ); + await expect(await $(CurrencyAllPage.currencyQuestion3()).isExisting()).toBe(false); + await click(CurrencyAllPage.submit()); + }); + it("Given I change my response to include the calculated summary, When I press continue, Then I am routed to the new block that opens up", async () => { + await $(HubPage.summaryRowLink("calculated-summary-section")).click(); + await $(CalculatedSummarySectionSummaryPage.skipAnswer2Edit()).click(); + await $(SkipCalculatedSummaryPage.no()).click(); + await click(SkipCalculatedSummaryPage.submit()); + await verifyUrlContains(CurrencyQuestion3Page.pageName); + }); + it("Given I confirm the calculated summary and the blocks following it are already complete, When I press submit, Then I am returned to the section summary anchored to the answer I edited initially", async () => { + await click(CurrencyQuestion3Page.submit()); + await verifyUrlContains("calculated-summary-section/#skip-answer-2"); + }); + it("Given I change an answer, When I press previous from the now incomplete calculated summary, Then I am routed to the block before the calculated summary", async () => { + await $(CalculatedSummarySectionSummaryPage.thirdNumberAnswerPartAEdit()).click(); + await $(ThirdNumberBlockPage.thirdNumberPartA()).setValue(120); + await click(ThirdNumberBlockPage.submit()); + await verifyUrlContains(CurrencyQuestion3Page.pageName); + await $(CurrencyQuestion3Page.previous()).click(); + await verifyUrlContains(SkipCalculatedSummaryPage.pageName); + }); + it("Given I complete the section, When I go back to the grand calculated summary, Then I see the new calculated summary included", async () => { + await click(SkipCalculatedSummaryPage.submit()); + await click(CurrencyQuestion3Page.submit()); + await click(CalculatedSummarySectionSummaryPage.submit()); + await expect(await $(HubPage.summaryRowState("grand-calculated-summary-section")).getText()).toBe("Partially completed"); + await $(HubPage.summaryRowLink("grand-calculated-summary-section")).click(); + await expect(await $(CurrencyAllPage.grandCalculatedSummaryTitle()).getText()).toBe( + "The grand calculated summary is calculated to be ÂŖ450.00. Is this correct?", + ); + }); + it("Given I provide an answer to question 3b from the grand calculated summary, this opens up an additional question, and when I press continue I am taken to this question first, then the calculated summary, and then the grand calculated summary", async () => { + await $(CurrencyAllPage.currencyQuestion3Edit()).click(); + await $(CurrencyQuestion3Page.thirdNumberAnswerPartBEdit()).click(); + await $(ThirdNumberBlockPage.thirdNumberPartB()).setValue(10); + await click(ThirdNumberBlockPage.submit()); + await verifyUrlContains(FourthNumberBlockPage.pageName); + await $(FourthNumberBlockPage.fourthNumber()).setValue(1); + await click(FourthNumberBlockPage.submit()); + await verifyUrlContains(CurrencyQuestion3Page.pageName); + await click(CurrencyQuestion3Page.submit()); + await verifyUrlContains(CurrencyAllPage.pageName); + await expect(await $(CurrencyAllPage.grandCalculatedSummaryTitle()).getText()).toBe( + "The grand calculated summary is calculated to be ÂŖ461.00. Is this correct?", + ); + await click(CurrencyAllPage.submit()); + }); + it("Given I go back to section one and skip the first block, it is not included in the first calculated summary and consequently not included in the grand calculated summary", async () => { + await $(HubPage.summaryRowLink("questions-section")).click(); + await $(QuestionsSectionSummaryPage.skipAnswer1Edit()).click(); + await $(SkipFirstBlockPage.yes()).click(); + await click(SkipFirstBlockPage.submit()); + await click(QuestionsSectionSummaryPage.submit()); + await $(HubPage.summaryRowLink("grand-calculated-summary-section")).click(); + await expect(await $(CurrencyAllPage.currencySection1()).getText()).toBe("ÂŖ30.00"); + await expect(await $(CurrencyAllPage.grandCalculatedSummaryTitle()).getText()).toBe( + "The grand calculated summary is calculated to be ÂŖ161.00. Is this correct?", + ); + }); + }); +}); diff --git a/tests/functional/spec/summaries/grand_calculated_summary/grand_calculated_summary_inside_repeating_section.spec.js b/tests/functional/spec/summaries/grand_calculated_summary/grand_calculated_summary_inside_repeating_section.spec.js new file mode 100644 index 0000000000..da64efe138 --- /dev/null +++ b/tests/functional/spec/summaries/grand_calculated_summary/grand_calculated_summary_inside_repeating_section.spec.js @@ -0,0 +1,441 @@ +import { assertSummaryValues, click, listItemIds, verifyUrlContains } from "../../../helpers"; +import { expect } from "@wdio/globals"; +import AddVehiclePage from "../../../generated_pages/grand_calculated_summary_inside_repeating_section/list-collector-add.page.js"; +import AnyCostPage from "../../../generated_pages/grand_calculated_summary_inside_repeating_section/any-cost.page.js"; +import AnyVehiclePage from "../../../generated_pages/grand_calculated_summary_inside_repeating_section/any-vehicle.page.js"; +import BaseCostPaymentBreakdownPage from "../../../generated_pages/grand_calculated_summary_inside_repeating_section/base-cost-payment-breakdown.page"; +import BaseCostsSectionPage from "../../../generated_pages/grand_calculated_summary_inside_repeating_section/base-costs-section-summary.page.js"; +import CalculatedSummaryBaseCostPage from "../../../generated_pages/grand_calculated_summary_inside_repeating_section/calculated-summary-base-cost.page.js"; +import CalculatedSummaryRunningCostPage from "../../../generated_pages/grand_calculated_summary_inside_repeating_section/calculated-summary-running-cost.page.js"; +import CostRepeatingBlock1RepeatingBlockPage from "../../../generated_pages/grand_calculated_summary_inside_repeating_section/cost-repeating-block-1-repeating-block.page"; +import DynamicCostBlockPage from "../../../generated_pages/grand_calculated_summary_inside_repeating_section/dynamic-cost-block.page"; +import FinanceCostPage from "../../../generated_pages/grand_calculated_summary_inside_repeating_section/finance-cost.page"; +import GcsBreakdownBlockPage from "../../../generated_pages/grand_calculated_summary_inside_repeating_section/gcs-breakdown-block.page"; +import GcsPipingPage from "../../../generated_pages/grand_calculated_summary_inside_repeating_section/gcs-piping.page"; +import GrandCalculatedSummaryVehiclePage from "../../../generated_pages/grand_calculated_summary_inside_repeating_section/grand-calculated-summary-vehicle.page.js"; +import HubPage from "../../../base_pages/hub.page"; +import ListCollectorCostAddPage from "../../../generated_pages/grand_calculated_summary_inside_repeating_section/list-collector-cost-add.page"; +import ListCollectorCostPage from "../../../generated_pages/grand_calculated_summary_inside_repeating_section/list-collector-cost.page"; +import ListCollectorCostRemovePage from "../../../generated_pages/grand_calculated_summary_inside_repeating_section/list-collector-cost-remove.page"; +import ListCollectorPage from "../../../generated_pages/grand_calculated_summary_inside_repeating_section/list-collector.page.js"; +import VehicleDetailsSectionPage from "../../../generated_pages/grand_calculated_summary_inside_repeating_section/vehicle-details-section-summary.page.js"; +import VehicleFuelBlockPage from "../../../generated_pages/grand_calculated_summary_inside_repeating_section/vehicle-fuel-block.page.js"; +import VehicleMaintenanceBlockPage from "../../../generated_pages/grand_calculated_summary_inside_repeating_section/vehicle-maintenance-block.page.js"; +import VehiclesSectionPage from "../../../generated_pages/grand_calculated_summary_inside_repeating_section/vehicles-section-summary.page.js"; + +describe("Grand Calculated Summary inside a repeating section", () => { + let vehicleListItemIds = []; + let costListItemIds = []; + const summaryActions = 'dd[class="ons-summary__actions"]'; + const dynamicAnswerChangeLink = (answerIndex) => $$(summaryActions)[answerIndex].$("a"); + + before("Load the survey", async () => { + await browser.openQuestionnaire("test_grand_calculated_summary_inside_repeating_section.json"); + }); + + it("Given I have a Grand Calculated Summary inside a repeating section, When I reach it for the first list item, Then I see placeholder content rendered correctly", async () => { + await click(HubPage.submit()); + await $(AnyCostPage.yes()).click(); + await click(AnyCostPage.submit()); + await $(ListCollectorCostAddPage.costName()).selectByAttribute("value", "Road Tax"); + await click(ListCollectorCostAddPage.submit()); + await $(CostRepeatingBlock1RepeatingBlockPage.repeatingBlock1CostBase()).setValue(5); + await click(CostRepeatingBlock1RepeatingBlockPage.submit()); + await $(ListCollectorCostPage.yes()).click(); + await click(ListCollectorCostPage.submit()); + await $(ListCollectorCostAddPage.costName()).selectByAttribute("value", "Parking Permit"); + await click(ListCollectorCostAddPage.submit()); + await $(CostRepeatingBlock1RepeatingBlockPage.repeatingBlock1CostBase()).setValue(12); + await click(CostRepeatingBlock1RepeatingBlockPage.submit()); + costListItemIds = await listItemIds(); + await $(ListCollectorCostPage.no()).click(); + await click(ListCollectorCostPage.submit()); + await $$(DynamicCostBlockPage.inputs())[0].setValue(5); + await $$(DynamicCostBlockPage.inputs())[1].setValue(8); + await click(DynamicCostBlockPage.submit()); + await $(FinanceCostPage.answer()).setValue(60); + await click(FinanceCostPage.submit()); + await expect(await $(CalculatedSummaryBaseCostPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total base cost for any owned vehicle to be ÂŖ90.00. Is this correct?", + ); + await click(CalculatedSummaryBaseCostPage.submit()); + await $(BaseCostPaymentBreakdownPage.baseCredit()).setValue(30); + await $(BaseCostPaymentBreakdownPage.baseDebit()).setValue(40); + await click(BaseCostPaymentBreakdownPage.submit()); + await click(BaseCostsSectionPage.submit()); + await click(HubPage.submit()); + await $(AnyVehiclePage.yes()).click(); + await click(AnyVehiclePage.submit()); + await $(AddVehiclePage.vehicleName()).selectByAttribute("value", "Car"); + await click(AddVehiclePage.submit()); + await $(ListCollectorPage.yes()).click(); + await click(ListCollectorPage.submit()); + await $(AddVehiclePage.vehicleName()).selectByAttribute("value", "Van"); + await click(AddVehiclePage.submit()); + vehicleListItemIds = await listItemIds(); + await $(ListCollectorPage.no()).click(); + await click(ListCollectorPage.submit()); + await click(VehiclesSectionPage.submit()); + await click(HubPage.submit()); + await $(VehicleMaintenanceBlockPage.vehicleMaintenanceCost()).setValue(100); + await click(VehicleMaintenanceBlockPage.submit()); + await $(VehicleFuelBlockPage.vehicleFuelCost()).setValue(125); + await click(VehicleFuelBlockPage.submit()); + await expect(await $(CalculatedSummaryRunningCostPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the monthly running costs of your Car to be ÂŖ225.00. Is this correct?", + ); + await click(CalculatedSummaryRunningCostPage.submit()); + await verifyUrlContains(GrandCalculatedSummaryVehiclePage.pageName); + await expect(await $(GrandCalculatedSummaryVehiclePage.grandCalculatedSummaryTitle()).getText()).toBe( + "The total cost of owning and running your Car is calculated to be ÂŖ315.00. Is this correct?", + ); + await expect(await $(GrandCalculatedSummaryVehiclePage.calculatedSummaryBaseCostLabel()).getText()).toBe("Vehicle base cost"); + await expect(await $(GrandCalculatedSummaryVehiclePage.calculatedSummaryRunningCostLabel()).getText()).toBe("Monthly Car costs"); + await expect(await $(GrandCalculatedSummaryVehiclePage.grandCalculatedSummaryQuestion()).getText()).toBe("Grand total Car expenditure"); + await assertSummaryValues(["ÂŖ90.00", "ÂŖ225.00", "ÂŖ315.00"]); + }); + + it("Given I immediately use that Grand Calculated Summary for validation, When I enter a sum of values too high, Then I see an error message", async () => { + await click(GrandCalculatedSummaryVehiclePage.submit()); + await $(GcsBreakdownBlockPage.payDebit()).setValue(100); + await $(GcsBreakdownBlockPage.payCredit()).setValue(115); + await $(GcsBreakdownBlockPage.payOther()).setValue(200); + await click(GcsBreakdownBlockPage.submit()); + await expect(await $(GcsBreakdownBlockPage.errorNumber()).getText()).toBe("Enter answers that add up to 315"); + }); + + it("Given I enter a valid value for the Grand Calculated Summary breakdown, When I press continue, Then I see an Interstitial page with my values correctly piped in", async () => { + await $(GcsBreakdownBlockPage.payOther()).setValue(100); + await click(GcsBreakdownBlockPage.submit()); + await verifyUrlContains(GcsPipingPage.pageName); + await expect(await $("body").getText()).toContain("Monthly maintenance cost: ÂŖ100.00"); + await expect(await $("body").getText()).toContain("Monthly fuel cost: ÂŖ125.00"); + await expect(await $("body").getText()).toContain("Total base cost: ÂŖ90.00"); + await expect(await $("body").getText()).toContain("Total running cost: ÂŖ225.00"); + await expect(await $("body").getText()).toContain("Total owning and running cost: ÂŖ315.00"); + await expect(await $("body").getText()).toContain("Paid by debit card: ÂŖ100.00"); + await expect(await $("body").getText()).toContain("Paid by credit card: ÂŖ115.00"); + await expect(await $("body").getText()).toContain("Paid by other means: ÂŖ100.00"); + }); + + it("Given I have a Grand Calculated Summary inside a repeating section, When I reach it for the second list item, Then I see placeholder content rendered correctly", async () => { + await click(GcsPipingPage.submit()); + await click(VehicleDetailsSectionPage.submit()); + await click(HubPage.submit()); + await $(VehicleMaintenanceBlockPage.vehicleMaintenanceCost()).setValue(50); + await click(VehicleMaintenanceBlockPage.submit()); + await $(VehicleFuelBlockPage.vehicleFuelCost()).setValue(45); + await click(VehicleFuelBlockPage.submit()); + await expect(await $(CalculatedSummaryRunningCostPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the monthly running costs of your Van to be ÂŖ95.00. Is this correct?", + ); + await click(CalculatedSummaryRunningCostPage.submit()); + await verifyUrlContains(GrandCalculatedSummaryVehiclePage.pageName); + await expect(await $(GrandCalculatedSummaryVehiclePage.grandCalculatedSummaryTitle()).getText()).toBe( + "The total cost of owning and running your Van is calculated to be ÂŖ185.00. Is this correct?", + ); + await expect(await $(GrandCalculatedSummaryVehiclePage.calculatedSummaryBaseCostLabel()).getText()).toBe("Vehicle base cost"); + await expect(await $(GrandCalculatedSummaryVehiclePage.calculatedSummaryRunningCostLabel()).getText()).toBe("Monthly Van costs"); + await expect(await $(GrandCalculatedSummaryVehiclePage.grandCalculatedSummaryQuestion()).getText()).toBe("Grand total Van expenditure"); + await assertSummaryValues(["ÂŖ90.00", "ÂŖ95.00", "ÂŖ185.00"]); + }); + + it("Given I am at a Grand Summary inside a repeating section, When I click the change link for a repeating calculated summary, Then I am taken to the correct page", async () => { + await verifyUrlContains(`vehicles/${vehicleListItemIds[1]}/`); + await $(GrandCalculatedSummaryVehiclePage.calculatedSummaryRunningCostEdit()).click(); + await verifyUrlContains(CalculatedSummaryRunningCostPage.pageName); + await verifyUrlContains(`vehicles/${vehicleListItemIds[1]}/`); + }); + + it("Given I have used a change link for a repeating calculated summary, When I click the continue button, Then I am taken to the Grand Calculated Summary", async () => { + await click(CalculatedSummaryRunningCostPage.submit()); + await verifyUrlContains(GrandCalculatedSummaryVehiclePage.pageName); + await verifyUrlContains(`vehicles/${vehicleListItemIds[1]}/`); + }); + + it("Given I am at a Grand Summary inside a repeating section, When I click the change link for a non repeating calculated summary, Then I am taken to the correct page", async () => { + await $(GrandCalculatedSummaryVehiclePage.calculatedSummaryBaseCostEdit()).click(); + await verifyUrlContains(CalculatedSummaryBaseCostPage.pageName); + }); + + it("Given I have used a change link for a non repeating calculated summary from a repeating section, When I click the continue button, Then I am taken to the Grand Calculated Summary for the correct list item", async () => { + await click(CalculatedSummaryBaseCostPage.submit()); + await verifyUrlContains(GrandCalculatedSummaryVehiclePage.pageName); + await verifyUrlContains(`vehicles/${vehicleListItemIds[1]}/`); + }); + + it("Given I use a change link for a repeating calculated summary, When I use a change link there, Then pressing continue twice takes me back to the correct grand calculated summary", async () => { + await $(GrandCalculatedSummaryVehiclePage.calculatedSummaryRunningCostEdit()).click(); + await $(CalculatedSummaryRunningCostPage.vehicleMaintenanceCostEdit()).click(); + await verifyUrlContains(VehicleMaintenanceBlockPage.pageName); + await verifyUrlContains(`vehicles/${vehicleListItemIds[1]}/`); + await click(VehicleMaintenanceBlockPage.submit()); + await verifyUrlContains(CalculatedSummaryRunningCostPage.pageName); + await verifyUrlContains(`vehicles/${vehicleListItemIds[1]}/`); + await click(CalculatedSummaryRunningCostPage.submit()); + await verifyUrlContains(GrandCalculatedSummaryVehiclePage.pageName); + await verifyUrlContains(`vehicles/${vehicleListItemIds[1]}/`); + }); + + it("Given I use a change link for a non repeating calculated summary, When I use a change link there, Then pressing continue twice takes me back to the correct grand calculated summary", async () => { + await $(GrandCalculatedSummaryVehiclePage.calculatedSummaryBaseCostEdit()).click(); + await $(CalculatedSummaryBaseCostPage.financeCostAnswerEdit()).click(); + await verifyUrlContains(FinanceCostPage.pageName); + await verifyUrlContains(`return_to_list_item_id=${vehicleListItemIds[1]}`); + await click(FinanceCostPage.submit()); + await verifyUrlContains(CalculatedSummaryBaseCostPage.pageName); + await verifyUrlContains(`return_to_list_item_id=${vehicleListItemIds[1]}`); + await click(CalculatedSummaryBaseCostPage.submit()); + await verifyUrlContains(GrandCalculatedSummaryVehiclePage.pageName); + await verifyUrlContains(`vehicles/${vehicleListItemIds[1]}/`); + }); + + it("Given I change a non repeating answer which results in the section being incomplete, When I press continue, Then I go to the next incomplete location with the list item id preserved", async () => { + await $(GrandCalculatedSummaryVehiclePage.calculatedSummaryBaseCostEdit()).click(); + await $(CalculatedSummaryBaseCostPage.financeCostAnswerEdit()).click(); + await $(FinanceCostPage.answer()).setValue(70); + await click(FinanceCostPage.submit()); + await verifyUrlContains(CalculatedSummaryBaseCostPage.pageName); + await expect(await $(CalculatedSummaryBaseCostPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total base cost for any owned vehicle to be ÂŖ100.00. Is this correct?", + ); + await click(CalculatedSummaryBaseCostPage.submit()); + await verifyUrlContains(BaseCostPaymentBreakdownPage.pageName); + await verifyUrlContains(`return_to_list_item_id=${vehicleListItemIds[1]}`); + }); + + it("Given I have changed a non repeating answer, When I return to the Grand Calculated Summary, Then I see the correctly updated values", async () => { + await click(BaseCostPaymentBreakdownPage.submit()); + + await verifyUrlContains(GrandCalculatedSummaryVehiclePage.pageName); + await expect(await $(GrandCalculatedSummaryVehiclePage.grandCalculatedSummaryTitle()).getText()).toBe( + "The total cost of owning and running your Van is calculated to be ÂŖ195.00. Is this correct?", + ); + }); + + it("Given I change a repeating answer, When I return to the Grand Calculated Summary, Then I see the correctly updated values", async () => { + await $(GrandCalculatedSummaryVehiclePage.calculatedSummaryRunningCostEdit()).click(); + await $(CalculatedSummaryRunningCostPage.vehicleMaintenanceCostEdit()).click(); + await $(VehicleMaintenanceBlockPage.vehicleMaintenanceCost()).setValue(75); + await click(VehicleMaintenanceBlockPage.submit()); + await verifyUrlContains(CalculatedSummaryRunningCostPage.pageName); + await expect(await $(CalculatedSummaryRunningCostPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the monthly running costs of your Van to be ÂŖ120.00. Is this correct?", + ); + await click(CalculatedSummaryRunningCostPage.submit()); + await verifyUrlContains(GrandCalculatedSummaryVehiclePage.pageName); + await expect(await $(GrandCalculatedSummaryVehiclePage.grandCalculatedSummaryTitle()).getText()).toBe( + "The total cost of owning and running your Van is calculated to be ÂŖ220.00. Is this correct?", + ); + }); + + it("Given I have edited a static answer whilst completing the repeating section, When I return to the Hub and enter the other repeat, Then I see the breakdown block needs to be revisited", async () => { + await click(GrandCalculatedSummaryVehiclePage.submit()); + await $(GcsBreakdownBlockPage.payDebit()).setValue(100); + await $(GcsBreakdownBlockPage.payCredit()).setValue(110); + await $(GcsBreakdownBlockPage.payOther()).setValue(10); + await click(GcsBreakdownBlockPage.submit()); + await click(GcsPipingPage.submit()); + await click(VehicleDetailsSectionPage.submit()); + await $(HubPage.summaryRowLink("vehicle-details-section-1")).click(); + await click(GrandCalculatedSummaryVehiclePage.submit()); + await verifyUrlContains(GcsBreakdownBlockPage.pageName); + await expect(await $(GcsBreakdownBlockPage.questionText()).getText()).toBe("How do you pay for the monthly fees of ÂŖ325.00?"); + await $(GcsBreakdownBlockPage.payCredit()).setValue(125); + await click(GcsBreakdownBlockPage.submit()); + await click(VehicleDetailsSectionPage.submit()); + await expect(await $(HubPage.summaryRowState("vehicle-details-section-1")).getText()).toBe("Completed"); + await expect(await $(HubPage.summaryRowState("vehicle-details-section-2")).getText()).toBe("Completed"); + }); + + it("Given I edit the non-repeating calculated summary, When I return to the Hub, Then I see repeating sections are incomplete", async () => { + await $(HubPage.summaryRowLink("base-costs-section")).click(); + await $(BaseCostsSectionPage.financeCostAnswerEdit()).click(); + await $(FinanceCostPage.answer()).setValue(80); + await click(FinanceCostPage.submit()); + await click(CalculatedSummaryBaseCostPage.submit()); + await click(BaseCostPaymentBreakdownPage.submit()); + await click(BaseCostsSectionPage.submit()); + await expect(await $(HubPage.summaryRowState("vehicle-details-section-1")).getText()).toBe("Partially completed"); + await expect(await $(HubPage.summaryRowState("vehicle-details-section-2")).getText()).toBe("Partially completed"); + }); + + it("Given I have two partially complete repeating sections, When I press continue, Then I am taken straight to the grand calculated summary as it is the first incomplete block", async () => { + await click(HubPage.submit()); + await verifyUrlContains(GrandCalculatedSummaryVehiclePage.pageName); + await verifyUrlContains(`vehicles/${vehicleListItemIds[0]}/`); + await expect(await $(GrandCalculatedSummaryVehiclePage.grandCalculatedSummaryTitle()).getText()).toBe( + "The total cost of owning and running your Car is calculated to be ÂŖ335.00. Is this correct?", + ); + await click(GrandCalculatedSummaryVehiclePage.submit()); + await verifyUrlContains(GcsBreakdownBlockPage.pageName); + await $(GcsBreakdownBlockPage.payCredit()).setValue(135); + await click(GcsBreakdownBlockPage.submit()); + await click(VehicleDetailsSectionPage.submit()); + }); + + it("Given I've completed the first repeating section, When I press continue, I am taken straight to the grand calculated summary of the second repeat", async () => { + await click(HubPage.submit()); + await verifyUrlContains(GrandCalculatedSummaryVehiclePage.pageName); + await verifyUrlContains(`vehicles/${vehicleListItemIds[1]}/`); + await expect(await $(GrandCalculatedSummaryVehiclePage.grandCalculatedSummaryTitle()).getText()).toBe( + "The total cost of owning and running your Van is calculated to be ÂŖ230.00. Is this correct?", + ); + }); + + it("Given I go to the non-repeating calculated summary, When I click a change link for a dynamic answer and press continue twice, Then I go back to the Grand Calculated Summary for the correct list item", async () => { + await $(GrandCalculatedSummaryVehiclePage.calculatedSummaryBaseCostEdit()).click(); + await dynamicAnswerChangeLink(2).click(); + await verifyUrlContains(DynamicCostBlockPage.pageName); + await click(DynamicCostBlockPage.submit()); + await click(CalculatedSummaryBaseCostPage.submit()); + await verifyUrlContains(GrandCalculatedSummaryVehiclePage.pageName); + await verifyUrlContains(`vehicles/${vehicleListItemIds[1]}/`); + }); + + it("Given I go to the non-repeating calculated summary, When I click a change link for a repeating block answer and press continue twice, Then I go back to the Grand Calculated Summary for the correct list item", async () => { + await $(GrandCalculatedSummaryVehiclePage.calculatedSummaryBaseCostEdit()).click(); + await dynamicAnswerChangeLink(0).click(); + await verifyUrlContains(CostRepeatingBlock1RepeatingBlockPage.pageName); + await verifyUrlContains(`costs/${costListItemIds[0]}/`); + await click(CostRepeatingBlock1RepeatingBlockPage.submit()); + await click(CalculatedSummaryBaseCostPage.submit()); + await verifyUrlContains(GrandCalculatedSummaryVehiclePage.pageName); + await verifyUrlContains(`vehicles/${vehicleListItemIds[1]}/`); + }); + + it("Given I edit a dynamic answer from the non-repeating calculated summary, When I return to the Grand Calculated Summary, Then I see the correct total", async () => { + await $(GrandCalculatedSummaryVehiclePage.calculatedSummaryBaseCostEdit()).click(); + await dynamicAnswerChangeLink(3).click(); + await $$(DynamicCostBlockPage.inputs())[1].setValue(28); + await click(DynamicCostBlockPage.submit()); + await click(CalculatedSummaryBaseCostPage.submit()); + await click(BaseCostPaymentBreakdownPage.submit()); + await verifyUrlContains(GrandCalculatedSummaryVehiclePage.pageName); + await expect(await $(GrandCalculatedSummaryVehiclePage.grandCalculatedSummaryTitle()).getText()).toBe( + "The total cost of owning and running your Van is calculated to be ÂŖ250.00. Is this correct?", + ); + }); + + it("Given I edit a repeating block answer from the non-repeating calculated summary, When I return to the Grand Calculated Summary, Then I see the correct total", async () => { + await $(GrandCalculatedSummaryVehiclePage.calculatedSummaryBaseCostEdit()).click(); + await dynamicAnswerChangeLink(1).click(); + await verifyUrlContains(CostRepeatingBlock1RepeatingBlockPage.pageName); + await verifyUrlContains(`costs/${costListItemIds[1]}/`); + await $(CostRepeatingBlock1RepeatingBlockPage.repeatingBlock1CostBase()).setValue(22); + await click(CostRepeatingBlock1RepeatingBlockPage.submit()); + await click(CalculatedSummaryBaseCostPage.submit()); + await click(BaseCostPaymentBreakdownPage.submit()); + await verifyUrlContains(GrandCalculatedSummaryVehiclePage.pageName); + await expect(await $(GrandCalculatedSummaryVehiclePage.grandCalculatedSummaryTitle()).getText()).toBe( + "The total cost of owning and running your Van is calculated to be ÂŖ260.00. Is this correct?", + ); + }); + + it("Given I complete the Grand Calculated Summary, When I press continue, I am taken to the calculation question that depends on it and cant proceed till entering a valid breakdown", async () => { + await click(GrandCalculatedSummaryVehiclePage.submit()); + await verifyUrlContains(GcsBreakdownBlockPage.pageName); + await click(GcsBreakdownBlockPage.submit()); + await expect(await $(GcsBreakdownBlockPage.errorNumber()).getText()).toBe("Enter answers that add up to 260"); + await $(GcsBreakdownBlockPage.payOther()).setValue(50); + await click(GcsBreakdownBlockPage.submit()); + await verifyUrlContains(VehicleDetailsSectionPage.pageName); + }); + + it("Given I have changed a static calculated summary during the section, When I return to the Hub, Then I see the other repeating section is incomplete as it also uses this calculated summary", async () => { + await click(VehicleDetailsSectionPage.submit()); + await expect(await $(HubPage.summaryRowState("vehicle-details-section-1")).getText()).toBe("Partially completed"); + await expect(await $(HubPage.summaryRowState("vehicle-details-section-2")).getText()).toBe("Completed"); + }); + + it("Given I go to the other repeating section, When I enter the section, Then I see the grand calculated summary with correctly updated totals", async () => { + await click(HubPage.submit()); + await verifyUrlContains(GrandCalculatedSummaryVehiclePage.pageName); + await expect(await $(GrandCalculatedSummaryVehiclePage.grandCalculatedSummaryTitle()).getText()).toBe( + "The total cost of owning and running your Car is calculated to be ÂŖ365.00. Is this correct?", + ); + }); + + it("Given I the grand calculated summary has changed, When I confirm it, Then I see the breakdown question and need to update the values", async () => { + await click(GrandCalculatedSummaryVehiclePage.submit()); + await verifyUrlContains(GcsBreakdownBlockPage.pageName); + await $(GcsBreakdownBlockPage.payOther()).setValue(130); + await click(GcsBreakdownBlockPage.submit()); + await click(VehicleDetailsSectionPage.submit()); + }); + + it("Given I remove an item from the costs lists, When I return to the Hub, Then I see both repeating sections revert to partially complete", async () => { + await $(HubPage.summaryRowLink("base-costs-section")).click(); + await $(BaseCostsSectionPage.costsListRemoveLink(1)).click(); + await $(ListCollectorCostRemovePage.yes()).click(); + await click(ListCollectorCostRemovePage.submit()); + await click(DynamicCostBlockPage.submit()); + await expect(await $(CalculatedSummaryBaseCostPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total base cost for any owned vehicle to be ÂŖ130.00. Is this correct?", + ); + await click(CalculatedSummaryBaseCostPage.submit()); + await click(BaseCostPaymentBreakdownPage.submit()); + await click(BaseCostsSectionPage.submit()); + await expect(await $(HubPage.summaryRowState("vehicle-details-section-1")).getText()).toBe("Partially completed"); + await expect(await $(HubPage.summaryRowState("vehicle-details-section-2")).getText()).toBe("Partially completed"); + }); + + it("Given I revisit both repeating sections, When I start each, Then I see the grand calculated summary page with correct values and must update the breakdown after", async () => { + await click(HubPage.submit()); + await expect(await $(GrandCalculatedSummaryVehiclePage.grandCalculatedSummaryTitle()).getText()).toBe( + "The total cost of owning and running your Car is calculated to be ÂŖ355.00. Is this correct?", + ); + await click(GrandCalculatedSummaryVehiclePage.submit()); + await verifyUrlContains(GcsBreakdownBlockPage.pageName); + await $(GcsBreakdownBlockPage.payOther()).setValue(120); + await click(GcsBreakdownBlockPage.submit()); + await click(VehicleDetailsSectionPage.submit()); + await click(HubPage.submit()); + await expect(await $(GrandCalculatedSummaryVehiclePage.grandCalculatedSummaryTitle()).getText()).toBe( + "The total cost of owning and running your Van is calculated to be ÂŖ250.00. Is this correct?", + ); + await click(GrandCalculatedSummaryVehiclePage.submit()); + await verifyUrlContains(GcsBreakdownBlockPage.pageName); + await $(GcsBreakdownBlockPage.payOther()).setValue(40); + await click(GcsBreakdownBlockPage.submit()); + await click(VehicleDetailsSectionPage.submit()); + }); + + it("Given I add an item to the costs lists, When I return to the Hub, Then I see both repeating sections revert to partially complete", async () => { + await $(HubPage.summaryRowLink("base-costs-section")).click(); + await $(BaseCostsSectionPage.costsListAddLink()).click(); + await $(ListCollectorCostAddPage.costName()).selectByAttribute("value", "Road Tax"); + await click(ListCollectorCostAddPage.submit()); + await $(CostRepeatingBlock1RepeatingBlockPage.repeatingBlock1CostBase()).setValue(30); + await click(CostRepeatingBlock1RepeatingBlockPage.submit()); + await $(ListCollectorCostPage.no()).click(); + await click(ListCollectorCostPage.submit()); + await $$(DynamicCostBlockPage.inputs())[1].setValue(20); + await click(DynamicCostBlockPage.submit()); + await expect(await $(CalculatedSummaryBaseCostPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total base cost for any owned vehicle to be ÂŖ180.00. Is this correct?", + ); + await click(CalculatedSummaryBaseCostPage.submit()); + await click(BaseCostPaymentBreakdownPage.submit()); + await click(BaseCostsSectionPage.submit()); + await expect(await $(HubPage.summaryRowState("vehicle-details-section-1")).getText()).toBe("Partially completed"); + await expect(await $(HubPage.summaryRowState("vehicle-details-section-2")).getText()).toBe("Partially completed"); + }); + + it("Given I revisit both repeating sections with new items, When I start each, Then I see the grand calculated summary page with correct values and the breakdown after", async () => { + await click(HubPage.submit()); + await expect(await $(GrandCalculatedSummaryVehiclePage.grandCalculatedSummaryTitle()).getText()).toBe( + "The total cost of owning and running your Car is calculated to be ÂŖ405.00. Is this correct?", + ); + await click(GrandCalculatedSummaryVehiclePage.submit()); + await $(GcsBreakdownBlockPage.payOther()).setValue(170); + await click(GcsBreakdownBlockPage.submit()); + await click(VehicleDetailsSectionPage.submit()); + await click(HubPage.submit()); + await expect(await $(GrandCalculatedSummaryVehiclePage.grandCalculatedSummaryTitle()).getText()).toBe( + "The total cost of owning and running your Van is calculated to be ÂŖ300.00. Is this correct?", + ); + await click(GrandCalculatedSummaryVehiclePage.submit()); + await verifyUrlContains(GcsBreakdownBlockPage.pageName); + }); +}); diff --git a/tests/functional/spec/summaries/grand_calculated_summary/grand_calculated_summary_overlapping_answers.spec.js b/tests/functional/spec/summaries/grand_calculated_summary/grand_calculated_summary_overlapping_answers.spec.js new file mode 100644 index 0000000000..9e176b898e --- /dev/null +++ b/tests/functional/spec/summaries/grand_calculated_summary/grand_calculated_summary_overlapping_answers.spec.js @@ -0,0 +1,161 @@ +import HubPage from "../../../base_pages/hub.page"; +import IntroductionBlockPage from "../../../generated_pages/grand_calculated_summary_overlapping_answers/introduction-block.page"; +import Block1Page from "../../../generated_pages/grand_calculated_summary_overlapping_answers/block-1.page"; +import Block2Page from "../../../generated_pages/grand_calculated_summary_overlapping_answers/block-2.page"; +import CalculatedSummary1Page from "../../../generated_pages/grand_calculated_summary_overlapping_answers/calculated-summary-1.page"; +import CalculatedSummary2Page from "../../../generated_pages/grand_calculated_summary_overlapping_answers/calculated-summary-2.page"; +import Block3Page from "../../../generated_pages/grand_calculated_summary_overlapping_answers/block-3.page"; +import CalculatedSummary4Page from "../../../generated_pages/grand_calculated_summary_overlapping_answers/calculated-summary-4.page"; +import GrandCalculatedSummaryShoppingPage from "../../../generated_pages/grand_calculated_summary_overlapping_answers/grand-calculated-summary-shopping.page"; +import Section1SummaryPage from "../../../generated_pages/grand_calculated_summary_overlapping_answers/section-1-summary.page"; +import { click, verifyUrlContains } from "../../../helpers"; +import { expect } from "@wdio/globals"; + +describe("Feature: Grand Calculated Summary", () => { + describe("Given I have a Grand Calculated Summary with overlapping answers", () => { + before("completing the survey", async () => { + await browser.openQuestionnaire("test_grand_calculated_summary_overlapping_answers.json"); + await click(IntroductionBlockPage.submit()); + + // grand calculated summary should not be enabled until section-1 complete + await expect(await $(HubPage.summaryRowLink("section-3")).isExisting()).toBe(false); + + await click(HubPage.submit()); + await $(Block1Page.q1A1()).setValue(100); + await $(Block1Page.q1A2()).setValue(200); + await click(Block1Page.submit()); + await $(Block2Page.q2A1()).setValue(10); + await $(Block2Page.q2A2()).setValue(20); + await click(Block2Page.submit()); + await click(CalculatedSummary1Page.submit()); + await click(CalculatedSummary2Page.submit()); + await $(Block3Page.yesExtraBreadAndCheese()).click(); + await click(Block3Page.submit()); + await click(CalculatedSummary4Page.submit()); + await click(Section1SummaryPage.submit()); + await click(HubPage.submit()); + await expect(await $(GrandCalculatedSummaryShoppingPage.grandCalculatedSummaryTitle()).getText()).toBe( + "Grand Calculated Summary of purchases this week comes to ÂŖ360.00. Is this correct?.", + ); + await click(GrandCalculatedSummaryShoppingPage.submit()); + }); + + it("Given I edit an answer that is only used in a single calculated summary, I am routed back to the calculated summary and then the grand calculated summary and the correct fields are focused", async () => { + await $(HubPage.summaryRowLink("section-3")).click(); + await $(GrandCalculatedSummaryShoppingPage.calculatedSummary2Edit()).click(); + await $(CalculatedSummary2Page.q1A2Edit()).click(); + await $(Block1Page.q1A2()).setValue(300); + await click(Block1Page.submit()); + + // taken back to calculated summary + await verifyUrlContains(CalculatedSummary2Page.pageName); + await verifyUrlContains( + "/questionnaire/calculated-summary-2/?return_to=grand-calculated-summary&return_to_block_id=grand-calculated-summary-shopping&return_to_answer_id=calculated-summary-2#q1-a2", + ); + await click(CalculatedSummary2Page.submit()); + + // then grand calculated summary + await verifyUrlContains(GrandCalculatedSummaryShoppingPage.pageName); + await expect(await $(GrandCalculatedSummaryShoppingPage.grandCalculatedSummaryTitle()).getText()).toBe( + "Grand Calculated Summary of purchases this week comes to ÂŖ460.00. Is this correct?.", + ); + await verifyUrlContains("/questionnaire/grand-calculated-summary-shopping/#calculated-summary-2"); + }); + + it("Given I edit an answer that is used in two calculated summaries, if I edit it from the first calculated summary change link, I taken through each block between the question and the second calculated summary before returning to the grand calculated summary", async () => { + await $(GrandCalculatedSummaryShoppingPage.calculatedSummary2Edit()).click(); + await $(CalculatedSummary2Page.q2A2Edit()).click(); + await $(Block2Page.q2A2()).setValue(400); + await click(Block2Page.submit()); + + // taken back to the FIRST calculated summary which uses it + await verifyUrlContains(CalculatedSummary2Page.pageName); + await expect(await $(CalculatedSummary2Page.calculatedSummaryTitle()).getText()).toBe( + "Total of eggs and cheese is calculated to be ÂŖ700.00. Is this correct?", + ); + await click(CalculatedSummary2Page.submit()); + + // taken back to the SECOND calculated summary which uses it + await verifyUrlContains(CalculatedSummary4Page.pageName); + await expect(await $(CalculatedSummary4Page.calculatedSummaryTitle()).getText()).toContain( + "Total extra items cost is calculated to be ÂŖ410.00. Is this correct?", + ); + await click(CalculatedSummary4Page.submit()); + + // then grand calculated summary + await verifyUrlContains(GrandCalculatedSummaryShoppingPage.pageName); + await expect(await $(GrandCalculatedSummaryShoppingPage.grandCalculatedSummaryTitle()).getText()).toContain( + "Grand Calculated Summary of purchases this week comes to ÂŖ1,220.00. Is this correct?", + ); + }); + + it("Given I edit an answer that is used in two calculated summaries, if I edit it from the second calculated summary change link, I taken through each block between the question and the second calculated summary before returning to the grand calculated summary", async () => { + await $(GrandCalculatedSummaryShoppingPage.calculatedSummary4Edit()).click(); + await $(CalculatedSummary4Page.q2A2Edit()).click(); + await $(Block2Page.q2A2()).setValue(500); + await click(Block2Page.submit()); + + // taken back to the FIRST calculated summary which uses it + await verifyUrlContains(CalculatedSummary2Page.pageName); + await expect(await $(CalculatedSummary2Page.calculatedSummaryTitle()).getText()).toBe( + "Total of eggs and cheese is calculated to be ÂŖ800.00. Is this correct?", + ); + await click(CalculatedSummary2Page.submit()); + + // taken back to the SECOND calculated summary which uses it + await verifyUrlContains(CalculatedSummary4Page.pageName); + await expect(await $(CalculatedSummary4Page.calculatedSummaryTitle()).getText()).toContain( + "Total extra items cost is calculated to be ÂŖ510.00. Is this correct?", + ); + await click(CalculatedSummary4Page.submit()); + + // then grand calculated summary + await verifyUrlContains(GrandCalculatedSummaryShoppingPage.pageName); + await expect(await $(GrandCalculatedSummaryShoppingPage.grandCalculatedSummaryTitle()).getText()).toContain( + "Grand Calculated Summary of purchases this week comes to ÂŖ1,420.00. Is this correct?", + ); + await click(GrandCalculatedSummaryShoppingPage.submit()); + }); + + it("Given I change an answer and return to the Hub before all calculated summaries are confirmed, the grand calculated summary section becomes inaccessible", async () => { + await $(HubPage.summaryRowLink("section-3")).click(); + await $(GrandCalculatedSummaryShoppingPage.calculatedSummary4Edit()).click(); + await $(CalculatedSummary4Page.q2A2Edit()).click(); + await $(Block2Page.q2A2()).setValue(50); + await click(Block2Page.submit()); + + // confirm one of the calculated summaries but return to the hub instead of confirming the other + await click(CalculatedSummary2Page.submit()); + await browser.url(HubPage.url()); + + // calculated summary 4 is not confirmed so GCS doesn't show + await expect(await $(HubPage.summaryRowState("section-1")).getText()).toBe("Partially completed"); + await expect(await $(HubPage.summaryRowLink("section-3")).isExisting()).toBe(false); + }); + + it("Given I complete the calculated and grand calculated summaries, When I return to the Hub, Then I see a new conditional section has opened up", async () => { + await click(HubPage.submit()); + await click(CalculatedSummary4Page.submit()); + await click(Section1SummaryPage.submit()); + await click(HubPage.submit()); + await expect(await $(GrandCalculatedSummaryShoppingPage.grandCalculatedSummaryTitle()).getText()).toContain( + "Grand Calculated Summary of purchases this week comes to ÂŖ520.00. Is this correct?", + ); + await click(GrandCalculatedSummaryShoppingPage.submit()); + await expect(await $(HubPage.summaryRowState("section-1")).getText()).toBe("Completed"); + await expect(await $(HubPage.summaryRowState("section-3")).getText()).toBe("Completed"); + await expect(await $(HubPage.summaryRowLink("section-4")).isExisting()).toBe(true); + }); + + it("Given I change my answer about purchasing additional items decreasing the gcs, When I return to the Hub, Then I see the conditional section is gone", async () => { + await $(HubPage.summaryRowLink("section-1")).click(); + await $(Section1SummaryPage.radioExtraEdit()).click(); + await $(Block3Page.no()).click(); + await click(Block3Page.submit()); + await click(Section1SummaryPage.submit()); + await expect(await $(HubPage.summaryRowState("section-1")).getText()).toBe("Completed"); + await expect(await $(HubPage.summaryRowState("section-3")).getText()).toBe("Completed"); + await expect(await $(HubPage.summaryRowLink("section-4")).isExisting()).toBe(false); + }); + }); +}); diff --git a/tests/functional/spec/summaries/grand_calculated_summary/grand_calculated_summary_repeating_answers.spec.js b/tests/functional/spec/summaries/grand_calculated_summary/grand_calculated_summary_repeating_answers.spec.js new file mode 100644 index 0000000000..90a1028c4f --- /dev/null +++ b/tests/functional/spec/summaries/grand_calculated_summary/grand_calculated_summary_repeating_answers.spec.js @@ -0,0 +1,529 @@ +import HubPage from "../../../base_pages/hub.page"; +import Block1Page from "../../../generated_pages/grand_calculated_summary_repeating_answers/block-1.page"; +import Block2Page from "../../../generated_pages/grand_calculated_summary_repeating_answers/block-2.page"; +import CalculatedSummary1Page from "../../../generated_pages/grand_calculated_summary_repeating_answers/calculated-summary-1.page"; +import Block3Page from "../../../generated_pages/grand_calculated_summary_repeating_answers/block-3.page"; +import Block4Page from "../../../generated_pages/grand_calculated_summary_repeating_answers/block-4.page"; +import CalculatedSummary2Page from "../../../generated_pages/grand_calculated_summary_repeating_answers/calculated-summary-2.page"; +import CalculatedSummary3Page from "../../../generated_pages/grand_calculated_summary_repeating_answers/calculated-summary-3.page"; +import CalculatedSummary4Page from "../../../generated_pages/grand_calculated_summary_repeating_answers/calculated-summary-4.page"; +import GrandCalculatedSummary1Page from "../../../generated_pages/grand_calculated_summary_repeating_answers/grand-calculated-summary-1.page"; +import GrandCalculatedSummary2Page from "../../../generated_pages/grand_calculated_summary_repeating_answers/grand-calculated-summary-2.page"; +import Section1SummaryPage from "../../../generated_pages/grand_calculated_summary_repeating_answers/section-1-summary.page"; +import AddUtilityBillPage from "../../../generated_pages/grand_calculated_summary_repeating_answers/any-other-utility-bills-add.page.js"; +import AnyOtherUtilityBillsPage from "../../../generated_pages/grand_calculated_summary_repeating_answers/any-other-utility-bills.page.js"; +import DynamicAnswerPage from "../../../generated_pages/grand_calculated_summary_repeating_answers/dynamic-answer.page.js"; +import CalculatedSummary5Page from "../../../generated_pages/grand_calculated_summary_repeating_answers/calculated-summary-5.page.js"; +import AnyStreamingServicesPage from "../../../generated_pages/grand_calculated_summary_repeating_answers/any-streaming-services.page.js"; +import AddStreamingServicePage from "../../../generated_pages/grand_calculated_summary_repeating_answers/any-other-streaming-services-add.page.js"; +import RemoveStreamingServicePage from "../../../generated_pages/grand_calculated_summary_repeating_answers/any-other-streaming-services-remove.page.js"; +import StreamingServiceRepeatingBlock1Page from "../../../generated_pages/grand_calculated_summary_repeating_answers/streaming-service-repeating-block-1-repeating-block.page.js"; +import StreamingServiceRepeatingBlock2Page from "../../../generated_pages/grand_calculated_summary_repeating_answers/streaming-service-repeating-block-2-repeating-block.page.js"; +import AnyOtherStreamingServicesPage from "../../../generated_pages/grand_calculated_summary_repeating_answers/any-other-streaming-services.page.js"; +import CalculatedSummary6Page from "../../../generated_pages/grand_calculated_summary_repeating_answers/calculated-summary-6.page.js"; +import CalculatedSummary7Page from "../../../generated_pages/grand_calculated_summary_repeating_answers/calculated-summary-7.page.js"; +import OtherInternetUsagePage from "../../../generated_pages/grand_calculated_summary_repeating_answers/other-internet-usage.page.js"; +import CalculatedSummary8Page from "../../../generated_pages/grand_calculated_summary_repeating_answers/calculated-summary-8.page.js"; +import GrandCalculatedSummary3Page from "../../../generated_pages/grand_calculated_summary_repeating_answers/grand-calculated-summary-3.page.js"; +import GrandCalculatedSummary4Page from "../../../generated_pages/grand_calculated_summary_repeating_answers/grand-calculated-summary-4.page.js"; +import GrandCalculatedSummary5Page from "../../../generated_pages/grand_calculated_summary_repeating_answers/grand-calculated-summary-5.page.js"; +import AnyUtilityBillsPage from "../../../generated_pages/grand_calculated_summary_repeating_answers/any-utility-bills.page"; +import Section4SummaryPage from "../../../generated_pages/grand_calculated_summary_repeating_answers/section-4-summary.page"; +import Section5SummaryPage from "../../../generated_pages/grand_calculated_summary_repeating_answers/section-5-summary.page"; +import { assertSummaryItems, assertSummaryValues, repeatingAnswerChangeLink, click, verifyUrlContains } from "../../../helpers"; +import { expect } from "@wdio/globals"; +import InternetBreakdownBlockPage from "../../../generated_pages/grand_calculated_summary_repeating_answers/internet-breakdown-block.page"; +import Section6SummaryPage from "../../../generated_pages/grand_calculated_summary_repeating_answers/section-6-summary.page"; +import PersonalExpenditureBlockPage from "../../../generated_pages/grand_calculated_summary_repeating_answers/personal-expenditure-block.page"; +import GrandCalculatedSummaryPipingPage from "../../../generated_pages/grand_calculated_summary_repeating_answers/grand-calculated-summary-piping.page"; + +describe("Feature: Grand Calculated Summary", () => { + const summaryRowTitles = ".ons-summary__row-title"; + + describe("Given I have a Grand Calculated Summary across multiple sections", () => { + before("Reaching the grand calculated summary section", async () => { + await browser.openQuestionnaire("test_grand_calculated_summary_repeating_answers.json"); + await click(HubPage.submit()); + + // complete 2 questions in section 1 + await $(Block1Page.q1A1()).setValue(10); + await $(Block1Page.q1A2()).setValue(20); + await click(Block1Page.submit()); + await $(Block2Page.q2A1()).setValue(30); + await $(Block2Page.q2A2()).setValue(40); + await click(Block2Page.submit()); + await click(CalculatedSummary1Page.submit()); + + // and the one for section 2 + await $(Block3Page.q3A1()).setValue(100); + await $(Block3Page.q3A2()).setValue(200); + await click(Block3Page.submit()); + await click(CalculatedSummary2Page.submit()); + await click(CalculatedSummary3Page.submit()); + await click(GrandCalculatedSummary1Page.submit()); + await click(Section1SummaryPage.submit()); + await click(HubPage.submit()); + await $(Block4Page.q4A1()).setValue(5); + await $(Block4Page.q4A2()).setValue(10); + await click(Block4Page.submit()); + await click(CalculatedSummary4Page.submit()); + await click(HubPage.submit()); + }); + + it("Given I click on the change link for a calculated summary, When I press continue, Then I am taken back to the grand calculated summary", async () => { + await expect(await $(GrandCalculatedSummary2Page.grandCalculatedSummaryTitle()).getText()).toBe( + "Grand Calculated Summary for shopping and entertainment is calculated to be ÂŖ415.00. Is this correct?", + ); + await $(GrandCalculatedSummary2Page.calculatedSummary1Edit()).click(); + await verifyUrlContains(CalculatedSummary1Page.pageName); + + await click(CalculatedSummary1Page.submit()); + await verifyUrlContains(GrandCalculatedSummary2Page.pageName); + }); + + it("Given I click on the change link for a calculated summary then one for an answer, When I press previous twice, I am return to the calculated summary then grand calculated summary", async () => { + await $(GrandCalculatedSummary2Page.calculatedSummary1Edit()).click(); + await $(CalculatedSummary1Page.q1A1Edit()).click(); + await verifyUrlContains(Block1Page.pageName); + + await $(Block1Page.previous()).click(); + await verifyUrlContains(CalculatedSummary1Page.pageName); + + await $(CalculatedSummary1Page.previous()).click(); + await verifyUrlContains(GrandCalculatedSummary2Page.pageName); + }); + + it("Given I go back to the calculated summary and then to a question and edit the answer. I am first taken back to the each calculated summary that uses the answer, the grand calculated summary in section 1, and then the updated grand calculated summary in section 3.", async () => { + await $(GrandCalculatedSummary2Page.calculatedSummary4Edit()).click(); + await expect(await $(CalculatedSummary4Page.calculatedSummaryTitle()).getText()).toBe( + "Calculated Summary for games expenditure is calculated to be ÂŖ15.00. Is this correct?", + ); + await $(CalculatedSummary4Page.q4A1Edit()).click(); + await verifyUrlContains(Block4Page.pageName); + + await $(Block4Page.q4A1()).setValue(50); + await click(Block4Page.submit()); + + // first taken back to the calculated summary which has updated + await verifyUrlContains(CalculatedSummary4Page.pageName); + await expect(await $(CalculatedSummary4Page.calculatedSummaryTitle()).getText()).toBe( + "Calculated Summary for games expenditure is calculated to be ÂŖ60.00. Is this correct?", + ); + await click(CalculatedSummary4Page.submit()); + + // then taken back to the grand calculated summary which has also been updated correctly + await verifyUrlContains(GrandCalculatedSummary2Page.pageName); + await expect(await $(GrandCalculatedSummary2Page.grandCalculatedSummaryTitle()).getText()).toBe( + "Grand Calculated Summary for shopping and entertainment is calculated to be ÂŖ460.00. Is this correct?", + ); + }); + + it("Given I go back to another calculated summary and edit multiple answers, I am still correctly routed back to the grand calculated summary", async () => { + await $(GrandCalculatedSummary2Page.calculatedSummary1Edit()).click(); + await expect(await $(CalculatedSummary1Page.calculatedSummaryTitle()).getText()).toBe( + "Calculated Summary for food expenditure is calculated to be ÂŖ100.00. Is this correct?", + ); + + // change first answer + await $(CalculatedSummary1Page.q1A1Edit()).click(); + await verifyUrlContains(Block1Page.pageName); + await $(Block1Page.q1A1()).setValue(100); + await click(Block1Page.submit()); + + // go to each calculated summary that uses the answer in turn, then each grand calculated summary up to the one we were editing + await verifyUrlContains(CalculatedSummary1Page.pageName); + await expect(await $(CalculatedSummary1Page.calculatedSummaryTitle()).getText()).toBe( + "Calculated Summary for food expenditure is calculated to be ÂŖ190.00. Is this correct?", + ); + + // change another answer + await $(CalculatedSummary1Page.q2A2Edit()).click(); + await verifyUrlContains(Block2Page.pageName); + await $(Block2Page.q2A2()).setValue(400); + await click(Block2Page.submit()); + + // back at updated calculated summary + await expect(await $(CalculatedSummary1Page.calculatedSummaryTitle()).getText()).toBe( + "Calculated Summary for food expenditure is calculated to be ÂŖ550.00. Is this correct?", + ); + + // Go to each calculated/grand calculated summary including this answer and reconfirm before being taken back to grand calculated summary + await click(CalculatedSummary1Page.submit()); + await verifyUrlContains(CalculatedSummary3Page.pageName); + await click(CalculatedSummary3Page.submit()); + await verifyUrlContains(GrandCalculatedSummary1Page.pageName); + await click(GrandCalculatedSummary1Page.submit()); + await verifyUrlContains(GrandCalculatedSummary2Page.pageName); + await expect(await $(GrandCalculatedSummary2Page.grandCalculatedSummaryTitle()).getText()).toBe( + "Grand Calculated Summary for shopping and entertainment is calculated to be ÂŖ910.00. Is this correct?", + ); + }); + + it("Given I edit an answer included in a grand calculated summary, the calculated summary sections should return to partially completed, and the grand calculated summary becomes unavailable.", async () => { + await click(GrandCalculatedSummary2Page.submit()); + await expect(await $(HubPage.summaryRowState("section-3")).getText()).toBe("Completed"); + + // Now edit an answer from section 2 and go back to the hub + await $(HubPage.summaryRowLink("section-3")).click(); + await $(GrandCalculatedSummary2Page.calculatedSummary4Edit()).click(); + await $(CalculatedSummary4Page.q4A1Edit()).click(); + await $(Block4Page.q4A1()).setValue(1); + await click(Block4Page.submit()); + await $(CalculatedSummary4Page.previous()).click(); + await $(Block4Page.previous()).click(); + + // calculated summary section should be in progress + await expect(await $(HubPage.summaryRowState("section-2")).getText()).toBe("Partially completed"); + await expect(await $(HubPage.summaryRowLink("section-3")).isExisting()).toBe(false); + }); + + it("Given I confirm the calculated summary, When I return to the Hub, Then I see the grand calculated summary come back marked as partially completed", async () => { + await $(HubPage.summaryRowLink("section-2")).click(); + await click(CalculatedSummary4Page.submit()); + await expect(await $(HubPage.summaryRowState("section-3")).getText()).toBe("Partially completed"); + }); + + it("Given I set both answers to block 4 to zero which removes the Grand Calculated Summary from the path, I am routed back to the Hub after the calculated summary", async () => { + await $(HubPage.summaryRowLink("section-3")).click(); + await $(GrandCalculatedSummary2Page.calculatedSummary4Edit()).click(); + await $(CalculatedSummary4Page.q4A1Edit()).click(); + await $(Block4Page.q4A1()).setValue(0); + await $(Block4Page.q4A2()).setValue(0); + await click(Block4Page.submit()); + await click(CalculatedSummary4Page.submit()); + // should be back at Hub, and grand calculated summary section not present + await verifyUrlContains(HubPage.pageName); + await expect(await $(HubPage.summaryRowLink("section-3")).isExisting()).toBe(false); + }); + + it("Given I have a grand calculated summary section requiring completion of all previous sections, When I complete each section in turn, Then I don't see the grand calculated summary until all sections are complete, at which point I see it on the Hub", async () => { + // no grand calculated summary section on the hub + await expect(await $(HubPage.summaryRowLink("section-6")).isExisting()).toBe(false); + + await click(HubPage.submit()); + await $(AnyUtilityBillsPage.yes()).click(); + await click(AnyUtilityBillsPage.submit()); + await $(AddUtilityBillPage.utilityBillName()).selectByAttribute("value", "Electricity"); + await click(AddUtilityBillPage.submit()); + await $(AnyOtherUtilityBillsPage.yes()).click(); + await click(AnyOtherUtilityBillsPage.submit()); + await $(AddUtilityBillPage.utilityBillName()).selectByAttribute("value", "Internet"); + await click(AddUtilityBillPage.submit()); + await $(AnyOtherUtilityBillsPage.yes()).click(); + await click(AnyOtherUtilityBillsPage.submit()); + await $(AddUtilityBillPage.utilityBillName()).selectByAttribute("value", "Gas"); + await click(AddUtilityBillPage.submit()); + await $(AnyOtherUtilityBillsPage.no()).click(); + await click(AnyOtherUtilityBillsPage.submit()); + await $$(DynamicAnswerPage.inputs())[0].setValue(150); + await $$(DynamicAnswerPage.inputs())[1].setValue(35); + await $$(DynamicAnswerPage.inputs())[2].setValue(65); + await click(DynamicAnswerPage.submit()); + await click(CalculatedSummary5Page.submit()); + await click(Section4SummaryPage.submit()); + // still no grand calculated summary yet + await expect(await $(HubPage.summaryRowLink("section-6")).isExisting()).toBe(false); + + await click(HubPage.submit()); + await $(AnyStreamingServicesPage.yes()).click(); + await click(AnyStreamingServicesPage.submit()); + await $(AddStreamingServicePage.streamingServiceName()).selectByAttribute("value", "Netflix"); + await click(AddStreamingServicePage.submit()); + await $(StreamingServiceRepeatingBlock1Page.streamingServiceMonthlyCost()).setValue(10); + await $(StreamingServiceRepeatingBlock1Page.streamingServiceExtraCost()).setValue(0); + await click(StreamingServiceRepeatingBlock1Page.submit()); + await $(StreamingServiceRepeatingBlock2Page.streamingServiceUsage()).setValue(20); + await click(StreamingServiceRepeatingBlock2Page.submit()); + await $(AnyOtherStreamingServicesPage.yes()).click(); + await click(AnyOtherStreamingServicesPage.submit()); + await $(AddStreamingServicePage.streamingServiceName()).selectByAttribute("value", "Prime video"); + await click(AddStreamingServicePage.submit()); + await $(StreamingServiceRepeatingBlock1Page.streamingServiceMonthlyCost()).setValue(8); + await $(StreamingServiceRepeatingBlock1Page.streamingServiceExtraCost()).setValue(12); + await click(StreamingServiceRepeatingBlock1Page.submit()); + await $(StreamingServiceRepeatingBlock2Page.streamingServiceUsage()).setValue(25); + await click(StreamingServiceRepeatingBlock2Page.submit()); + await $(AnyOtherStreamingServicesPage.no()).click(); + await click(AnyOtherStreamingServicesPage.submit()); + await click(CalculatedSummary6Page.submit()); + await click(CalculatedSummary7Page.submit()); + await $(OtherInternetUsagePage.mediaDownloads()).setValue(50); + await $(OtherInternetUsagePage.miscInternet()).setValue(5); + await click(OtherInternetUsagePage.submit()); + await click(CalculatedSummary8Page.submit()); + await click(Section5SummaryPage.submit()); + // grand calculated summary now present + await expect(await $(HubPage.summaryRowLink("section-6")).isExisting()).toBe(true); + await expect(await $(HubPage.summaryRowState("section-6")).getText()).toBe("Not started"); + }); + + it("Given I have a calculated summary of repeating answers and a calculated summary of dynamic answers, When I reach the grand calculated summary of both, Then I see the correct total and items", async () => { + await click(HubPage.submit()); + await expect(await $(GrandCalculatedSummary3Page.grandCalculatedSummaryTitle()).getText()).toBe( + "Grand Calculated Summary for monthly spending on bills and services is calculated to be ÂŖ280.00. Is this correct?", + ); + await assertSummaryValues(["ÂŖ250.00", "ÂŖ30.00", "ÂŖ280.00"]); + await assertSummaryItems([ + "Total monthly expenditure on utility bills", + "Total monthly expenditure on streaming services", + "Total monthly expenditure on bills and streaming services", + ]); + }); + + it("Given I have 2 calculated summaries of list repeating block answers, When I reach the grand calculated summary of both, Then I see the correct total and items", async () => { + await click(GrandCalculatedSummary3Page.submit()); + await expect(await $(GrandCalculatedSummary4Page.grandCalculatedSummaryTitle()).getText()).toBe( + "Grand Calculated Summary for internet usage is calculated to be 100 GB. Is this correct?", + ); + await assertSummaryValues(["45 GB", "55 GB", "100 GB"]); + await assertSummaryItems(["Total internet usage on streaming services", "Total internet usage on other services", "Total internet usage"]); + }); + + it("Given I have multiple calculated summaries of static, repeating and dynamic answers, When I reach the grand calculated summary of them all, Then I see the correct total and items", async () => { + await click(GrandCalculatedSummary4Page.submit()); + await expect(await $(GrandCalculatedSummary5Page.grandCalculatedSummaryTitle()).getText()).toBe( + "Grand Calculated Summary for total monthly household expenditure is calculated to be ÂŖ1,130.00. Is this correct?", + ); + await assertSummaryValues(["ÂŖ550.00", "ÂŖ300.00", "ÂŖ0.00", "ÂŖ250.00", "ÂŖ30.00", "ÂŖ1,130.00"]); + await assertSummaryItems([ + "Total monthly food expenditure", + "Total monthly clothes expenditure", + "Total games expenditure", + "Total monthly expenditure on utility bills", + "Total monthly expenditure on streaming services", + "Total monthly expenditure", + ]); + }); + + it("Given I a grand calculated summary featuring repeating answers, When I click edit links to return to a dynamic answer then previous twice, Then I return to the grand calculated summary where I started", async () => { + await $(GrandCalculatedSummary5Page.calculatedSummary5Edit()).click(); + await repeatingAnswerChangeLink(0).click(); + await verifyUrlContains(DynamicAnswerPage.pageName); + await $(DynamicAnswerPage.previous()).click(); + await $(CalculatedSummary5Page.previous()).click(); + await verifyUrlContains(GrandCalculatedSummary5Page.pageName); + }); + + it("Given I have a grand calculated summary featuring repeating answers, When I edit a dynamic answer, Then I return to the calculated summary to confirm, and then each affected grand calculated summary in turn", async () => { + await $(GrandCalculatedSummary5Page.calculatedSummary5Edit()).click(); + await repeatingAnswerChangeLink(1).click(); + await $$(DynamicAnswerPage.inputs())[0].setValue("175"); + await click(DynamicAnswerPage.submit()); + await click(CalculatedSummary5Page.submit()); + await verifyUrlContains(GrandCalculatedSummary3Page.pageName); + await expect(await $(GrandCalculatedSummary3Page.grandCalculatedSummaryTitle()).getText()).toBe( + "Grand Calculated Summary for monthly spending on bills and services is calculated to be ÂŖ305.00. Is this correct?", + ); + await click(GrandCalculatedSummary3Page.submit()); + await verifyUrlContains(GrandCalculatedSummary5Page.pageName); + await expect(await $(GrandCalculatedSummary5Page.grandCalculatedSummaryTitle()).getText()).toBe( + "Grand Calculated Summary for total monthly household expenditure is calculated to be ÂŖ1,155.00. Is this correct?", + ); + }); + + it("Given I have a grand calculated summary featuring repeating answers, When I click edit links to return to a list repeating block answer then previous twice, Then I return to the grand calculated summary anchored from where I started", async () => { + await $(GrandCalculatedSummary5Page.calculatedSummary6Edit()).click(); + await repeatingAnswerChangeLink(2).click(); + await $(StreamingServiceRepeatingBlock1Page.previous()).click(); + await $(CalculatedSummary5Page.previous()).click(); + await verifyUrlContains(GrandCalculatedSummary5Page.pageName); + }); + + it("Given I have a grand calculated summary featuring repeating answers, When I edit a list repeating block answer, Then I return to the calculated summary to confirm, and then the grand calculated summary to confirm", async () => { + await $(GrandCalculatedSummary5Page.calculatedSummary6Edit()).click(); + await repeatingAnswerChangeLink(2).click(); + await $(StreamingServiceRepeatingBlock1Page.streamingServiceMonthlyCost()).setValue(12); + await click(StreamingServiceRepeatingBlock1Page.submit()); + await click(CalculatedSummary5Page.submit()); + await verifyUrlContains(GrandCalculatedSummary3Page.pageName); + await expect(await $(GrandCalculatedSummary3Page.grandCalculatedSummaryTitle()).getText()).toBe( + "Grand Calculated Summary for monthly spending on bills and services is calculated to be ÂŖ309.00. Is this correct?", + ); + await click(GrandCalculatedSummary3Page.submit()); + await verifyUrlContains(GrandCalculatedSummary5Page.pageName); + await expect(await $(GrandCalculatedSummary5Page.grandCalculatedSummaryTitle()).getText()).toBe( + "Grand Calculated Summary for total monthly household expenditure is calculated to be ÂŖ1,159.00. Is this correct?", + ); + }); + + it("Given I pipe the grand calculated summary into the next question, When I press continue, Then I see the correct title", async () => { + await click(GrandCalculatedSummary5Page.submit()); + await expect(await $(InternetBreakdownBlockPage.questionTitle()).getText()).toContain("How did you use the 100 GB across your devices?"); + }); + + it("Given I use the grand calculated summary for validation, When I enter values with too large a sum, Then I see a validation error", async () => { + await $(InternetBreakdownBlockPage.internetPc()).setValue(60); + await $(InternetBreakdownBlockPage.internetPhone()).setValue(60); + await click(InternetBreakdownBlockPage.submit()); + await expect(await $(InternetBreakdownBlockPage.errorNumber(1)).getText()).toBe("Enter answers that add up to 100"); + }); + + it("Given I use the grand calculated summary for validation, When I enter values with the correct sum, Then I progress to the summary page", async () => { + await $(InternetBreakdownBlockPage.internetPhone()).setValue(40); + await click(InternetBreakdownBlockPage.submit()); + await verifyUrlContains(Section6SummaryPage.pageName); + await click(Section6SummaryPage.submit()); + }); + + it("Given I have a grand calculated summary featuring dynamic answers, When I add an item to the list collector and return to the hub, Then I see the section with dynamic answers is in progress, and the grand calculated summary section is not available", async () => { + await $(HubPage.summaryRowLink("section-4")).click(); + await $(Section4SummaryPage.utilityBillsListAddLink()).click(); + await $(AddUtilityBillPage.utilityBillName()).selectByAttribute("value", "Water"); + await click(AddUtilityBillPage.submit()); + await $(AnyOtherUtilityBillsPage.no()).click(); + await click(AnyOtherUtilityBillsPage.submit()); + await $$(DynamicAnswerPage.inputs())[3].setValue("40"); + await click(DynamicAnswerPage.submit()); + await browser.url(HubPage.url()); + await expect(await $(HubPage.summaryRowState("section-4")).getText()).toBe("Partially completed"); + await expect(await $(HubPage.summaryRowLink("section-6")).isExisting()).toBe(false); + }); + + it("Given I complete the in progress section, When I return to the Hub, Then I see the grand calculated summary section re-enabled, and partially completed", async () => { + await $(HubPage.summaryRowLink("section-4")).click(); + await expect(await $(CalculatedSummary5Page.calculatedSummaryTitle()).getText()).toBe( + "Calculated Summary for monthly spending on utility bills is calculated to be ÂŖ315.00. Is this correct?", + ); + await click(CalculatedSummary5Page.submit()); + await click(Section4SummaryPage.submit()); + await expect(await $(HubPage.summaryRowState("section-6")).getText()).toBe("Partially completed"); + }); + + it("Given I return to the grand calculated summary section, When I go to each grand calculated summary, Then I see the correct new values", async () => { + await $(HubPage.summaryRowLink("section-6")).click(); + await verifyUrlContains(GrandCalculatedSummary3Page.pageName); + await expect(await $(GrandCalculatedSummary3Page.grandCalculatedSummaryTitle()).getText()).toBe( + "Grand Calculated Summary for monthly spending on bills and services is calculated to be ÂŖ349.00. Is this correct?", + ); + await click(GrandCalculatedSummary3Page.submit()); + await verifyUrlContains(GrandCalculatedSummary4Page.pageName); + await expect(await $(GrandCalculatedSummary4Page.grandCalculatedSummaryTitle()).getText()).toBe( + "Grand Calculated Summary for internet usage is calculated to be 100 GB. Is this correct?", + ); + await click(GrandCalculatedSummary4Page.submit()); + await verifyUrlContains(GrandCalculatedSummary5Page.pageName); + await expect(await $(GrandCalculatedSummary5Page.grandCalculatedSummaryTitle()).getText()).toBe( + "Grand Calculated Summary for total monthly household expenditure is calculated to be ÂŖ1,199.00. Is this correct?", + ); + await click(GrandCalculatedSummary5Page.submit()); + await verifyUrlContains(Section6SummaryPage.pageName); + await expect(await $$(summaryRowTitles)[0].getText()).toBe("How did you use the 100 GB across your devices?"); + await click(Section6SummaryPage.submit()); + await expect(await $(HubPage.summaryRowState("section-6")).getText()).toBe("Completed"); + }); + + it("Given I add a list item for the section with list repeating blocks, When I return to the hub before and after completing the section, Then I see the grand calculated summary go from unavailable, to enabled and in progress", async () => { + await $(HubPage.summaryRowLink("section-5")).click(); + await $(Section5SummaryPage.streamingServicesListAddLink()).click(); + await $(AddStreamingServicePage.streamingServiceName()).selectByAttribute("value", "Disney+"); + await click(AddStreamingServicePage.submit()); + await $(StreamingServiceRepeatingBlock1Page.streamingServiceMonthlyCost()).setValue(10); + await click(StreamingServiceRepeatingBlock1Page.submit()); + await $(StreamingServiceRepeatingBlock2Page.streamingServiceUsage()).setValue(5); + await click(StreamingServiceRepeatingBlock2Page.submit()); + await $(AnyOtherStreamingServicesPage.no()).click(); + await click(AnyOtherStreamingServicesPage.submit()); + await expect(await $(CalculatedSummary6Page.calculatedSummaryTitle()).getText()).toBe( + "Calculated Summary for monthly expenditure on streaming services is calculated to be ÂŖ44.00. Is this correct?", + ); + await browser.url(HubPage.url()); + await expect(await $(HubPage.summaryRowState("section-5")).getText()).toBe("Partially completed"); + await expect(await $(HubPage.summaryRowLink("section-6")).isExisting()).toBe(false); + await $(HubPage.summaryRowLink("section-5")).click(); + await click(CalculatedSummary6Page.submit()); + await expect(await $(CalculatedSummary7Page.calculatedSummaryTitle()).getText()).toBe( + "Total monthly internet usage on streaming services is calculated to be 50 GB. Is this correct?", + ); + await click(CalculatedSummary7Page.submit()); + await click(Section5SummaryPage.submit()); + await expect(await $(HubPage.summaryRowState("section-6")).getText()).toBe("Partially completed"); + }); + + it("Given I the grand calculated summary section is now incomplete, When I return to the section, Then I am taken to each updated grand calculated summary to confirm the new total", async () => { + await $(HubPage.summaryRowLink("section-6")).click(); + await verifyUrlContains(GrandCalculatedSummary3Page.pageName); + await expect(await $(GrandCalculatedSummary3Page.grandCalculatedSummaryTitle()).getText()).toBe( + "Grand Calculated Summary for monthly spending on bills and services is calculated to be ÂŖ359.00. Is this correct?", + ); + await click(GrandCalculatedSummary3Page.submit()); + await verifyUrlContains(GrandCalculatedSummary4Page.pageName); + await expect(await $(GrandCalculatedSummary4Page.grandCalculatedSummaryTitle()).getText()).toBe( + "Grand Calculated Summary for internet usage is calculated to be 105 GB. Is this correct?", + ); + await click(GrandCalculatedSummary4Page.submit()); + await verifyUrlContains(GrandCalculatedSummary5Page.pageName); + await expect(await $(GrandCalculatedSummary5Page.grandCalculatedSummaryTitle()).getText()).toBe( + "Grand Calculated Summary for total monthly household expenditure is calculated to be ÂŖ1,209.00. Is this correct?", + ); + await click(GrandCalculatedSummary5Page.submit()); + await $(InternetBreakdownBlockPage.internetPhone()).setValue(45); + await click(InternetBreakdownBlockPage.submit()); + await click(Section6SummaryPage.submit()); + }); + + it("Given I remove a list item involved in the grand calculated summary, When I confirm, Then I am taken to each affected calculated summary to reconfirm, and when I return to the Hub the grand calculated summary is in progress", async () => { + await expect(await $(HubPage.summaryRowState("section-6")).getText()).toBe("Completed"); + await $(HubPage.summaryRowLink("section-5")).click(); + await $(Section5SummaryPage.streamingServicesListRemoveLink(1)).click(); + await $(RemoveStreamingServicePage.yes()).click(); + await click(RemoveStreamingServicePage.submit()); + await expect(await $(CalculatedSummary6Page.calculatedSummaryTitle()).getText()).toBe( + "Calculated Summary for monthly expenditure on streaming services is calculated to be ÂŖ34.00. Is this correct?", + ); + await click(CalculatedSummary6Page.submit()); + await expect(await $(CalculatedSummary7Page.calculatedSummaryTitle()).getText()).toBe( + "Total monthly internet usage on streaming services is calculated to be 30 GB. Is this correct?", + ); + await click(CalculatedSummary7Page.submit()); + await click(Section5SummaryPage.submit()); + await expect(await $(HubPage.summaryRowState("section-6")).getText()).toBe("Partially completed"); + }); + + it("Given the section has reverted to partially complete, When I go back to the section, Then I am taken to each grand calculated summary to reconfirm with correct values", async () => { + await $(HubPage.summaryRowLink("section-6")).click(); + await verifyUrlContains(GrandCalculatedSummary3Page.pageName); + await expect(await $(GrandCalculatedSummary3Page.grandCalculatedSummaryTitle()).getText()).toBe( + "Grand Calculated Summary for monthly spending on bills and services is calculated to be ÂŖ349.00. Is this correct?", + ); + await click(GrandCalculatedSummary3Page.submit()); + await verifyUrlContains(GrandCalculatedSummary4Page.pageName); + await expect(await $(GrandCalculatedSummary4Page.grandCalculatedSummaryTitle()).getText()).toBe( + "Grand Calculated Summary for internet usage is calculated to be 85 GB. Is this correct?", + ); + await click(GrandCalculatedSummary4Page.submit()); + await verifyUrlContains(GrandCalculatedSummary5Page.pageName); + await expect(await $(GrandCalculatedSummary5Page.grandCalculatedSummaryTitle()).getText()).toBe( + "Grand Calculated Summary for total monthly household expenditure is calculated to be ÂŖ1,199.00. Is this correct?", + ); + }); + + it("Given I have a further section depending on the grand calculated summary section, When I return to the Hub, Then I see the new section is available", async () => { + await click(GrandCalculatedSummary5Page.submit()); + await $(InternetBreakdownBlockPage.internetPhone()).setValue(25); + await click(InternetBreakdownBlockPage.submit()); + await click(Section6SummaryPage.submit()); + await expect(await $(HubPage.summaryRowState("section-7")).getText()).toBe("Not started"); + await click(HubPage.submit()); + }); + + it("Given I use a grand calculated summary value as a maximum, When I enter a value that is too large, Then I see a validation error", async () => { + await expect(await $(PersonalExpenditureBlockPage.questionTitle()).getText()).toContain( + "How much of the ÂŖ1,199.00 household expenditure do you contribute personally?", + ); + await $(PersonalExpenditureBlockPage.personalExpenditure()).setValue(1200); + await click(PersonalExpenditureBlockPage.submit()); + await expect(await $(PersonalExpenditureBlockPage.errorNumber(1)).getText()).toBe("Enter an answer less than or equal to ÂŖ1,199.00"); + }); + + it("Given I display multiple grand calculated summaries on an Interstitial page, When I reach the page, Then I see the correct values piped in", async () => { + await $(PersonalExpenditureBlockPage.personalExpenditure()).setValue(1100); + await click(PersonalExpenditureBlockPage.submit()); + await verifyUrlContains(GrandCalculatedSummaryPipingPage.pageName); + await expect(await $("body").getText()).toContain("Total household expenditure: ÂŖ1,199.00"); + await expect(await $("body").getText()).toContain("Personal contribution: ÂŖ1,100.00"); + await expect(await $("body").getText()).toContain("Total internet usage: 85 GB"); + await expect(await $("body").getText()).toContain("Usage by phone: 25 GB"); + await expect(await $("body").getText()).toContain("Usage by PC: 60 GB"); + }); + }); +}); diff --git a/tests/functional/spec/summaries/question_summary/custom_question_summary.spec.js b/tests/functional/spec/summaries/question_summary/custom_question_summary.spec.js new file mode 100644 index 0000000000..a78a46c300 --- /dev/null +++ b/tests/functional/spec/summaries/question_summary/custom_question_summary.spec.js @@ -0,0 +1,38 @@ +import AddressBlockPage from "../../../generated_pages/custom_question_summary/address.page.js"; +import AgeBlock from "../../../generated_pages/custom_question_summary/age.page.js"; +import NameBlockPage from "../../../generated_pages/custom_question_summary/name.page.js"; +import SubmitPage from "../../../generated_pages/custom_question_summary/submit.page.js"; +import { click, verifyUrlContains } from "../../../helpers"; +describe("Summary Screen", () => { + beforeEach("Load the survey", async () => { + await browser.openQuestionnaire("test_custom_question_summary.json"); + }); + + it("Given a survey has question summary concatenations and has been completed when on the summary page then the correct response should be displayed formatted correctly", async () => { + await completeAllQuestions(); + await expect(await $(SubmitPage.summaryRowState("name-question-concatenated-answer")).getText()).toBe("John Smith"); + await expect(await $(SubmitPage.summaryRowState("address-question-concatenated-answer")).getText()).toBe("Cardiff Road\nNewport\nNP10 8XG"); + await expect(await $(SubmitPage.summaryRowState("age-question-concatenated-answer")).getText()).toBe("7\nThis age is an estimate"); + }); + + it("Given no values are entered in a question with multiple answers and concatenation set, when on the summary screen then the correct response should be displayed", async () => { + await click(NameBlockPage.submit()); + await click(AddressBlockPage.submit()); + await click(AgeBlock.submit()); + await verifyUrlContains(SubmitPage.pageName); + await expect(await $(SubmitPage.summaryRowState("name-question-concatenated-answer")).getText()).toBe("No answer provided"); + }); + + async function completeAllQuestions() { + await $(NameBlockPage.first()).setValue("John"); + await $(NameBlockPage.last()).setValue("Smith"); + await click(NameBlockPage.submit()); + await $(AddressBlockPage.line1()).setValue("Cardiff Road"); + await $(AddressBlockPage.townCity()).setValue("Newport"); + await $(AddressBlockPage.postcode()).setValue("NP10 8XG"); + await click(AddressBlockPage.submit()); + await $(AgeBlock.number()).setValue(7); + await $(AgeBlock.singleCheckboxThisAgeIsAnEstimate()).click(); + await click(AgeBlock.submit()); + } +}); diff --git a/tests/functional/spec/summaries/section_summary/section_summary.spec.js b/tests/functional/spec/summaries/section_summary/section_summary.spec.js new file mode 100644 index 0000000000..c21261604b --- /dev/null +++ b/tests/functional/spec/summaries/section_summary/section_summary.spec.js @@ -0,0 +1,159 @@ +import AddressDurationPage from "../../../generated_pages/section_summary/address-duration.page.js"; +import HouseholdCountSectionSummaryPage from "../../../generated_pages/section_summary/household-count-section-summary.page.js"; +import HouseholdDetailsSummaryPage from "../../../generated_pages/section_summary/house-details-section-summary.page.js"; +import HouseType from "../../../generated_pages/section_summary/house-type.page.js"; +import InsuranceAddressPage from "../../../generated_pages/section_summary/insurance-address.page.js"; +import InsuranceTypePage from "../../../generated_pages/section_summary/insurance-type.page.js"; +import ListedPage from "../../../generated_pages/section_summary/listed.page.js"; +import NumberOfPeoplePage from "../../../generated_pages/section_summary/number-of-people.page.js"; +import PropertyDetailsSummaryPage from "../../../generated_pages/section_summary/property-details-section-summary.page.js"; +import SubmitPage from "../../../generated_pages/section_summary/submit.page.js"; +import { click, verifyUrlContains } from "../../../helpers"; + +describe("Section Summary", () => { + describe("Given I start a Test Section Summary survey and complete to Section Summary", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_section_summary.json"); + await $(InsuranceTypePage.both()).click(); + await click(InsuranceTypePage.submit()); + await click(InsuranceAddressPage.submit()); + await click(ListedPage.submit()); + await expect(await $(PropertyDetailsSummaryPage.insuranceTypeAnswer()).getText()).toBe("Both"); + }); + + it("When I get to the section summary page, Then the submit button should read 'Continue'", async () => { + await expect(await $(PropertyDetailsSummaryPage.submit()).getText()).toBe("Continue"); + }); + + it("When I have selected an answer to edit and edit it, Then I should return to the section summary with new value displayed", async () => { + await $(PropertyDetailsSummaryPage.insuranceAddressAnswerEdit()).click(); + await $(InsuranceAddressPage.answer()).setValue("Test Address"); + await click(InsuranceAddressPage.submit()); + await expect(await $(PropertyDetailsSummaryPage.insuranceAddressAnswer()).getText()).toBe("Test Address"); + }); + + it("When I select edit from the section summary and click previous on the question page, Then I should be taken back to the section summary", async () => { + await $(PropertyDetailsSummaryPage.insuranceAddressAnswerEdit()).click(); + await $(InsuranceAddressPage.previous()).click(); + await verifyUrlContains(PropertyDetailsSummaryPage.url()); + }); + + it("When I continue on the section summary page, Then I should be taken to the next section", async () => { + await click(PropertyDetailsSummaryPage.submit()); + await verifyUrlContains(HouseType.pageName); + }); + + it("When I select edit from Section Summary but change routing, Then I should step through the section and be returned to the Section Summary once all new questions have been answered", async () => { + await $(PropertyDetailsSummaryPage.insuranceTypeAnswerEdit()).click(); + await $(InsuranceTypePage.contents()).click(); + await click(InsuranceTypePage.submit()); + await verifyUrlContains(AddressDurationPage.pageName); + await click(AddressDurationPage.submit()); + await verifyUrlContains(PropertyDetailsSummaryPage.pageName); + }); + + it("When I select edit from Section Summary but change routing, Then using previous should not prevent me returning to the section summary once all new questions have been answered", async () => { + await $(PropertyDetailsSummaryPage.insuranceTypeAnswerEdit()).click(); + await $(InsuranceTypePage.contents()).click(); + await click(InsuranceTypePage.submit()); + await verifyUrlContains(AddressDurationPage.pageName); + await $(AddressDurationPage.previous()).click(); + await verifyUrlContains(InsuranceAddressPage.pageName); + await click(InsuranceAddressPage.submit()); + await click(AddressDurationPage.submit()); + await verifyUrlContains(PropertyDetailsSummaryPage.pageName); + }); + }); + + describe("Given I start a Test Section Summary survey and complete to Final Summary", () => { + beforeEach(async () => { + await browser.openQuestionnaire("test_section_summary.json"); + await $(InsuranceTypePage.both()).click(); + await click(InsuranceTypePage.submit()); + await click(InsuranceAddressPage.submit()); + await click(ListedPage.submit()); + await click(PropertyDetailsSummaryPage.submit()); + await click(HouseType.submit()); + await click(HouseholdDetailsSummaryPage.submit()); + await $(NumberOfPeoplePage.answer()).setValue(3); + await click(NumberOfPeoplePage.submit()); + await click(HouseholdCountSectionSummaryPage.submit()); + await verifyUrlContains(SubmitPage.url()); + }); + + it("When I select edit from Final Summary and don't change an answer, Then I should be taken to the Final Summary", async () => { + await $(SubmitPage.summaryShowAllButton()).click(); + await $(SubmitPage.insuranceAddressAnswerEdit()).click(); + await click(InsuranceAddressPage.submit()); + await verifyUrlContains(SubmitPage.url()); + }); + + it("When I select edit from Final Summary and change an answer that doesn't affect completeness, Then I should be taken to the Final Summary", async () => { + await $(SubmitPage.summaryShowAllButton()).click(); + await $(SubmitPage.insuranceAddressAnswerEdit()).click(); + await $(InsuranceAddressPage.answer()).setValue("Test Address"); + await click(InsuranceAddressPage.submit()); + await verifyUrlContains(SubmitPage.url()); + }); + + it("When I select edit from Final Summary but change routing, Then I should step through the section and be returned to the Final Summary once all new questions have been answered", async () => { + await $(SubmitPage.summaryShowAllButton()).click(); + await $(SubmitPage.insuranceTypeAnswerEdit()).click(); + await $(InsuranceTypePage.contents()).click(); + await click(InsuranceTypePage.submit()); + await verifyUrlContains(AddressDurationPage.pageName); + await click(AddressDurationPage.submit()); + await verifyUrlContains(SubmitPage.pageName); + }); + + it("When I select edit from Final Summary but change routing, Then using previous should not prevent me returning to the section summary once all new questions have been answered", async () => { + await $(SubmitPage.summaryShowAllButton()).click(); + await $(SubmitPage.insuranceTypeAnswerEdit()).click(); + await $(InsuranceTypePage.contents()).click(); + await click(InsuranceTypePage.submit()); + await verifyUrlContains(AddressDurationPage.pageName); + await $(AddressDurationPage.previous()).click(); + await verifyUrlContains(InsuranceAddressPage.pageName); + await click(InsuranceAddressPage.submit()); + await click(AddressDurationPage.submit()); + await verifyUrlContains(SubmitPage.pageName); + }); + it("When I select edit from Final Summary and change an answer and then go to the next question and click previous, Since I cannot return to the section summary yet I return to the previous block in the section", async () => { + await $(SubmitPage.summaryShowAllButton()).click(); + await $(SubmitPage.insuranceTypeAnswerEdit()).click(); + await $(InsuranceTypePage.contents()).click(); + await click(InsuranceTypePage.submit()); + await $(AddressDurationPage.previous()).click(); + await verifyUrlContains(InsuranceAddressPage.pageName); + }); + + it("When I change an answer, Then the final summary should display the updated value", async () => { + await $(SubmitPage.summaryShowAllButton()).click(); + await expect(await $(SubmitPage.insuranceAddressAnswer()).getText()).toBe("No answer provided"); + await $(SubmitPage.insuranceAddressAnswerEdit()).click(); + await verifyUrlContains(InsuranceAddressPage.pageName); + await $(InsuranceAddressPage.answer()).setValue("Test Address"); + await click(InsuranceAddressPage.submit()); + await $(SubmitPage.summaryShowAllButton()).click(); + await expect(await $(SubmitPage.insuranceAddressAnswer()).getText()).toBe("Test Address"); + }); + }); + describe("Given I start the Test Section Summary questionnaire", () => { + before(async () => { + await browser.openQuestionnaire("test_section_summary.json"); + }); + it("When there is no title set in the sections summary, the section title is used for the section summary title", async () => { + await $(InsuranceTypePage.both()).click(); + await click(InsuranceTypePage.submit()); + await click(InsuranceAddressPage.submit()); + await click(ListedPage.submit()); + await expect(await $(PropertyDetailsSummaryPage.heading()).getText()).toBe("Property Details Section"); + }); + it("When there is a title set in the sections summary, it is used for the section summary title", async () => { + await click(PropertyDetailsSummaryPage.submit()); + await $(HouseType.semiDetached()).click(); + await click(HouseType.submit()); + await expect(await $(HouseholdDetailsSummaryPage.heading()).getText()).toBe("Household Summary - Semi-detached"); + }); + }); +}); diff --git a/tests/functional/spec/summaries/section_summary/section_summary_repeating_sections.spec.js b/tests/functional/spec/summaries/section_summary/section_summary_repeating_sections.spec.js new file mode 100644 index 0000000000..81d1e9d8c2 --- /dev/null +++ b/tests/functional/spec/summaries/section_summary/section_summary_repeating_sections.spec.js @@ -0,0 +1,71 @@ +import PrimaryPersonPage from "../../../generated_pages/repeating_section_summaries/primary-person-list-collector.page"; +import PrimaryPersonAddPage from "../../../generated_pages/repeating_section_summaries/primary-person-list-collector-add.page"; +import FirstListCollectorPage from "../../../generated_pages/repeating_section_summaries/list-collector.page"; +import FirstListCollectorAddPage from "../../../generated_pages/repeating_section_summaries/list-collector-add.page"; +import PersonalSummaryPage from "../../../generated_pages/repeating_section_summaries/personal-details-section-summary.page"; +import ProxyPage from "../../../generated_pages/repeating_section_summaries/proxy.page"; +import DateOfBirthPage from "../../../generated_pages/repeating_section_summaries/date-of-birth.page"; +import HubPage from "../../../base_pages/hub.page.js"; +import { click, verifyUrlContains } from "../../../helpers"; + +describe("Feature: Repeating Section Summaries", () => { + describe("Given the user has added some members to the household and is on the Hub", () => { + before("Open survey and add household members", async () => { + await browser.openQuestionnaire("test_repeating_section_summaries.json"); + // Ensure the questionnaire fully loads + await browser.pause(100); + // Ensure we are on the Hub + await verifyUrlContains(HubPage.url()); + // Start first section to add household members + await $(HubPage.summaryRowLink("section")).click(); + + // Add a primary person + await $(PrimaryPersonPage.yes()).click(); + await click(PrimaryPersonPage.submit()); + await $(PrimaryPersonAddPage.firstName()).setValue("Mark"); + await $(PrimaryPersonAddPage.lastName()).setValue("Twain"); + await click(PrimaryPersonPage.submit()); + + // Add other household members + + await $(FirstListCollectorPage.yes()).click(); + await click(FirstListCollectorPage.submit()); + await $(FirstListCollectorAddPage.firstName()).setValue("Jean"); + await $(FirstListCollectorAddPage.lastName()).setValue("Clemens"); + await click(FirstListCollectorAddPage.submit()); + + await $(FirstListCollectorPage.no()).click(); + await click(FirstListCollectorPage.submit()); + }); + + describe("When the user finishes a repeating section", () => { + before("Enter information for a repeating section", async () => { + await $(HubPage.summaryRowLink("personal-details-section-1")).click(); + await $(ProxyPage.yes()).click(); + await click(ProxyPage.submit()); + + await $(DateOfBirthPage.day()).setValue("30"); + await $(DateOfBirthPage.month()).setValue("11"); + await $(DateOfBirthPage.year()).setValue("1835"); + await click(DateOfBirthPage.submit()); + }); + + beforeEach("Navigate to the Section Summary", async () => { + await browser.url(HubPage.url()); + await $(HubPage.summaryRowLink("personal-details-section-1")).click(); + }); + + it("the title set in the repeating block is used for the section summary title", async () => { + await expect(await $(PersonalSummaryPage.heading()).getText()).toBe("Mark Twain"); + }); + + it("renders their name as part of the question title on the section summary", async () => { + await expect(await $(PersonalSummaryPage.dateOfBirthQuestion()).getText()).toContain("Mark Twain’s"); + }); + + it("renders the correct date of birth answer", async () => { + await expect(await $(PersonalSummaryPage.dateOfBirthAnswer()).getText()).toBe("30 November 1835"); + }); + }); + }); +}); diff --git a/tests/functional/spec/features/show_section_summary_on_completion/show_section_summary_on_completion.spec.js b/tests/functional/spec/summaries/show_section_summary_on_completion/show_section_summary_on_completion.spec.js similarity index 52% rename from tests/functional/spec/features/show_section_summary_on_completion/show_section_summary_on_completion.spec.js rename to tests/functional/spec/summaries/show_section_summary_on_completion/show_section_summary_on_completion.spec.js index 38a7c72f99..bd6804880e 100644 --- a/tests/functional/spec/features/show_section_summary_on_completion/show_section_summary_on_completion.spec.js +++ b/tests/functional/spec/summaries/show_section_summary_on_completion/show_section_summary_on_completion.spec.js @@ -5,52 +5,53 @@ import proxyQuestionPage from "../../../generated_pages/show_section_summary_on_ import accommodationSectionSummary from "../../../generated_pages/show_section_summary_on_completion/accommodation-section-summary.page"; import hubPage from "../../../base_pages/hub.page.js"; +import { click, verifyUrlContains } from "../../../helpers"; describe("Feature: Show section summary on completion", () => { - before("Launch survey", () => { - browser.openQuestionnaire("test_show_section_summary_on_completion.json"); + before("Launch survey", async () => { + await browser.openQuestionnaire("test_show_section_summary_on_completion.json"); }); describe("Given I am completing a section with the summary turned off for the forward journey", () => { - it("When I reach the end of that section, Then I go straight to the hub", () => { - $(employmentStatusBlockPage.workingAsAnEmployee()).click(); - $(employmentStatusBlockPage.submit()).click(); + it("When I reach the end of that section, Then I go straight to the hub", async () => { + await $(employmentStatusBlockPage.workingAsAnEmployee()).click(); + await click(employmentStatusBlockPage.submit()); - expect(browser.getUrl()).to.contain(hubPage.url()); + await verifyUrlContains(hubPage.url()); }); }); describe("Given I have completed a section with the summary turned off for the forward journey", () => { - it("When I return to a completed section from the hub, Then I am returned to that section summary", () => { - $(hubPage.summaryRowLink("employment-section")).click(); + it("When I return to a completed section from the hub, Then I am returned to that section summary", async () => { + await $(hubPage.summaryRowLink("employment-section")).click(); - expect(browser.getUrl()).to.contain(employmentSectionSummary.url()); + await verifyUrlContains(employmentSectionSummary.url()); }); }); describe("Given I am completing a section with the summary turned on for the forward journey", () => { - before("Get to hub", () => { - browser.url(hubPage.url()); + before("Get to hub", async () => { + await browser.url(hubPage.url()); }); - it("When I reach the end of that section, Then I will be taken to the section summary to enable me to amend an answer", () => { - $(hubPage.summaryRowLink("accommodation-section")).click(); - $(proxyQuestionPage.noIMAnsweringForMyself()).click(); - $(proxyQuestionPage.submit()).click(); + it("When I reach the end of that section, Then I will be taken to the section summary to enable me to amend an answer", async () => { + await $(hubPage.summaryRowLink("accommodation-section")).click(); + await $(proxyQuestionPage.noIMAnsweringForMyself()).click(); + await click(proxyQuestionPage.submit()); - expect(browser.getUrl()).to.contain(accommodationSectionSummary.url()); + await verifyUrlContains(accommodationSectionSummary.url()); }); }); describe("Given I have completed a section with the summary turned on for the forward journey", () => { - before("Get to hub", () => { - browser.url(hubPage.url()); + before("Get to hub", async () => { + await browser.url(hubPage.url()); }); - it("When I return to a completed section from the hub, Then I am returned to the correct section summary", () => { - $(hubPage.summaryRowLink("accommodation-section")).click(); + it("When I return to a completed section from the hub, Then I am returned to the correct section summary", async () => { + await $(hubPage.summaryRowLink("accommodation-section")).click(); - expect(browser.getUrl()).to.contain(accommodationSectionSummary.url()); + await verifyUrlContains(accommodationSectionSummary.url()); }); }); }); diff --git a/tests/functional/spec/supplementary_data/supplementary_data.spec.js b/tests/functional/spec/supplementary_data/supplementary_data.spec.js new file mode 100644 index 0000000000..171f3ae1a8 --- /dev/null +++ b/tests/functional/spec/supplementary_data/supplementary_data.spec.js @@ -0,0 +1,524 @@ +import { assertSummaryItems, assertSummaryTitles, assertSummaryValues, listItemComplete, click, verifyUrlContains } from "../../helpers"; +import { expect } from "@wdio/globals"; +import { getRandomString } from "../../jwt_helper"; +import AddAdditionalEmployeePage from "../../generated_pages/supplementary_data/list-collector-additional-add.page.js"; +import AdditionalLengthOfEmploymentPage from "../../generated_pages/supplementary_data/additional-length-of-employment.page.js"; +import AnyAdditionalEmployeesPage from "../../generated_pages/supplementary_data/any-additional-employees.page.js"; +import CalculatedSummarySalesPage from "../../generated_pages/supplementary_data/calculated-summary-sales.page.js"; +import CalculatedSummaryValueSalesPage from "../../generated_pages/supplementary_data/calculated-summary-value-sales.page.js"; +import CalculatedSummaryVolumeSalesPage from "../../generated_pages/supplementary_data/calculated-summary-volume-sales.page.js"; +import CalculatedSummaryVolumeTotalPage from "../../generated_pages/supplementary_data/calculated-summary-volume-total.page.js"; +import DynamicProductsPage from "../../generated_pages/supplementary_data/dynamic-products.page.js"; +import EmailBlockPage from "../../generated_pages/supplementary_data/email-block.page.js"; +import HubPage from "../../base_pages/hub.page"; +import IntroductionBlockPage from "../../generated_pages/supplementary_data/introduction-block.page.js"; +import LengthOfEmploymentPage from "../../generated_pages/supplementary_data/length-of-employment.page.js"; +import ListCollectorAdditionalPage from "../../generated_pages/supplementary_data/list-collector-additional.page.js"; +import ListCollectorEmployeesPage from "../../generated_pages/supplementary_data/list-collector-employees.page.js"; +import ListCollectorProductsPage from "../../generated_pages/supplementary_data/list-collector-products.page.js"; +import LoadedSuccessfullyBlockPage from "../../generated_pages/supplementary_data/loaded-successfully-block.page.js"; +import NewEmailPage from "../../generated_pages/supplementary_data/new-email.page.js"; +import ProductQuestion3EnabledPage from "../../generated_pages/supplementary_data/product-question-3-enabled.page"; +import ProductRepeatingBlock1Page from "../../generated_pages/supplementary_data/product-repeating-block-1-repeating-block.page.js"; +import ProductSalesInterstitialPage from "../../generated_pages/supplementary_data/product-sales-interstitial.page"; +import ProductVolumeInterstitialPage from "../../generated_pages/supplementary_data/product-volume-interstitial.page"; +import SalesBreakdownBlockPage from "../../generated_pages/supplementary_data/sales-breakdown-block.page.js"; +import Section1InterstitialPage from "../../generated_pages/supplementary_data/section-1-interstitial.page.js"; +import Section1Page from "../../generated_pages/supplementary_data/section-1-summary.page.js"; +import Section3Page from "../../generated_pages/supplementary_data/section-3-summary.page.js"; +import Section4Page from "../../generated_pages/supplementary_data/section-4-summary.page.js"; +import Section5Page from "../../generated_pages/supplementary_data/section-5-summary.page.js"; +import Section6Page from "../../generated_pages/supplementary_data/section-6-summary.page.js"; +import ThankYouPage from "../../base_pages/thank-you.page"; +import TradingPage from "../../generated_pages/supplementary_data/trading.page.js"; +import ViewSubmittedResponsePage from "../../generated_pages/supplementary_data/view-submitted-response.page.js"; + +describe("Using supplementary data", () => { + const responseId = getRandomString(16); + const summaryItems = ".ons-summary__item--text"; + const summaryValues = ".ons-summary__values"; + const summaryRowTitles = ".ons-summary__row-title"; + + before("Starting the survey", async () => { + await browser.openQuestionnaire("test_supplementary_data.json", { + version: "v2", + sdsDatasetId: "203b2f9d-c500-8175-98db-86ffcfdccfa3", + responseId, + }); + }); + it("Given I launch a survey using supplementary data, When I am outside a repeating section, Then I am able to see the list of items relating to a given supplementary data list item on the page", async () => { + await expect(await $("#main-content #guidance-1").getText()).toContain("The surnames of the employees are: Potter, Kent."); + await expect(await $$("#main-content li")[0].getText()).toBe("Articles and equipment for sports or outdoor games"); + await expect(await $$("#main-content li")[1].getText()).toBe("Kitchen Equipment"); + }); + + it("Given I progress through the interstitial block, When I begin the introduction block, Then I see the supplementary data piped in", async () => { + await click(LoadedSuccessfullyBlockPage.submit()); + await $(IntroductionBlockPage.acceptCookies()).click(); + await expect(await $(IntroductionBlockPage.businessDetailsContent()).getText()).toContain("You are completing this survey for Tesco"); + await expect(await $(IntroductionBlockPage.businessDetailsContent()).getText()).toContain( + "If the company details or structure have changed contact us on 01171231231", + ); + await expect(await $(IntroductionBlockPage.guidancePanel(1)).getText()).toContain("Some supplementary guidance about the survey"); + await click(IntroductionBlockPage.submit()); + await click(HubPage.submit()); + await $(EmailBlockPage.yes()).click(); + await click(EmailBlockPage.submit()); + }); + + it("Given I have dynamic answer options based off a supplementary date value, When I reach the block using them, Then I see a correct list of options to choose from", async () => { + await expect(await $(TradingPage.answerLabelByIndex(0)).getText()).toBe("Thursday 27 November 1947"); + await expect(await $(TradingPage.answerLabelByIndex(1)).getText()).toBe("Friday 28 November 1947"); + await expect(await $(TradingPage.answerLabelByIndex(2)).getText()).toBe("Saturday 29 November 1947"); + await expect(await $(TradingPage.answerLabelByIndex(3)).getText()).toBe("Sunday 30 November 1947"); + await expect(await $(TradingPage.answerLabelByIndex(4)).getText()).toBe("Monday 1 December 1947"); + await expect(await $(TradingPage.answerLabelByIndex(5)).getText()).toBe("Tuesday 2 December 1947"); + await expect(await $(TradingPage.answerLabelByIndex(6)).getText()).toBe("Wednesday 3 December 1947"); + await $(TradingPage.answerByIndex(3)).click(); + await click(TradingPage.submit()); + }); + + it("Given I have a calculated question validated against a supplementary data value, When I enter a breakdown that exceeds the total, Then I see an error message", async () => { + await $(SalesBreakdownBlockPage.salesBristol()).setValue(333000); + await $(SalesBreakdownBlockPage.salesLondon()).setValue(444000); + await click(SalesBreakdownBlockPage.submit()); + await expect(await $(SalesBreakdownBlockPage.errorNumber(1)).getText()).toContain("Enter answers that add up to or are less than 555,000"); + }); + + it("Given I have a calculated question validated against a supplementary data value, When I enter a breakdown less than the total, Then I see a calculated summary page with the sum of my previous answers", async () => { + await $(SalesBreakdownBlockPage.salesLondon()).setValue(111000); + await click(SalesBreakdownBlockPage.submit()); + await expect(await $(CalculatedSummarySalesPage.calculatedSummaryTitle()).getText()).toBe( + "Total value of sales from Bristol and London is calculated to be ÂŖ444,000.00. Is this correct?", + ); + }); + + it("Given I have an interstitial block with all answers and supplementary data, When I reach this block, Then I see the placeholders rendered correctly", async () => { + await click(CalculatedSummarySalesPage.submit()); + await expect(await $(Section1InterstitialPage.questionText()).getText()).toContain("Summary of information provided for Tesco"); + await expect(await $("body").getText()).toContain("Telephone Number: 01171231231"); + await expect(await $("body").getText()).toContain("Email: contact@tesco.org"); + await expect(await $("body").getText()).toContain("Note Title: Value of total sales"); + await expect(await $("body").getText()).toContain("Note Description: Total value of goods sold during the period of the return"); + await expect(await $("body").getText()).toContain("Note Example Title: Including"); + await expect(await $("body").getText()).toContain("Note Example Description: Sales across all UK stores"); + await expect(await $("body").getText()).toContain("Incorporation Date: 27 November 1947"); + await expect(await $("body").getText()).toContain("Trading start date: 30 November 1947"); + await expect(await $("body").getText()).toContain("Guidance: Some supplementary guidance about the survey"); + await expect(await $("body").getText()).toContain("Total Uk Sales: ÂŖ555,000.00"); + await expect(await $("body").getText()).toContain("Bristol sales: ÂŖ333,000.00"); + await expect(await $("body").getText()).toContain("London sales: ÂŖ111,000.00"); + await expect(await $("body").getText()).toContain("Sum of Bristol and London sales: ÂŖ444,000.00"); + }); + + it("Given I have a section summary enabled, When I reach the section summary, Then I see it rendered correctly with supplementary data", async () => { + await click(Section1InterstitialPage.submit()); + await expect(await $(Section1Page.emailQuestion()).getText()).toBe("Is contact@tesco.org still the correct contact email for Tesco?"); + await expect(await $(Section1Page.sameEmailAnswer()).getText()).toBe("Yes"); + await expect(await $(Section1Page.tradingQuestion()).getText()).toBe("When did Tesco begin trading?"); + await expect(await $(Section1Page.tradingAnswer()).getText()).toBe("Sunday 30 November 1947"); + await expect(await $$(summaryRowTitles)[0].getText()).toBe("How much of the ÂŖ555,000.00 total UK sales was from Bristol and London?"); + await expect(await $(Section1Page.salesBristolAnswer()).getText()).toBe("ÂŖ333,000.00"); + await expect(await $(Section1Page.salesLondonAnswer()).getText()).toBe("ÂŖ111,000.00"); + }); + + it("Given I add an answer used in a first non empty item transform with supplementary data, When I return to the interstitial block, Then I see the transform value has updated", async () => { + await $(Section1Page.sameEmailAnswerEdit()).click(); + await $(EmailBlockPage.no()).click(); + await click(EmailBlockPage.submit()); + await $(NewEmailPage.answer()).setValue("new.contact@gmail.com"); + await click(NewEmailPage.submit()); + await $(Section1Page.previous()).click(); + await expect(await $("body").getText()).toContain("Email: new.contact@gmail.com"); + await click(Section1InterstitialPage.submit()); + await click(Section1Page.submit()); + }); + + it("Given I have a list collector content block using a supplementary list, When I start the section, I see the supplementary list items in the list", async () => { + await click(HubPage.submit()); + await expect(await $(ListCollectorEmployeesPage.listLabel(1)).getText()).toBe("Harry Potter"); + await expect(await $(ListCollectorEmployeesPage.listLabel(2)).getText()).toBe("Clark Kent"); + await click(ListCollectorEmployeesPage.submit()); + }); + + it("Given I add some additional employees via another list collector, When I return to the Hub, Then I see new enabled sections for the supplementary list items and my added ones", async () => { + await click(HubPage.submit()); + await $(AnyAdditionalEmployeesPage.yes()).click(); + await click(AnyAdditionalEmployeesPage.submit()); + await $(AddAdditionalEmployeePage.employeeFirstName()).setValue("Jane"); + await $(AddAdditionalEmployeePage.employeeLastName()).setValue("Doe"); + await click(AddAdditionalEmployeePage.submit()); + await $(ListCollectorAdditionalPage.yes()).click(); + await click(ListCollectorAdditionalPage.submit()); + await $(AddAdditionalEmployeePage.employeeFirstName()).setValue("John"); + await $(AddAdditionalEmployeePage.employeeLastName()).setValue("Smith"); + await click(AddAdditionalEmployeePage.submit()); + await $(ListCollectorAdditionalPage.no()).click(); + await click(ListCollectorAdditionalPage.submit()); + await click(Section3Page.submit()); + await expect(await $(HubPage.summaryItems("section-4-1")).getText()).toContain("Harry Potter"); + await expect(await $(HubPage.summaryItems("section-4-2")).getText()).toContain("Clark Kent"); + await expect(await $(HubPage.summaryItems("section-5-1")).getText()).toContain("Jane Doe"); + await expect(await $(HubPage.summaryItems("section-5-2")).getText()).toContain("John Smith"); + await click(HubPage.submit()); + }); + + it("Given I have repeating sections for both supplementary and dynamic list items, When I start a repeating section for a supplementary list item, Then I see static supplementary data correctly piped in", async () => { + await expect(await $(LengthOfEmploymentPage.questionTitle()).getText()).toContain("When did Harry Potter start working for Tesco?"); + await expect(await $(LengthOfEmploymentPage.employmentStartLegend()).getText()).toContain("Start date at Tesco"); + }); + + it("Given I have validation on the start date in the repeating section, When I enter a date before the incorporation date, Then I see an error message", async () => { + await $(LengthOfEmploymentPage.day()).setValue(1); + await $(LengthOfEmploymentPage.month()).setValue(1); + await $(LengthOfEmploymentPage.year()).setValue(1930); + await click(LengthOfEmploymentPage.submit()); + await expect(await $(LengthOfEmploymentPage.singleErrorLink()).getText()).toBe("Enter a date after 26 November 1947"); + }); + + it("Given I have validation on the start date in the repeating section, When I enter a date after the incorporation date, Then I see that date on the summary page for the section", async () => { + await $(LengthOfEmploymentPage.year()).setValue(1990); + await click(LengthOfEmploymentPage.submit()); + await expect(await $(Section4Page.lengthEmploymentQuestion()).getText()).toBe("When did Harry Potter start working for Tesco?"); + await expect(await $(Section4Page.employmentStart()).getText()).toBe("1 January 1990"); + }); + + it("Given I complete the repeating section for another supplementary item, When I reach the summary page, Then I see the correct supplementary data with my answers", async () => { + await click(Section4Page.submit()); + await click(HubPage.submit()); + await expect(await $(LengthOfEmploymentPage.questionTitle()).getText()).toContain("When did Clark Kent start working for Tesco?"); + await $(LengthOfEmploymentPage.day()).setValue(5); + await $(LengthOfEmploymentPage.month()).setValue(6); + await $(LengthOfEmploymentPage.year()).setValue(2011); + await click(LengthOfEmploymentPage.submit()); + await expect(await $(Section4Page.lengthEmploymentQuestion()).getText()).toBe("When did Clark Kent start working for Tesco?"); + await expect(await $(Section4Page.employmentStart()).getText()).toBe("5 June 2011"); + }); + + it("Given I move onto the dynamic list items, When I start a repeating section for a dynamic list item, Then I see static supplementary data correctly piped in and the same validation and summary", async () => { + await click(Section4Page.submit()); + await click(HubPage.submit()); + await expect(await $(AdditionalLengthOfEmploymentPage.questionTitle()).getText()).toContain("When did Jane Doe start working for Tesco?"); + await expect(await $(AdditionalLengthOfEmploymentPage.additionalEmploymentStartLegend()).getText()).toBe("Start date at Tesco"); + await $(AdditionalLengthOfEmploymentPage.day()).setValue(1); + await $(AdditionalLengthOfEmploymentPage.month()).setValue(1); + await $(AdditionalLengthOfEmploymentPage.year()).setValue(1930); + await click(AdditionalLengthOfEmploymentPage.submit()); + await expect(await $(AdditionalLengthOfEmploymentPage.singleErrorLink()).getText()).toBe("Enter a date after 26 November 1947"); + await $(AdditionalLengthOfEmploymentPage.year()).setValue(2000); + await click(AdditionalLengthOfEmploymentPage.submit()); + await expect(await $(Section5Page.additionalLengthEmploymentQuestion()).getText()).toBe("When did Jane Doe start working for Tesco?"); + await expect(await $(Section5Page.additionalEmploymentStart()).getText()).toBe("1 January 2000"); + await click(Section5Page.submit()); + await click(HubPage.submit()); + await $(AdditionalLengthOfEmploymentPage.day()).setValue(3); + await $(AdditionalLengthOfEmploymentPage.month()).setValue(3); + await $(AdditionalLengthOfEmploymentPage.year()).setValue(2010); + await click(AdditionalLengthOfEmploymentPage.submit()); + await expect(await $(Section5Page.additionalLengthEmploymentQuestion()).getText()).toBe("When did John Smith start working for Tesco?"); + await expect(await $(Section5Page.additionalEmploymentStart()).getText()).toBe("3 March 2010"); + await click(Section5Page.submit()); + }); + + it("Given I have some repeating blocks with supplementary data, When I begin the section, Then I see the supplementary names rendered correctly", async () => { + await click(HubPage.submit()); + await expect(await $(ListCollectorProductsPage.listLabel(1)).getText()).toBe("Articles and equipment for sports or outdoor games"); + await expect(await $(ListCollectorProductsPage.listLabel(2)).getText()).toBe("Kitchen Equipment"); + await click(ListCollectorProductsPage.submit()); + }); + + it("Given I have repeating blocks with supplementary data, When I start the first repeating block, Then I see the supplementary data for the first list item", async () => { + await expect(await $("body").getHTML()).toContain("

    Include

    "); + await expect(await $("body").getHTML()).toContain("
  • for children's playgrounds
  • "); + await expect(await $("body").getHTML()).toContain("
  • swimming pools and paddling pools
  • "); + await expect(await $("body").getHTML()).toContain("

    Exclude

    "); + await expect(await $("body").getHTML()).toContain( + "
  • sports holdalls, gloves, clothing of textile materials, footwear, protective eyewear, rackets, balls, skates
  • ", + ); + await expect(await $("body").getHTML()).toContain( + "
  • for skiing, water sports, golf, fishing', for skiing, water sports, golf, fishing, table tennis, PE, gymnastics, athletics
  • ", + ); + await expect(await $(ProductRepeatingBlock1Page.productVolumeSalesLabel()).getText()).toBe( + "Volume of sales for Articles and equipment for sports or outdoor games", + ); + await expect(await $(ProductRepeatingBlock1Page.productVolumeTotalLabel()).getText()).toBe( + "Total volume produced for Articles and equipment for sports or outdoor games", + ); + await $(ProductRepeatingBlock1Page.productVolumeSales()).setValue(100); + await $(ProductRepeatingBlock1Page.productVolumeTotal()).setValue(200); + }); + + it("Given I have repeating blocks with supplementary data, When I start the second repeating block, Then I see the supplementary data for the second list item", async () => { + await click(ProductRepeatingBlock1Page.submit()); + await click(ListCollectorProductsPage.submit()); + await expect(await $("body").getText()).toContain("Include"); + await expect(await $("body").getText()).toContain("pots and pans"); + await expect(await $("body").getText()).not.toBe("Exclude"); + await expect(await $(ProductRepeatingBlock1Page.productVolumeSalesLabel()).getText()).toBe("Volume of sales for Kitchen Equipment"); + await expect(await $(ProductRepeatingBlock1Page.productVolumeTotalLabel()).getText()).toBe("Total volume produced for Kitchen Equipment"); + await $(ProductRepeatingBlock1Page.productVolumeSales()).setValue(50); + await $(ProductRepeatingBlock1Page.productVolumeTotal()).setValue(300); + await click(ProductRepeatingBlock1Page.submit()); + }); + + it("Given I have a calculated summary using the repeating blocks, When I reach the Calculated Summary, Then I see the correct total and supplementary data labels", async () => { + await click(ListCollectorProductsPage.submit()); + await verifyUrlContains(CalculatedSummaryVolumeSalesPage.pageName); + await expect(await $(CalculatedSummaryVolumeSalesPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total volume of sales over the previous quarter to be 150 kg. Is this correct?", + ); + await assertSummaryItems([ + "Volume of sales for Articles and equipment for sports or outdoor games", + "Volume of sales for Kitchen Equipment", + "Total sales volume", + ]); + await assertSummaryValues(["100 kg", "50 kg", "150 kg"]); + await click(CalculatedSummaryVolumeSalesPage.submit()); + }); + + it("Given I have another calculated summary using the repeating blocks, When I reach the Calculated Summary, Then I see the correct total and supplementary data labels", async () => { + await expect(await $(CalculatedSummaryVolumeTotalPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total volume produced over the previous quarter to be 500 kg. Is this correct?", + ); + await assertSummaryItems([ + "Total volume produced for Articles and equipment for sports or outdoor games", + "Total volume produced for Kitchen Equipment", + "Total volume produced", + ]); + await assertSummaryValues(["200 kg", "300 kg", "500 kg"]); + await click(CalculatedSummaryVolumeTotalPage.submit()); + }); + + it("Given I have dynamic answers using a supplementary list, When I reach the dynamic answer page, Then I see the correct supplementary data in the answer labels", async () => { + await expect(await $$(DynamicProductsPage.labels())[0].getText()).toBe("Value of sales for Articles and equipment for sports or outdoor games"); + await expect(await $$(DynamicProductsPage.labels())[1].getText()).toBe("Value of sales for Kitchen Equipment"); + await expect(await $$(DynamicProductsPage.labels())[2].getText()).toBe("Value of sales from other categories"); + await $$(DynamicProductsPage.inputs())[0].setValue(110); + await $$(DynamicProductsPage.inputs())[1].setValue(220); + await $$(DynamicProductsPage.inputs())[2].setValue(330); + await click(DynamicProductsPage.submit()); + }); + + it("Given I have a calculated summary of dynamic answers for a supplementary list, When I reach the calculated summary, Then I see the correct supplementary data in the title and labels", async () => { + await expect(await $(CalculatedSummaryValueSalesPage.calculatedSummaryTitle()).getText()).toBe( + "We calculate the total value of sales over the previous quarter to be ÂŖ660.00. Is this correct?", + ); + await assertSummaryItems([ + "Value of sales for Articles and equipment for sports or outdoor games", + "Value of sales for Kitchen Equipment", + "Value of sales from other categories", + "Total sales value", + ]); + await assertSummaryValues(["ÂŖ110.00", "ÂŖ220.00", "ÂŖ330.00", "ÂŖ660.00"]); + await click(CalculatedSummaryValueSalesPage.submit()); + }); + + it("Given I have a section with repeating answers for a supplementary list, When I reach the section summary page, Then I see the supplementary data and my answers rendered correctly", async () => { + await expect(await $$(summaryRowTitles)[0].getText()).toBe("Sales during the previous quarter"); + await assertSummaryItems([ + "Articles and equipment for sports or outdoor games", + "Volume of sales for Articles and equipment for sports or outdoor games", + "Total volume produced for Articles and equipment for sports or outdoor games", + "Kitchen Equipment", + "Volume of sales for Kitchen Equipment", + "Total volume produced for Kitchen Equipment", + "Value of sales for Articles and equipment for sports or outdoor games", + "Value of sales for Kitchen Equipment", + "Value of sales from other categories", + ]); + await assertSummaryValues(["100 kg", "200 kg", "50 kg", "300 kg", "ÂŖ110.00", "ÂŖ220.00", "ÂŖ330.00"]); + await click(Section6Page.submit()); + await expect(await $(HubPage.summaryRowState("section-6")).getText()).toBe("Completed"); + }); + + it("Given I am using a supplementary dataset where the size of one of the lists skips a question in a section, When I enter the section, Then I only see an interstitial block as the other block is skipped", async () => { + await $(HubPage.summaryRowLink("section-8")).click(); + await verifyUrlContains(ProductVolumeInterstitialPage.pageName); + await click(ProductVolumeInterstitialPage.submit()); + await expect(await $(HubPage.summaryRowState("section-8")).getText()).toBe("Completed"); + }); + + it("Given I relaunch the survey with new supplementary data and new list items for the repeating section, When I open the Hub page, Then I see the new supplementary list items as new incomplete sections and not any old ones", async () => { + await browser.openQuestionnaire("test_supplementary_data.json", { + version: "v2", + sdsDatasetId: "3bb41d29-4daa-9520-82f0-cae365f390c6", + responseId, + }); + await expect(await $(HubPage.summaryItems("section-4-1")).getText()).toContain("Harry Potter"); + await expect(await $(HubPage.summaryItems("section-4-2")).getText()).toContain("Bruce Wayne"); + await expect(await $(HubPage.summaryItems("section-5-1")).getText()).toContain("Jane Doe"); + await expect(await $(HubPage.summaryItems("section-5-2")).getText()).toContain("John Smith"); + await expect(await $(HubPage.summaryRowState("section-4-1")).getText()).toBe("Completed"); + await expect(await $(HubPage.summaryRowState("section-4-2")).getText()).toBe("Not started"); + await expect(await $(HubPage.summaryRowState("section-5-1")).getText()).toBe("Completed"); + await expect(await $(HubPage.summaryRowState("section-5-2")).getText()).toBe("Completed"); + await expect(await $("body").getText()).not.toContain("Clark Kent"); + }); + + it("Given the survey has been relaunched with new data and more items in the products list, When I am on the Hub, Then I see the products section and section with a new block due to the product list size are both in progress", async () => { + await expect(await $(HubPage.summaryRowState("section-6")).getText()).toBe("Partially completed"); + await expect(await $(HubPage.summaryRowState("section-8")).getText()).toBe("Partially completed"); + }); + + it("Given I am using a supplementary dataset with a product list size that skips a question in the sales target section, When I enter the section, Then I only see an interstitial block", async () => { + await $(HubPage.summaryRowLink("section-7")).click(); + await verifyUrlContains(ProductSalesInterstitialPage.pageName); + await click(ProductSalesInterstitialPage.submit()); + await expect(await $(HubPage.summaryRowState("section-7")).getText()).toBe("Completed"); + }); + + it("Given there is now an additional product, When I resume the Product Details Section, Then I start from the list collector content block and see the new product is incomplete", async () => { + await $(HubPage.summaryRowLink("section-6")).click(); + await verifyUrlContains(ListCollectorProductsPage.pageName); + await listItemComplete(`li[data-qa="list-item-1-label"]`, true); + await listItemComplete(`li[data-qa="list-item-2-label"]`, true); + await listItemComplete(`li[data-qa="list-item-3-label"]`, false); + await click(ListCollectorProductsPage.submit()); + await verifyUrlContains(ProductRepeatingBlock1Page.pageName); + }); + + it("Given I complete the section and relaunch with the old data that has fewer items in the products list, When I am on the Hub, Then I see the products section and sales targets sections are now in progress", async () => { + await $(ProductRepeatingBlock1Page.productVolumeSales()).setValue(40); + await $(ProductRepeatingBlock1Page.productVolumeTotal()).setValue(50); + await click(ProductRepeatingBlock1Page.submit()); + await click(ListCollectorProductsPage.submit()); + await click(CalculatedSummaryVolumeSalesPage.submit()); + await click(CalculatedSummaryVolumeTotalPage.submit()); + await $$(DynamicProductsPage.inputs())[2].setValue(115); + await click(DynamicProductsPage.submit()); + await click(CalculatedSummaryValueSalesPage.submit()); + await click(Section6Page.submit()); + await expect(await $(HubPage.summaryRowState("section-6")).getText()).toBe("Completed"); + await browser.openQuestionnaire("test_supplementary_data.json", { + version: "v2", + sdsDatasetId: "203b2f9d-c500-8175-98db-86ffcfdccfa3", + responseId, + }); + await expect(await $(HubPage.summaryRowState("section-6")).getText()).toBe("Partially completed"); + await expect(await $(HubPage.summaryRowState("section-7")).getText()).toBe("Partially completed"); + }); + + it("Given I return to the new data resulting in a new incomplete section, When I start the section, Then I see the new supplementary data piped in accordingly", async () => { + await browser.openQuestionnaire("test_supplementary_data.json", { + version: "v2", + sdsDatasetId: "3bb41d29-4daa-9520-82f0-cae365f390c6", + responseId, + }); + await click(HubPage.submit()); + await $(LengthOfEmploymentPage.day()).setValue(10); + await $(LengthOfEmploymentPage.month()).setValue(10); + await $(LengthOfEmploymentPage.year()).setValue(1999); + await click(LengthOfEmploymentPage.submit()); + await expect(await $(Section4Page.lengthEmploymentQuestion()).getText()).toBe("When did Bruce Wayne start working for Lidl?"); + await expect(await $(Section4Page.employmentStart()).getText()).toBe("10 October 1999"); + await click(Section4Page.submit()); + }); + + it("Given I can view my response after submission, When I submit the survey, Then I see the values I've entered and correct rendering with supplementary data", async () => { + await click(HubPage.submit()); + await click(ListCollectorProductsPage.submit()); + await $(ProductRepeatingBlock1Page.productVolumeSales()).setValue(40); + await $(ProductRepeatingBlock1Page.productVolumeTotal()).setValue(50); + await click(ProductRepeatingBlock1Page.submit()); + await click(ListCollectorProductsPage.submit()); + await click(CalculatedSummaryVolumeSalesPage.submit()); + await click(CalculatedSummaryVolumeTotalPage.submit()); + await $$(DynamicProductsPage.inputs())[2].setValue(115); + await click(DynamicProductsPage.submit()); + await click(CalculatedSummaryValueSalesPage.submit()); + await click(Section6Page.submit()); + await click(HubPage.submit()); + await $(ProductQuestion3EnabledPage.yes()).click(); + await click(ProductQuestion3EnabledPage.submit()); + await click(HubPage.submit()); + await $(ThankYouPage.savePrintAnswersLink()).click(); + + await assertSummaryTitles([ + "Company Details", + "Additional Employees", + "Harry Potter", + "Bruce Wayne", + "Jane Doe", + "John Smith", + "Product details", + "Production Targets", + ]); + + // Company details + await expect(await $(ViewSubmittedResponsePage.emailQuestion()).getText()).toBe("Is contact@lidl.org still the correct contact email for Lidl?"); + await expect(await $(ViewSubmittedResponsePage.sameEmailAnswer()).getText()).toBe("No"); + await expect(await $(ViewSubmittedResponsePage.newEmailQuestion()).getText()).toBe("What is the new contact email for Lidl?"); + await expect(await $(ViewSubmittedResponsePage.newEmailAnswer()).getText()).toBe("new.contact@gmail.com"); + await expect(await $(ViewSubmittedResponsePage.tradingQuestion()).getText()).toBe("When did Lidl begin trading?"); + await expect(await $(ViewSubmittedResponsePage.tradingAnswer()).getText()).toBe("Sunday 30 November 1947"); + await expect(await $$(summaryRowTitles)[0].getText()).toBe("How much of the ÂŖ555,000.00 total UK sales was from Bristol and London?"); + await expect(await $(ViewSubmittedResponsePage.salesBristolAnswer()).getText()).toBe("ÂŖ333,000.00"); + await expect(await $(ViewSubmittedResponsePage.salesLondonAnswer()).getText()).toBe("ÂŖ111,000.00"); + + // Additional Employees + await expect(await $(ViewSubmittedResponsePage.anyAdditionalEmployeeQuestion()).getText()).toBe("Do you have any additional employees to report on?"); + await expect(await $(ViewSubmittedResponsePage.anyAdditionalEmployeeAnswer()).getText()).toBe("Yes"); + await expect(await $(ViewSubmittedResponsePage.additionalEmployeeReportingContent(1)).$$(summaryItems)[0].getText()).toBe("Jane Doe"); + await expect(await $(ViewSubmittedResponsePage.additionalEmployeeReportingContent(1)).$$(summaryItems)[1].getText()).toBe("John Smith"); + + // Harry Potter + await expect(await $(ViewSubmittedResponsePage.employeeDetailQuestionsContent(0)).$$(summaryItems)[0].getText()).toBe( + "When did Harry Potter start working for Lidl?", + ); + await expect(await $(ViewSubmittedResponsePage.employeeDetailQuestionsContent(0)).$$(summaryValues)[0].getText()).toBe("1 January 1990"); + + // Bruce Wayne + await expect(await $(ViewSubmittedResponsePage.employeeDetailQuestionsContent("0-1")).$$(summaryItems)[0].getText()).toBe( + "When did Bruce Wayne start working for Lidl?", + ); + await expect(await $(ViewSubmittedResponsePage.employeeDetailQuestionsContent("0-1")).$$(summaryValues)[0].getText()).toBe("10 October 1999"); + + // Jane Doe + await expect(await $(ViewSubmittedResponsePage.additionalEmployeeDetailQuestionsContent(0)).$$(summaryItems)[0].getText()).toBe( + "When did Jane Doe start working for Lidl?", + ); + await expect(await $(ViewSubmittedResponsePage.additionalEmployeeDetailQuestionsContent(0)).$$(summaryValues)[0].getText()).toBe("1 January 2000"); + + // John Smith + await expect(await $(ViewSubmittedResponsePage.additionalEmployeeDetailQuestionsContent("0-2")).$$(summaryItems)[0].getText()).toBe( + "When did John Smith start working for Lidl?", + ); + await expect(await $(ViewSubmittedResponsePage.additionalEmployeeDetailQuestionsContent("0-2")).$$(summaryValues)[0].getText()).toBe("3 March 2010"); + + // Product details + await expect(await $(ViewSubmittedResponsePage.productReportingContent(0)).$$(summaryItems)[0].getText()).toBe( + "Articles and equipment for sports or outdoor games", + ); + await expect(await $(ViewSubmittedResponsePage.productReportingContent(0)).$$(summaryItems)[1].getText()).toBe( + "Volume of sales for Articles and equipment for sports or outdoor games", + ); + await expect(await $(ViewSubmittedResponsePage.productReportingContent(0)).$$(summaryItems)[2].getText()).toBe( + "Total volume produced for Articles and equipment for sports or outdoor games", + ); + await expect(await $(ViewSubmittedResponsePage.productReportingContent(0)).$$(summaryValues)[0].getText()).toBe("100 kg"); + await expect(await $(ViewSubmittedResponsePage.productReportingContent(0)).$$(summaryValues)[1].getText()).toBe("200 kg"); + await expect(await $(ViewSubmittedResponsePage.productReportingContent(0)).$$(summaryItems)[3].getText()).toBe("Kitchen Equipment"); + await expect(await $(ViewSubmittedResponsePage.productReportingContent(0)).$$(summaryItems)[4].getText()).toBe("Volume of sales for Kitchen Equipment"); + await expect(await $(ViewSubmittedResponsePage.productReportingContent(0)).$$(summaryItems)[5].getText()).toBe( + "Total volume produced for Kitchen Equipment", + ); + await expect(await $(ViewSubmittedResponsePage.productReportingContent(0)).$$(summaryValues)[2].getText()).toBe("50 kg"); + await expect(await $(ViewSubmittedResponsePage.productReportingContent(0)).$$(summaryValues)[3].getText()).toBe("300 kg"); + await expect(await $(ViewSubmittedResponsePage.productReportingContent(0)).$$(summaryItems)[6].getText()).toBe("Groceries"); + await expect(await $(ViewSubmittedResponsePage.productReportingContent(0)).$$(summaryItems)[7].getText()).toBe("Volume of sales for Groceries"); + await expect(await $(ViewSubmittedResponsePage.productReportingContent(0)).$$(summaryItems)[8].getText()).toBe("Total volume produced for Groceries"); + await expect(await $(ViewSubmittedResponsePage.productReportingContent(0)).$$(summaryValues)[4].getText()).toBe("40 kg"); + await expect(await $(ViewSubmittedResponsePage.productReportingContent(0)).$$(summaryValues)[5].getText()).toBe("50 kg"); + await expect(await $(ViewSubmittedResponsePage.productReportingContent(1)).$$(summaryRowTitles)[0].getText()).toBe("Sales during the previous quarter"); + await expect(await $(ViewSubmittedResponsePage.productReportingContent(1)).$$(summaryItems)[0].getText()).toBe( + "Value of sales for Articles and equipment for sports or outdoor games", + ); + await expect(await $(ViewSubmittedResponsePage.productReportingContent(1)).$$(summaryItems)[1].getText()).toBe("Value of sales for Kitchen Equipment"); + await expect(await $(ViewSubmittedResponsePage.productReportingContent(1)).$$(summaryItems)[2].getText()).toBe("Value of sales for Groceries"); + await expect(await $(ViewSubmittedResponsePage.productReportingContent(1)).$$(summaryItems)[3].getText()).toBe("Value of sales from other categories"); + await expect(await $(ViewSubmittedResponsePage.productReportingContent(1)).$$(summaryValues)[0].getText()).toBe("ÂŖ110.00"); + await expect(await $(ViewSubmittedResponsePage.productReportingContent(1)).$$(summaryValues)[1].getText()).toBe("ÂŖ220.00"); + await expect(await $(ViewSubmittedResponsePage.productReportingContent(1)).$$(summaryValues)[2].getText()).toBe("ÂŖ115.00"); + await expect(await $(ViewSubmittedResponsePage.productReportingContent(1)).$$(summaryValues)[3].getText()).toBe("ÂŖ330.00"); + }); +}); diff --git a/tests/functional/spec/textarea.spec.js b/tests/functional/spec/textarea.spec.js index de77241300..495fe5d1bb 100644 --- a/tests/functional/spec/textarea.spec.js +++ b/tests/functional/spec/textarea.spec.js @@ -1,49 +1,49 @@ import TextareaBlock from "../generated_pages/textarea/textarea-block.page.js"; import TextareaSummary from "../generated_pages/textarea/submit.page.js"; - +import { click } from "../helpers"; describe("Textarea", () => { const textareaSchema = "test_textarea.json"; const textareaLimit = `${TextareaBlock.answer()} + [data-charcount-singular]`; - beforeEach(() => { - browser.openQuestionnaire(textareaSchema); + beforeEach(async () => { + await browser.openQuestionnaire(textareaSchema); }); - it("Given a textarea option, a user should be able to click the label of the textarea to focus", () => { - $(TextareaBlock.answerLabel()).click(); - expect($(TextareaBlock.answer()).isFocused()).to.be.true; + it("Given a textarea option, a user should be able to click the label of the textarea to focus", async () => { + await $(TextareaBlock.answerLabel()).click(); + await expect(await $(TextareaBlock.answer()).isFocused()).toBe(true); }); - it('Given a textarea option, When no text is entered, Then the summary should display "No answer provided"', () => { - $(TextareaBlock.submit()).click(); - expect($(TextareaSummary.answer()).getText()).to.contain("No answer provided"); + it('Given a textarea option, When no text is entered, Then the summary should display "No answer provided"', async () => { + await click(TextareaBlock.submit()); + await expect(await $(TextareaSummary.answer()).getText()).toBe("No answer provided"); }); - it("Given a textarea option, When some text is entered, Then the summary should display the text", () => { - $(TextareaBlock.answer()).setValue("Some text"); - $(TextareaBlock.submit()).click(); - expect($(TextareaSummary.answer()).getText()).to.contain("Some text"); + it("Given a textarea option, When some text is entered, Then the summary should display the text", async () => { + await $(TextareaBlock.answer()).setValue("Some text"); + await click(TextareaBlock.submit()); + await expect(await $(TextareaSummary.answer()).getText()).toBe("Some text"); }); - it("Given a text entered in textarea , When user submits and revisits the textarea, Then the textarea must contain the text entered previously", () => { - $(TextareaBlock.answer()).setValue("'Twenty><&Five'"); - $(TextareaBlock.submit()).click(); - expect($(TextareaSummary.answer()).getText()).to.contain("'Twenty><&Five'"); - $(TextareaSummary.answerEdit()).click(); - $(TextareaBlock.answer()).getValue(); + it("Given a text entered in textarea , When user submits and revisits the textarea, Then the textarea must contain the text entered previously", async () => { + await $(TextareaBlock.answer()).setValue("'Twenty><&Five'"); + await click(TextareaBlock.submit()); + await expect(await $(TextareaSummary.answer()).getText()).toBe("'Twenty><&Five'"); + await $(TextareaSummary.answerEdit()).click(); + await $(TextareaBlock.answer()).getValue(); }); - it("Displays the number of characters remaining", () => { - expect($(textareaLimit).getText()).to.contain("20"); + it("Displays the number of characters remaining", async () => { + await expect(await $(textareaLimit).getText()).toContain("20"); }); - it("Updates the number of characters remaining when the user adds content", () => { - $(TextareaBlock.answer()).setValue("Banjo"); - expect($(textareaLimit).getText()).to.contain("15"); + it("Updates the number of characters remaining when the user adds content", async () => { + await $(TextareaBlock.answer()).setValue("Banjo"); + await expect(await $(textareaLimit).getText()).toContain("15"); }); - it("The user is unable to add more characters when the limit is reached", () => { - $(TextareaBlock.answer()).setValue("This sentence is over twenty characters long"); - expect($(textareaLimit).getText()).to.contain("0"); - $(TextareaBlock.answer()).getValue(); + it("The user is unable to add more characters when the limit is reached", async () => { + await $(TextareaBlock.answer()).setValue("This sentence is over twenty characters long"); + await expect(await $(textareaLimit).getText()).toContain("0"); + await $(TextareaBlock.answer()).getValue(); }); }); diff --git a/tests/functional/spec/textfield.spec.js b/tests/functional/spec/textfield.spec.js index 4f9e8d58b2..c5e0d1e061 100644 --- a/tests/functional/spec/textfield.spec.js +++ b/tests/functional/spec/textfield.spec.js @@ -1,27 +1,27 @@ import TextFieldPage from "../generated_pages/textfield/name-block.page.js"; import SubmitPage from "../generated_pages/textfield/submit.page.js"; - +import { click, verifyUrlContains } from "../helpers"; describe("Textfield", () => { - it("Given a textfield option, a user should be able to click the label of the textfield to focus", () => { - browser.openQuestionnaire("test_textfield.json"); - $(TextFieldPage.nameLabel()).click(); - expect($(TextFieldPage.name()).isFocused()).to.be.true; + it("Given a textfield option, a user should be able to click the label of the textfield to focus", async () => { + await browser.openQuestionnaire("test_textfield.json"); + await $(TextFieldPage.nameLabel()).click(); + await expect(await $(TextFieldPage.name()).isFocused()).toBe(true); }); - it("Given a text entered in textfield , When user submits and revisits the textfield, Then the textfield must contain the text entered previously", () => { - browser.openQuestionnaire("test_textfield.json"); - $(TextFieldPage.name()).setValue("'Twenty><&Five'"); - $(TextFieldPage.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.pageName); - expect($(SubmitPage.nameAnswer()).getText()).to.contain("Twenty><&Five'"); - $(SubmitPage.nameAnswerEdit()).click(); - $(TextFieldPage.name()).getValue(); + it("Given a text entered in textfield , When user submits and revisits the textfield, Then the textfield must contain the text entered previously", async () => { + await browser.openQuestionnaire("test_textfield.json"); + await $(TextFieldPage.name()).setValue("'Twenty><&Five'"); + await click(TextFieldPage.submit()); + await verifyUrlContains(SubmitPage.pageName); + await expect(await $(SubmitPage.nameAnswer()).getText()).toBe("'Twenty><&Five'"); + await $(SubmitPage.nameAnswerEdit()).click(); + await $(TextFieldPage.name()).getValue(); }); - it("Given the string entered to the textfield is too long, When the user submits, then the correct error message is displayed", () => { - browser.openQuestionnaire("test_textfield.json"); - $(TextFieldPage.name()).setValue("This string is too long"); - $(TextFieldPage.submit()).click(); - expect($(TextFieldPage.errorNumber(1)).getText()).to.contain("You have entered too many characters. Enter up to 20 characters"); + it("Given the string entered to the textfield is too long, When the user submits, then the correct error message is displayed", async () => { + await browser.openQuestionnaire("test_textfield.json"); + await $(TextFieldPage.name()).setValue("This string is too long"); + await click(TextFieldPage.submit()); + await expect(await $(TextFieldPage.errorNumber(1)).getText()).toBe("You have entered too many characters. Enter up to 20 characters"); }); }); diff --git a/tests/functional/spec/textfield_suggestions.spec.js b/tests/functional/spec/textfield_suggestions.spec.js index 6c13cd4161..96455563e7 100644 --- a/tests/functional/spec/textfield_suggestions.spec.js +++ b/tests/functional/spec/textfield_suggestions.spec.js @@ -1,38 +1,35 @@ import SuggestionsPage from "../generated_pages/textfield_suggestions/country-block.page.js"; import MultipleSuggestionsPage from "../generated_pages/textfield_suggestions/multiple-country-block.page.js"; import SubmitPage from "../generated_pages/textfield_suggestions/submit.page.js"; - +import { click, verifyUrlContains } from "../helpers"; describe("Suggestions", () => { - it("Given I open a textfield with a suggestions url, when I have entered text, then it will show suggestions", () => { - browser.openQuestionnaire("test_textfield_suggestions.json"); - $(SuggestionsPage.country()).setValue("Uni"); + it("Given I open a textfield with a suggestions url, when I have entered text, then it will show suggestions", async () => { + await browser.openQuestionnaire("test_textfield_suggestions.json"); + await $(SuggestionsPage.country()).setValue("Uni"); $("#country-answer-listbox li").waitForDisplayed(); - expect($$(".ons-js-autosuggest-listbox li").length).to.not.equal(0); + await expect(await $$(".ons-js-autosuggest-listbox li").length).not.toBe(0); }); }); describe("Suggestions", () => { - it("Given I open a textfield with a suggestions url that allows multiple suggestions, when I have entered text and picked suggestion from a list, then after typing more text it will show new suggestions", () => { - browser.openQuestionnaire("test_textfield_suggestions.json"); - const suggestionsList = $("#multiple-country-answer-listbox li"); + it("Given I open a textfield with a suggestions url that allows multiple suggestions, when I have entered text and picked suggestion from a list, then after typing more text it will show new suggestions", async () => { + await browser.openQuestionnaire("test_textfield_suggestions.json"); const suggestionsOption = $("#multiple-country-answer-listbox__option--0"); - $(SuggestionsPage.country()).setValue("United States of America"); - $(SuggestionsPage.submit()).click(); - $(MultipleSuggestionsPage.multipleCountry()).click(); + await $(SuggestionsPage.country()).setValue("United States of America"); + await click(SuggestionsPage.submit()); + await $(MultipleSuggestionsPage.multipleCountry()).click(); // Browser needs to pause before typing starts to allow for the autosuggest Javascript to initialise - browser.pause(500); - browser.keys("Ita"); - suggestionsList.waitForExist(); - suggestionsOption.click(); - $(MultipleSuggestionsPage.multipleCountry()).click(); + await browser.pause(500); + await browser.keys("Ita"); + await suggestionsOption.click(); + await $(MultipleSuggestionsPage.multipleCountry()).click(); // Browser needs to pause before typing starts to allow for the autosuggest Javascript to initialise - browser.pause(500); - browser.keys(" United"); - suggestionsList.waitForExist(); - expect($$(".ons-js-autosuggest-listbox li").length).to.not.equal(0); - suggestionsOption.click(); - $(MultipleSuggestionsPage.submit()).click(); - expect(browser.getUrl()).to.contain(SubmitPage.url()); + await browser.pause(500); + await browser.keys(" United"); + await expect(await $$(".ons-js-autosuggest-listbox li").length).not.toBe(0); + await suggestionsOption.click(); + await click(MultipleSuggestionsPage.submit()); + await verifyUrlContains(SubmitPage.url()); }); }); diff --git a/tests/functional/spec/thank_you.spec.js b/tests/functional/spec/thank_you.spec.js index ad52461a15..1747cd5a87 100644 --- a/tests/functional/spec/thank_you.spec.js +++ b/tests/functional/spec/thank_you.spec.js @@ -4,67 +4,67 @@ import CheckboxPage from "../generated_pages/title/single-title-block.page"; import ThankYouPage from "../base_pages/thank-you.page"; import DidYouKnowPage from "../generated_pages/thank_you/did-you-know.page"; import ThankYouSubmitPage from "../generated_pages/thank_you/submit.page"; - +import { click, verifyUrlContains } from "../helpers"; describe("Thank You Social", () => { describe("Given I launch a social themed questionnaire", () => { - beforeEach(() => { - browser.openQuestionnaire("test_theme_social.json"); + beforeEach(async () => { + await browser.openQuestionnaire("test_theme_social.json", { theme: "social" }); }); - it("When I navigate to the thank you page, Then I should see social theme content", () => { - $(SubmitPage.submit()).click(); - $(HubPage.submit()).click(); - expect(browser.getUrl()).to.contain(ThankYouPage.pageName); - expect($(ThankYouPage.title()).getHTML()).to.contain("Thank you for completing the Test Social Survey"); - expect($(ThankYouPage.guidance()).getHTML()).to.contain("Your answers have been submitted"); - expect($(ThankYouPage.metadata()).getHTML()).to.contain("Submitted on:"); - expect($(ThankYouPage.metadata()).getHTML()).to.not.contain("Submission reference:"); + it("When I navigate to the thank you page, Then I should see social theme content", async () => { + await click(SubmitPage.submit()); + await click(HubPage.submit()); + await verifyUrlContains(ThankYouPage.pageName); + await expect(await $(ThankYouPage.title()).getHTML()).toContain("Thank you for completing the Test Social Survey"); + await expect(await $(ThankYouPage.guidance()).getHTML()).toContain("Your answers have been submitted"); + await expect(await $(ThankYouPage.metadata()).getHTML()).toContain("Submitted on:"); + await expect(await $(ThankYouPage.metadata()).getHTML()).not.toContain("Submission reference:"); }); }); }); describe("Thank You Default", () => { describe("Given I launch a default themed questionnaire", () => { - beforeEach(() => { - browser.openQuestionnaire("test_title.json"); + beforeEach(async () => { + await browser.openQuestionnaire("test_title.json"); }); - it("When I navigate to the thank you page, Then I should see default theme content", () => { - $(CheckboxPage.good()).click(); - $(SubmitPage.submit()).click(); - $(HubPage.submit()).click(); - expect(browser.getUrl()).to.contain(ThankYouPage.pageName); - expect($(ThankYouPage.title()).getHTML()).to.contain("Thank you for completing the Question Title Test"); - expect($(ThankYouPage.guidance()).getHTML()).to.contain("Your answers have been submitted for"); - expect($(ThankYouPage.metadata()).getHTML()).to.contain("Submitted on:"); - expect($(ThankYouPage.metadata()).getHTML()).to.contain("Submission reference:"); + it("When I navigate to the thank you page, Then I should see default theme content", async () => { + await $(CheckboxPage.good()).click(); + await click(SubmitPage.submit()); + await click(HubPage.submit()); + await verifyUrlContains(ThankYouPage.pageName); + await expect(await $(ThankYouPage.title()).getHTML()).toContain("Thank you for completing the Question Title Test"); + await expect(await $(ThankYouPage.guidance()).getHTML()).toContain("Your answers have been submitted for"); + await expect(await $(ThankYouPage.metadata()).getHTML()).toContain("Submitted on:"); + await expect(await $(ThankYouPage.metadata()).getHTML()).toContain("Submission reference:"); }); }); }); describe("Thank You Default View Response Enabled", () => { describe("Given I launch a questionnaire where view response is enabled", () => { - beforeEach(() => { - browser.openQuestionnaire("test_thank_you.json"); - $(DidYouKnowPage.yes()).click(); - $(DidYouKnowPage.submit()).click(); - $(ThankYouSubmitPage.submit()).click(); - expect(browser.getUrl()).to.contain(ThankYouPage.pageName); + beforeEach(async () => { + await browser.openQuestionnaire("test_thank_you.json"); + await $(DidYouKnowPage.yes()).click(); + await click(DidYouKnowPage.submit()); + await click(ThankYouSubmitPage.submit()); + await verifyUrlContains(ThankYouPage.pageName); }); - it("When I navigate to the thank you page, and I have submitted less than 40 seconds ago, Then I should see the countdown timer and option to view my answers", () => { - expect($(ThankYouPage.viewSubmittedGuidance()).isDisplayed()).to.be.false; - expect($(ThankYouPage.title()).getHTML()).to.contain("Thank you for completing the Test Thank You"); - expect($(ThankYouPage.viewAnswersTitle()).getHTML()).to.contain("Get a copy of your answers"); - expect($(ThankYouPage.viewAnswersLink()).getText()).to.contain("save or print your answers"); - expect($(ThankYouPage.viewSubmittedCountdown()).getHTML()).to.contain("For security, your answers will only be available to view for another"); + it("When I navigate to the thank you page, and I have submitted less than 40 seconds ago, Then I should see the countdown timer and option to view my answers", async () => { + await expect(await $(ThankYouPage.viewSubmittedGuidance()).isDisplayed()).toBe(false); + await expect(await $(ThankYouPage.title()).getHTML()).toContain("Thank you for completing the Test Thank You"); + await expect(await $(ThankYouPage.viewAnswersTitle()).getHTML()).toContain("Get a copy of your answers"); + await expect(await $(ThankYouPage.viewAnswersLink()).getText()).toContain("save or print your answers"); + await expect(await $(ThankYouPage.viewSubmittedCountdown()).getHTML()).toContain("For security, your answers will only be available to view for another"); }); - it("When I navigate to the thank you page, and I have submitted more than 40 seconds ago, Then I shouldn't see the option to view my answers", () => { - expect($(ThankYouPage.viewSubmittedGuidance()).isDisplayed()).to.be.false; - browser.pause(46000); // Waiting 40 seconds for the timeout to expire (45 minute timeout changed to 35 seconds by overriding VIEW_SUBMITTED_RESPONSE_EXPIRATION_IN_SECONDS for the purpose of the functional test) - expect($(ThankYouPage.viewSubmittedGuidance()).isDisplayed()).to.be.true; - expect($(ThankYouPage.viewSubmittedGuidance()).getHTML()).to.contain("For security, you can no longer view or get a copy of your answers"); + it("When I navigate to the thank you page, and I have submitted more than 40 seconds ago, Then I shouldn't see the option to view my answers", async () => { + await expect(await $(ThankYouPage.viewSubmittedGuidance()).isDisplayed()).toBe(false); + await browser.pause(46000); // Waiting 40 seconds for the timeout to expire (45 minute timeout changed to 35 seconds by overriding VIEW_SUBMITTED_RESPONSE_EXPIRATION_IN_SECONDS for the purpose of the functional test) + await expect(await $(ThankYouPage.viewSubmittedGuidance()).isDisplayed()).toBe(true); + await expect(await $(ThankYouPage.viewSubmittedGuidance()).getHTML()).toContain("For security, you can no longer view or get a copy of your answers"); }); }); }); diff --git a/tests/functional/spec/theme_dbt.spec.js b/tests/functional/spec/theme_dbt.spec.js new file mode 100644 index 0000000000..3dea14156a --- /dev/null +++ b/tests/functional/spec/theme_dbt.spec.js @@ -0,0 +1,15 @@ +import RadioPage from "../generated_pages/theme_dbt/radio.page"; +import { verifyUrlContains } from "../helpers"; + +describe("Theme DBT", () => { + describe("Given I launch a DBT themed questionnaire", () => { + before(async () => { + await browser.openQuestionnaire("test_theme_dbt.json"); + }); + + it("When I navigate to the radio page, Then I should see DBT theme content", async () => { + await verifyUrlContains(RadioPage.pageName); + await expect(await $("#dbt-logo-alt").getHTML()).toContain("Department for Business and Trade"); + }); + }); +}); diff --git a/tests/functional/spec/theme_dbt_dsit.spec.js b/tests/functional/spec/theme_dbt_dsit.spec.js new file mode 100644 index 0000000000..b52afe2d35 --- /dev/null +++ b/tests/functional/spec/theme_dbt_dsit.spec.js @@ -0,0 +1,16 @@ +import RadioPage from "../generated_pages/theme_dbt_dsit/radio.page"; +import { verifyUrlContains } from "../helpers"; + +describe("Theme DBT-DSIT", () => { + describe("Given I launch a DBT-DSIT themed questionnaire", () => { + before(async () => { + await browser.openQuestionnaire("test_theme_dbt_dsit.json"); + }); + + it("When I navigate to the radio page, Then I should see DBT-DSIT theme content", async () => { + await verifyUrlContains(RadioPage.pageName); + await expect(await $("#dbt-logo-alt").getHTML()).toContain("Department for Business and Trade logo"); + await expect(await $("#dsit-logo-alt").getHTML()).toContain("Department for Science, Innovation and Technology logo"); + }); + }); +}); diff --git a/tests/functional/spec/theme_dbt_dsit_ni.spec.js b/tests/functional/spec/theme_dbt_dsit_ni.spec.js new file mode 100644 index 0000000000..23e3e801f1 --- /dev/null +++ b/tests/functional/spec/theme_dbt_dsit_ni.spec.js @@ -0,0 +1,17 @@ +import RadioPage from "../generated_pages/theme_dbt_dsit_ni/radio.page"; +import { verifyUrlContains } from "../helpers"; + +describe("Theme DBT-DSIT-NI", () => { + describe("Given I launch a DBT-DSIT-NI themed questionnaire", () => { + before(async () => { + await browser.openQuestionnaire("test_theme_dbt_dsit_ni.json"); + }); + + it("When I navigate to the radio page, Then I should see DBT-DSIT-NI theme content", async () => { + await verifyUrlContains(RadioPage.pageName); + await expect(await $("#dbt-logo-alt").getHTML()).toContain("Department for Business and Trade logo"); + await expect(await $("#dsit-logo-alt").getHTML()).toContain("Department for Science, Innovation and Technology logo"); + await expect(await $("#finance-ni-logo-alt").getHTML()).toContain("Northern Ireland Department of Finance logo"); + }); + }); +}); diff --git a/tests/functional/spec/theme_dbt_ni.spec.js b/tests/functional/spec/theme_dbt_ni.spec.js new file mode 100644 index 0000000000..83915b7cd8 --- /dev/null +++ b/tests/functional/spec/theme_dbt_ni.spec.js @@ -0,0 +1,16 @@ +import RadioPage from "../generated_pages/theme_dbt_ni/radio.page"; +import { verifyUrlContains } from "../helpers"; + +describe("Theme DBT-NI", () => { + describe("Given I launch a DBT-NI themed questionnaire", () => { + before(async () => { + await browser.openQuestionnaire("test_theme_dbt_ni.json"); + }); + + it("When I navigate to the radio page, Then I should see DBT-NI theme content", async () => { + await verifyUrlContains(RadioPage.pageName); + await expect(await $("#dbt-logo-alt").getHTML()).toContain("Department for Business and Trade"); + await expect(await $("#finance-ni-logo-alt").getHTML()).toContain("Northern Ireland Department of Finance logo"); + }); + }); +}); diff --git a/tests/functional/spec/theme_desnz.spec.js b/tests/functional/spec/theme_desnz.spec.js new file mode 100644 index 0000000000..744283562f --- /dev/null +++ b/tests/functional/spec/theme_desnz.spec.js @@ -0,0 +1,15 @@ +import RadioPage from "../generated_pages/theme_desnz/radio.page"; +import { verifyUrlContains } from "../helpers"; + +describe("Theme DESNZ", () => { + describe("Given I launch a DESNZ themed questionnaire", () => { + before(async () => { + await browser.openQuestionnaire("test_theme_desnz.json"); + }); + + it("When I navigate to the radio page, Then I should see DESNZ theme content", async () => { + await verifyUrlContains(RadioPage.pageName); + await expect(await $("#desnz-logo-alt").getHTML()).toContain("Department for Energy Security and Net Zero"); + }); + }); +}); diff --git a/tests/functional/spec/theme_desnz_ni.spec.js b/tests/functional/spec/theme_desnz_ni.spec.js new file mode 100644 index 0000000000..0f9bf748a8 --- /dev/null +++ b/tests/functional/spec/theme_desnz_ni.spec.js @@ -0,0 +1,16 @@ +import RadioPage from "../generated_pages/theme_desnz_ni/radio.page"; +import { verifyUrlContains } from "../helpers"; + +describe("Theme DESNZ-NI", () => { + describe("Given I launch a DESNZ-NI themed questionnaire", () => { + before(async () => { + await browser.openQuestionnaire("test_theme_desnz_ni.json"); + }); + + it("When I navigate to the radio page, Then I should see DESNZ-NI theme content", async () => { + await verifyUrlContains(RadioPage.pageName); + await expect(await $("#desnz-logo-alt").getHTML()).toContain("Department for Energy Security and Net Zero"); + await expect(await $("#finance-ni-logo-alt").getHTML()).toContain("Northern Ireland Department of Finance logo"); + }); + }); +}); diff --git a/tests/functional/spec/theme_nhse.spec.js b/tests/functional/spec/theme_nhse.spec.js new file mode 100644 index 0000000000..760b0940c5 --- /dev/null +++ b/tests/functional/spec/theme_nhse.spec.js @@ -0,0 +1,17 @@ +import RadioPage from "../generated_pages/theme_ons_nhs/radio.page"; +import { expect } from "@wdio/globals"; +import { verifyUrlContains } from "../helpers"; + +describe("Theme NHSE", () => { + describe("Given I launch a NHSE themed questionnaire", () => { + before(async () => { + await browser.openQuestionnaire("test_theme_ons_nhs.json"); + }); + + it("When I navigate to the radio page, Then I should see NHSE theme content", async () => { + await verifyUrlContains(RadioPage.pageName); + await expect(await $("#ons-logo-stacked-en-alt").getHTML()).toContain("Office for National Statistics"); + await expect(await $("#nhs-logo-alt").getHTML()).toContain("National Heath Service"); + }); + }); +}); diff --git a/tests/functional/spec/theme_northernireland.spec.js b/tests/functional/spec/theme_northernireland.spec.js index 95ed3d4935..f3eef0e3f4 100644 --- a/tests/functional/spec/theme_northernireland.spec.js +++ b/tests/functional/spec/theme_northernireland.spec.js @@ -1,14 +1,15 @@ import RadioPage from "../generated_pages/theme_northernireland/radio.page"; +import { verifyUrlContains } from "../helpers"; describe("Theme Northern Ireland", () => { describe("Given I launch a Northern Ireland themed questionnaire", () => { - before(() => { - browser.openQuestionnaire("test_theme_northernireland.json"); + before(async () => { + await browser.openQuestionnaire("test_theme_northernireland.json"); }); - it("When I navigate to the radio page, Then I should see Northern Ireland theme content", () => { - expect(browser.getUrl()).to.contain(RadioPage.pageName); - expect($("#ni-finance-logo-alt").getHTML()).to.contain("Northern Ireland Department of Finance logo"); + it("When I navigate to the radio page, Then I should see Northern Ireland theme content", async () => { + await verifyUrlContains(RadioPage.pageName); + await expect(await $("#finance-ni-logo-alt").getHTML()).toContain("Northern Ireland Department of Finance logo"); }); }); }); diff --git a/tests/functional/spec/theme_orr.spec.js b/tests/functional/spec/theme_orr.spec.js new file mode 100644 index 0000000000..ae42be571a --- /dev/null +++ b/tests/functional/spec/theme_orr.spec.js @@ -0,0 +1,15 @@ +import RadioPage from "../generated_pages/theme_orr/radio.page"; +import { verifyUrlContains } from "../helpers"; + +describe("Theme Rail and Road", () => { + describe("Given I launch a Rail and Road themed questionnaire", () => { + before(async () => { + await browser.openQuestionnaire("test_theme_orr.json"); + }); + + it("When I navigate to the radio page, Then I should see Rail and Road theme content", async () => { + await verifyUrlContains(RadioPage.pageName); + await expect(await $("#orr-logo-mobile-alt").getHTML()).toContain("Office of Rail and Road logo"); + }); + }); +}); diff --git a/tests/functional/spec/theme_ukhsa_ons.spec.js b/tests/functional/spec/theme_ukhsa_ons.spec.js new file mode 100644 index 0000000000..116b7ea828 --- /dev/null +++ b/tests/functional/spec/theme_ukhsa_ons.spec.js @@ -0,0 +1,17 @@ +import RadioPage from "../generated_pages/theme_dbt_ni/radio.page"; +import { expect } from "@wdio/globals"; +import { verifyUrlContains } from "../helpers"; + +describe("Theme UKHSA-ONS", () => { + describe("Given I launch a UKHSA-ONS themed questionnaire", () => { + before(async () => { + await browser.openQuestionnaire("test_theme_ukhsa_ons.json"); + }); + + it("When I navigate to the radio page, Then I should see UKHSA-ONS theme content", async () => { + await verifyUrlContains(RadioPage.pageName); + await expect(await $("#ons-logo-stacked-en-alt").getHTML()).toContain("Office for National Statistics"); + await expect(await $("#ukhsa-logo-alt").getHTML()).toContain("UK Health Security Agency"); + }); + }); +}); diff --git a/tests/functional/spec/timeout/timeout_modal.js b/tests/functional/spec/timeout/timeout_modal.js index d8e3ed000e..56275134b1 100644 --- a/tests/functional/spec/timeout/timeout_modal.js +++ b/tests/functional/spec/timeout/timeout_modal.js @@ -1,46 +1,50 @@ import { TimeoutModalPage } from "../../base_pages/timeout-modal.page.js"; +import { click, verifyUrlContains } from "../../helpers"; class TestCase { - testCase(page) { - it("When the timeout modal is displayed, and I do not extend my session, Then I will be redirected to the session expired page", () => { - this.checkTimeoutModal(); - browser.pause(65000); // We are waiting for the session to expire - expect(browser.getUrl()).to.contain("/session-expired"); - expect($("body").getHTML()) - .to.include( - "Sorry, you need to sign in again", - "This is because you have either:", - "been inactive for 45 minutes and your session has timed out to protect your information", - "followed a link to a page you are not signed in to", - "followed a link to a survey that has already been submitted" - ) - .to.not.include("To protect your information, your progress will be saved and you will be signed out in"); + testCaseExpired(page) { + it("When the timeout modal is displayed, and I do not extend my session, Then I will be redirected to the session expired page", async () => { + await this.checkTimeoutModal(); + await browser.pause(65000); // We are waiting for the session to expire + await verifyUrlContains("/session-expired"); + await expect(await $("body").getHTML()).toContain( + "Sorry, you need to sign in again", + "This is because you have either:", + "been inactive for 45 minutes and your session has timed out to protect your information", + "followed a link to a page you are not signed in to", + "followed a link to a survey that has already been submitted", + ); + await expect(await $("body").getHTML()).not.toContain("To protect your information, your progress will be saved and you will be signed out in"); }).timeout(140000); + } - it("When the timeout modal is displayed, and I click the “Continue survey” button, Then my session will be extended", () => { - this.checkTimeoutModal(); - $(TimeoutModalPage.submit()).click(); - expect($(TimeoutModalPage.timer()).getText()).to.equal(""); - browser.pause(65000); // Waiting 65 seconds to sanity check that it hasn’t expired - browser.refresh(); - expect(browser.getUrl()).to.contain(page.pageName); - expect($("body").getHTML()).to.not.include("Sorry, you need to sign in again"); + testCaseExtended(page) { + it("When the timeout modal is displayed, and I click the “Continue survey” button, Then my session will be extended", async () => { + await this.checkTimeoutModal(); + await click(TimeoutModalPage.submit()); + await expect(await $(TimeoutModalPage.timer()).getText()).toBe(""); + await browser.pause(65000); // Waiting 65 seconds to sanity check that it hasn’t expired + await browser.refresh(); + await verifyUrlContains(await page.pageName); + await expect(await $("body").getHTML()).not.toContain("Sorry, you need to sign in again"); }).timeout(140000); + } - it("When the timeout modal is displayed, but I open a new window and then focus back on the timeout modal window, Then my session will be extended", () => { - this.checkTimeoutModal(); - browser.newWindow(""); - browser.switchWindow(page.pageName); - browser.refresh(); - browser.pause(65000); // Waiting 65 seconds to sanity check that it hasn’t expired - expect(browser.getUrl()).to.contain(page.pageName); + testCaseExtendedNewWindow(page) { + it("When the timeout modal is displayed, but I open a new window and then focus back on the timeout modal window, Then my session will be extended", async () => { + await this.checkTimeoutModal(); + await browser.newWindow(""); + await browser.switchWindow(await page.pageName); + await browser.refresh(); + await browser.pause(65000); // Waiting 65 seconds to sanity check that it hasn’t expired + await verifyUrlContains(await page.pageName); }).timeout(140000); } - checkTimeoutModal() { - $(TimeoutModalPage.timer()).waitForDisplayed({ timeout: 70000 }); - expect($(TimeoutModalPage.timer()).getText()).to.equal( - "To protect your information, your progress will be saved and you will be signed out in 59 seconds." + async checkTimeoutModal() { + await $(TimeoutModalPage.timer()).waitForDisplayed({ timeout: 70000 }); + await expect(await $(TimeoutModalPage.timer()).getText()).toBe( + "To protect your information, your progress will be saved and you will be signed out in 59 seconds.", ); } } diff --git a/tests/functional/spec/timeout/timeout_modal.spec.js b/tests/functional/spec/timeout/timeout_modal.spec.js deleted file mode 100644 index cbb423a349..0000000000 --- a/tests/functional/spec/timeout/timeout_modal.spec.js +++ /dev/null @@ -1,11 +0,0 @@ -import TimeoutInterstitialPage from "../../generated_pages/timeout_modal/timeout-modal-interstitial.page"; -import { TimeoutModalTestCase } from "./timeout_modal.js"; - -describe("Timeout Modal", () => { - describe("Given I am completing the survey,", () => { - beforeEach(() => { - browser.openQuestionnaire("test_timeout_modal.json"); - }); - TimeoutModalTestCase.testCase(TimeoutInterstitialPage); - }); -}); diff --git a/tests/functional/spec/timeout/timeout_modal_expired/timeout_modal_expired.spec.js b/tests/functional/spec/timeout/timeout_modal_expired/timeout_modal_expired.spec.js new file mode 100644 index 0000000000..52395bb078 --- /dev/null +++ b/tests/functional/spec/timeout/timeout_modal_expired/timeout_modal_expired.spec.js @@ -0,0 +1,11 @@ +import TimeoutInterstitialPage from "../../../generated_pages/timeout_modal/timeout-modal-interstitial.page"; +import { TimeoutModalTestCase } from "../timeout_modal.js"; + +describe("Timeout Modal Expired", () => { + describe("Given I am completing the survey,", () => { + before(async () => { + await browser.openQuestionnaire("test_timeout_modal.json"); + }); + TimeoutModalTestCase.testCaseExpired(TimeoutInterstitialPage); + }); +}); diff --git a/tests/functional/spec/timeout/timeout_modal_expired/timeout_modal_post_submission_expired.spec.js b/tests/functional/spec/timeout/timeout_modal_expired/timeout_modal_post_submission_expired.spec.js new file mode 100644 index 0000000000..714bda47d9 --- /dev/null +++ b/tests/functional/spec/timeout/timeout_modal_expired/timeout_modal_post_submission_expired.spec.js @@ -0,0 +1,16 @@ +import TimeoutInterstitialPage from "../../../generated_pages/timeout_modal/timeout-modal-interstitial.page"; +import TimeoutSubmitPage from "../../../generated_pages/timeout_modal/submit.page"; +import ThankYouPage from "../../../base_pages/thank-you.page"; +import { TimeoutModalTestCase } from "../timeout_modal.js"; +import { click } from "../../../helpers"; + +describe("Timeout Modal Post Submission Expired", () => { + describe("Given I am completing the survey and get to post submission page,", () => { + before(async () => { + await browser.openQuestionnaire("test_timeout_modal.json"); + await click(TimeoutInterstitialPage.submit()); + await click(TimeoutSubmitPage.submit()); + }); + TimeoutModalTestCase.testCaseExpired(ThankYouPage); + }); +}); diff --git a/tests/functional/spec/timeout/timeout_modal_extended/timeout_modal_extended.spec.js b/tests/functional/spec/timeout/timeout_modal_extended/timeout_modal_extended.spec.js new file mode 100644 index 0000000000..14795ea524 --- /dev/null +++ b/tests/functional/spec/timeout/timeout_modal_extended/timeout_modal_extended.spec.js @@ -0,0 +1,11 @@ +import TimeoutInterstitialPage from "../../../generated_pages/timeout_modal/timeout-modal-interstitial.page"; +import { TimeoutModalTestCase } from "../timeout_modal.js"; + +describe("Timeout Modal Expired", () => { + describe("Given I am completing the survey,", () => { + before(async () => { + await browser.openQuestionnaire("test_timeout_modal.json"); + }); + TimeoutModalTestCase.testCaseExtended(TimeoutInterstitialPage); + }); +}); diff --git a/tests/functional/spec/timeout/timeout_modal_extended/timeout_modal_post_submission_extended.spec.js b/tests/functional/spec/timeout/timeout_modal_extended/timeout_modal_post_submission_extended.spec.js new file mode 100644 index 0000000000..9c701a7002 --- /dev/null +++ b/tests/functional/spec/timeout/timeout_modal_extended/timeout_modal_post_submission_extended.spec.js @@ -0,0 +1,16 @@ +import TimeoutInterstitialPage from "../../../generated_pages/timeout_modal/timeout-modal-interstitial.page"; +import TimeoutSubmitPage from "../../../generated_pages/timeout_modal/submit.page"; +import ThankYouPage from "../../../base_pages/thank-you.page"; +import { TimeoutModalTestCase } from "../timeout_modal.js"; +import { click } from "../../../helpers"; + +describe("Timeout Modal Post Submission Expired", () => { + describe("Given I am completing the survey and get to post submission page,", () => { + before(async () => { + await browser.openQuestionnaire("test_timeout_modal.json"); + await click(TimeoutInterstitialPage.submit()); + await click(TimeoutSubmitPage.submit()); + }); + TimeoutModalTestCase.testCaseExtended(ThankYouPage); + }); +}); diff --git a/tests/functional/spec/timeout/timeout_modal_extended_new_window/timeout_modal_extended_new_window.spec.js b/tests/functional/spec/timeout/timeout_modal_extended_new_window/timeout_modal_extended_new_window.spec.js new file mode 100644 index 0000000000..eedd00ddc4 --- /dev/null +++ b/tests/functional/spec/timeout/timeout_modal_extended_new_window/timeout_modal_extended_new_window.spec.js @@ -0,0 +1,11 @@ +import TimeoutInterstitialPage from "../../../generated_pages/timeout_modal/timeout-modal-interstitial.page"; +import { TimeoutModalTestCase } from "../timeout_modal.js"; + +describe("Timeout Modal Expired", () => { + describe("Given I am completing the survey,", () => { + before(async () => { + await browser.openQuestionnaire("test_timeout_modal.json"); + }); + TimeoutModalTestCase.testCaseExtendedNewWindow(TimeoutInterstitialPage); + }); +}); diff --git a/tests/functional/spec/timeout/timeout_modal_extended_new_window/timeout_modal_post_submission_extended_new_window.spec.js b/tests/functional/spec/timeout/timeout_modal_extended_new_window/timeout_modal_post_submission_extended_new_window.spec.js new file mode 100644 index 0000000000..17bb98c81b --- /dev/null +++ b/tests/functional/spec/timeout/timeout_modal_extended_new_window/timeout_modal_post_submission_extended_new_window.spec.js @@ -0,0 +1,16 @@ +import TimeoutInterstitialPage from "../../../generated_pages/timeout_modal/timeout-modal-interstitial.page"; +import TimeoutSubmitPage from "../../../generated_pages/timeout_modal/submit.page"; +import ThankYouPage from "../../../base_pages/thank-you.page"; +import { TimeoutModalTestCase } from "../timeout_modal.js"; +import { click } from "../../../helpers"; + +describe("Timeout Modal Post Submission Expired", () => { + describe("Given I am completing the survey and get to post submission page,", () => { + before(async () => { + await browser.openQuestionnaire("test_timeout_modal.json"); + await click(TimeoutInterstitialPage.submit()); + await click(TimeoutSubmitPage.submit()); + }); + TimeoutModalTestCase.testCaseExtendedNewWindow(ThankYouPage); + }); +}); diff --git a/tests/functional/spec/timeout/timeout_modal_post_submission.spec.js b/tests/functional/spec/timeout/timeout_modal_post_submission.spec.js deleted file mode 100644 index 43fa2680dd..0000000000 --- a/tests/functional/spec/timeout/timeout_modal_post_submission.spec.js +++ /dev/null @@ -1,16 +0,0 @@ -import TimeoutInterstitialPage from "../../generated_pages/timeout_modal/timeout-modal-interstitial.page"; -import TimeoutSubmitPage from "../../generated_pages/timeout_modal/submit.page"; -import ThankYouPage from "../../base_pages/thank-you.page.js"; - -import { TimeoutModalTestCase } from "./timeout_modal.js"; - -describe("Timeout Modal Post Submission", () => { - describe("Given I am completing the survey and get to post submission page,", () => { - beforeEach(() => { - browser.openQuestionnaire("test_timeout_modal.json"); - $(TimeoutInterstitialPage.submit()).click(); - $(TimeoutSubmitPage.submit()).click(); - }); - TimeoutModalTestCase.testCase(ThankYouPage); - }); -}); diff --git a/tests/functional/wdio.conf.js b/tests/functional/wdio.conf.cjs similarity index 90% rename from tests/functional/wdio.conf.js rename to tests/functional/wdio.conf.cjs index edc15ed7c4..24ddfbe834 100644 --- a/tests/functional/wdio.conf.js +++ b/tests/functional/wdio.conf.cjs @@ -16,12 +16,19 @@ exports.config = { // NPM script (see https://docs.npmjs.com/cli/run-script) then the current working // directory is where your package.json resides, so `wdio` will be called from there. // - specs: ["./tests/functional/spec/**/*.js"], + specs: ["./spec/**/*.js"], suites: { - timeout_modal: ["./tests/functional/spec/timeout/*.spec.js"], - components: ["./tests/functional/spec/components/**/*.js"], - features: ["./tests/functional/spec/features/**/*.js"], - general: ["./tests/functional/spec/*.spec.js"], + timeout_modal_expired: ["./spec/timeout/timeout_modal_expired/*.js"], + timeout_modal_extended: ["./spec/timeout/timeout_modal_extended/*.js"], + timeout_modal_extended_new_window: ["./spec/timeout/timeout_modal_extended_new_window/*.js"], + components: ["./spec/components/**/*.js"], + features: ["./spec/features/**/*.js"], + summaries: ["./spec/summaries/**/*.js"], + journeys: ["./spec/journeys/**/*.js"], + list_collector: ["./spec/list_collector/**/*.js"], + general: ["./spec/*.spec.js"], + hub_and_spoke: ["./spec/hub_and_spoke/**/*.js"], + supplementary_data: ["./spec/supplementary_data/**/*.js"], }, // Patterns to exclude. exclude: [ @@ -43,7 +50,7 @@ exports.config = { // and 30 processes will get spawned. The property handles how many capabilities // from the same test should run tests. // - maxInstances: 2, + maxInstances: parseInt(process.env.EQ_FUNCTIONAL_TEST_MAX_INSTANCES || 2), // If you have trouble getting all important capabilities together, check out the // Sauce Labs platform configurator - a great tool to configure your capabilities: // https://docs.saucelabs.com/reference/platforms-configurator @@ -51,6 +58,7 @@ exports.config = { capabilities: [ { browserName: "chrome", + browserVersion: "stable", // If outputDir is provided WebdriverIO can capture driver session logs // it is possible to configure which logTypes to include/exclude. // excludeDriverLogs: ['*'], // pass '*' to exclude all driver session logs @@ -114,7 +122,7 @@ exports.config = { // Services take over a specific job you don't want to take care of. They enhance // your test setup with almost no effort. Unlike plugins, they don't add new // commands. Instead, they hook themselves up into the test process. - services: ["chromedriver"], + services: [], // Framework you want to run your specs with. // The following are supported: Mocha, Jasmine, and Cucumber @@ -191,40 +199,49 @@ exports.config = { * @param {Array} args arguments that command would receive */ before: async function (capabilities, specs) { - const chai = require("chai"); const JwtHelper = require("./jwt_helper"); - global.expect = chai.expect; - await browser.addCommand( "openQuestionnaire", async function ( schema, { + launchVersion = "v2", + theme = "default", userId = JwtHelper.getRandomString(10), collectionId = JwtHelper.getRandomString(10), responseId = JwtHelper.getRandomString(16), + surveyId = "123", periodId = "201605", periodStr = "May 2016", + ruRef = "12345678901A", + sdsDatasetId = null, region = "GB-ENG", language = "en", - sexualIdentity = false, includeLogoutUrl = false, - } = {} + cirInstrumentId = null, + booleanFlag = false, + } = {}, ) { const token = await JwtHelper.generateToken(schema, { + launchVersion, + theme, userId, collectionId, responseId, + surveyId, periodId, periodStr, + ruRef, + sdsDatasetId, regionCode: region, languageCode: language, - sexualIdentity, includeLogoutUrl, + cirInstrumentId, + booleanFlag, }); this.url(`/session?token=${token}`); - } + }, ); }, // beforeCommand: function (commandName, args) { diff --git a/tests/integration/components/mutually_exclusive/test_checkbox_single_checkbox_override.py b/tests/integration/components/mutually_exclusive/test_checkbox_single_checkbox_override.py index aa2062169c..263fc35a66 100644 --- a/tests/integration/components/mutually_exclusive/test_checkbox_single_checkbox_override.py +++ b/tests/integration/components/mutually_exclusive/test_checkbox_single_checkbox_override.py @@ -9,7 +9,7 @@ class TestCheckboxSingleCheckboxOverride(IntegrationTestCase): def setUp(self): super().setUp() - self.launchSurvey("test_mutually_exclusive") + self.launchSurveyV2(schema_name="test_mutually_exclusive") def test_non_exclusive_answer(self): # When diff --git a/tests/integration/components/mutually_exclusive/test_currency_single_checkbox_override.py b/tests/integration/components/mutually_exclusive/test_currency_single_checkbox_override.py index 1b02997a1d..f1823808bc 100644 --- a/tests/integration/components/mutually_exclusive/test_currency_single_checkbox_override.py +++ b/tests/integration/components/mutually_exclusive/test_currency_single_checkbox_override.py @@ -12,7 +12,7 @@ class TestCurrencySingleCheckboxOverride(IntegrationTestCase): def setUp(self): super().setUp() - self.launchSurvey("test_mutually_exclusive") + self.launchSurveyV2(schema_name="test_mutually_exclusive") self.get(MUTUALLY_EXCLUSIVE_CURRENCY) def test_non_exclusive_answer(self): @@ -21,7 +21,7 @@ def test_non_exclusive_answer(self): # Then self.assertInUrl("/sections/mutually-exclusive-currency-section/") - self.assertInBody("ÂŖ10.00") + self.assertInBody("ÂŖ10") def test_exclusive_answer(self): # When diff --git a/tests/integration/components/mutually_exclusive/test_date_single_checkbox_override.py b/tests/integration/components/mutually_exclusive/test_date_single_checkbox_override.py index 11917c54a1..bf32b3a7db 100644 --- a/tests/integration/components/mutually_exclusive/test_date_single_checkbox_override.py +++ b/tests/integration/components/mutually_exclusive/test_date_single_checkbox_override.py @@ -12,7 +12,7 @@ class TestDateSingleCheckboxOverride(IntegrationTestCase): def setUp(self): super().setUp() - self.launchSurvey("test_mutually_exclusive") + self.launchSurveyV2(schema_name="test_mutually_exclusive") self.get(MUTUALLY_EXCLUSIVE_DAY_MONTH_YEAR_DATE) def test_non_exclusive_answer(self): diff --git a/tests/integration/components/mutually_exclusive/test_duration_single_checkbox_override.py b/tests/integration/components/mutually_exclusive/test_duration_single_checkbox_override.py index d7e571617c..812c8bd3ee 100644 --- a/tests/integration/components/mutually_exclusive/test_duration_single_checkbox_override.py +++ b/tests/integration/components/mutually_exclusive/test_duration_single_checkbox_override.py @@ -12,7 +12,7 @@ class TestDurationSingleCheckboxOverride(IntegrationTestCase): def setUp(self): super().setUp() - self.launchSurvey("test_mutually_exclusive") + self.launchSurveyV2(schema_name="test_mutually_exclusive") self.get(MUTUALLY_EXCLUSIVE_DURATION) def test_non_exclusive_answer(self): diff --git a/tests/integration/components/mutually_exclusive/test_month_year_date_single_checkbox_override.py b/tests/integration/components/mutually_exclusive/test_month_year_date_single_checkbox_override.py index daff7e1da9..6e61d57440 100644 --- a/tests/integration/components/mutually_exclusive/test_month_year_date_single_checkbox_override.py +++ b/tests/integration/components/mutually_exclusive/test_month_year_date_single_checkbox_override.py @@ -12,7 +12,7 @@ class TestMonthYearDateSingleCheckboxOverride(IntegrationTestCase): def setUp(self): super().setUp() - self.launchSurvey("test_mutually_exclusive") + self.launchSurveyV2(schema_name="test_mutually_exclusive") self.get(MUTUALLY_EXCLUSIVE_MONTH_YEAR_DATE) def test_non_exclusive_answer(self): diff --git a/tests/integration/components/mutually_exclusive/test_number_single_checkbox_override.py b/tests/integration/components/mutually_exclusive/test_number_single_checkbox_override.py index 09813b6b5c..276cd603af 100644 --- a/tests/integration/components/mutually_exclusive/test_number_single_checkbox_override.py +++ b/tests/integration/components/mutually_exclusive/test_number_single_checkbox_override.py @@ -12,7 +12,7 @@ class TestNumberSingleCheckboxOverride(IntegrationTestCase): def setUp(self): super().setUp() - self.launchSurvey("test_mutually_exclusive") + self.launchSurveyV2(schema_name="test_mutually_exclusive") self.get(MUTUALLY_EXCLUSIVE_NUMBER) def test_non_exclusive_answer(self): diff --git a/tests/integration/components/mutually_exclusive/test_percentage_single_checkbox_override.py b/tests/integration/components/mutually_exclusive/test_percentage_single_checkbox_override.py index 9644a6692d..8a8250c640 100644 --- a/tests/integration/components/mutually_exclusive/test_percentage_single_checkbox_override.py +++ b/tests/integration/components/mutually_exclusive/test_percentage_single_checkbox_override.py @@ -12,7 +12,7 @@ class TestPercentageSingleCheckboxOverride(IntegrationTestCase): def setUp(self): super().setUp() - self.launchSurvey("test_mutually_exclusive") + self.launchSurveyV2(schema_name="test_mutually_exclusive") self.get(MUTUALLY_EXCLUSIVE_PERCENTAGE) def test_non_exclusive_answer(self): diff --git a/tests/integration/components/mutually_exclusive/test_texarea_single_checkbox_override.py b/tests/integration/components/mutually_exclusive/test_texarea_single_checkbox_override.py index e8585fd287..45acdb6de3 100644 --- a/tests/integration/components/mutually_exclusive/test_texarea_single_checkbox_override.py +++ b/tests/integration/components/mutually_exclusive/test_texarea_single_checkbox_override.py @@ -12,7 +12,7 @@ class TestTextAreaSingleCheckboxOverride(IntegrationTestCase): def setUp(self): super().setUp() - self.launchSurvey("test_mutually_exclusive") + self.launchSurveyV2(schema_name="test_mutually_exclusive") self.get(MUTUALLY_EXCLUSIVE_TEXTAREA) def test_non_exclusive_answer(self): diff --git a/tests/integration/components/mutually_exclusive/test_textfield_single_checkbox_override.py b/tests/integration/components/mutually_exclusive/test_textfield_single_checkbox_override.py index 527bd8ee0b..7512181a8a 100644 --- a/tests/integration/components/mutually_exclusive/test_textfield_single_checkbox_override.py +++ b/tests/integration/components/mutually_exclusive/test_textfield_single_checkbox_override.py @@ -12,7 +12,7 @@ class TestTextFieldSingleCheckboxOverride(IntegrationTestCase): def setUp(self): super().setUp() - self.launchSurvey("test_mutually_exclusive") + self.launchSurveyV2(schema_name="test_mutually_exclusive") self.get(MUTUALLY_EXCLUSIVE_TEXTFIELD) def test_non_exclusive_answer(self): diff --git a/tests/integration/components/mutually_exclusive/test_unit_single_checkbox_override.py b/tests/integration/components/mutually_exclusive/test_unit_single_checkbox_override.py index b336f8bdb0..22f7b16b9b 100644 --- a/tests/integration/components/mutually_exclusive/test_unit_single_checkbox_override.py +++ b/tests/integration/components/mutually_exclusive/test_unit_single_checkbox_override.py @@ -12,7 +12,7 @@ class TestUnitSingleCheckboxOverride(IntegrationTestCase): def setUp(self): super().setUp() - self.launchSurvey("test_mutually_exclusive") + self.launchSurveyV2(schema_name="test_mutually_exclusive") self.get(MUTUALLY_EXCLUSIVE_UNIT) def test_non_exclusive_answer(self): diff --git a/tests/integration/components/mutually_exclusive/test_year_date_single_checkbox_override.py b/tests/integration/components/mutually_exclusive/test_year_date_single_checkbox_override.py index 8930659738..3258fec14b 100644 --- a/tests/integration/components/mutually_exclusive/test_year_date_single_checkbox_override.py +++ b/tests/integration/components/mutually_exclusive/test_year_date_single_checkbox_override.py @@ -12,7 +12,7 @@ class TestYearDateSingleCheckboxOverride(IntegrationTestCase): def setUp(self): super().setUp() - self.launchSurvey("test_mutually_exclusive") + self.launchSurveyV2(schema_name="test_mutually_exclusive") self.get(MUTUALLY_EXCLUSIVE_YEAR_DATE) def test_non_exclusive_answer(self): diff --git a/tests/integration/components/test_address.py b/tests/integration/components/test_address.py index ba413cad18..4941c3516b 100644 --- a/tests/integration/components/test_address.py +++ b/tests/integration/components/test_address.py @@ -6,7 +6,7 @@ class TestAddressFields(IntegrationTestCase): def setUp(self): super().setUp() # Given - self.launchSurvey("test_address") + self.launchSurveyV2(schema_name="test_address") def test_mandatory_address_line_1_is_mandatory(self): # When @@ -66,7 +66,7 @@ class TestLookupAddressFields(IntegrationTestCase): def setUp(self): super().setUp() # Given - self.launchSurvey("test_address_lookups") + self.launchSurveyV2(schema_name="test_address_lookups") def test_address_fields_exist(self): # Then @@ -104,14 +104,14 @@ def test_uprn_field_not_displayed_on_summary(self): self.post( { "address-mandatory-line1": "first address", - "address-mandatory-uprn": "123456789", + "address-mandatory-uprn": "0123456789", } ) self.post({}) # Then - self.assertNotInBody("123456789") + self.assertNotInBody("0123456789") def test_auth_token_not_in_page(self): self.assertNotInBody("data-authorization-token") diff --git a/tests/integration/components/test_cookies_banner_content.py b/tests/integration/components/test_cookies_banner_content.py index 604cb93caf..75d917095d 100644 --- a/tests/integration/components/test_cookies_banner_content.py +++ b/tests/integration/components/test_cookies_banner_content.py @@ -4,7 +4,7 @@ class TestCookiesBannerContent(IntegrationTestCase): def setUp(self): super().setUp() - self.launchSurvey("test_dropdown_mandatory") + self.launchSurveyV2(schema_name="test_dropdown_mandatory") def test_cookies_banner_content(self): self.assertInBody("Tell us whether you accept cookies") diff --git a/tests/integration/components/test_render_dropdown_widget.py b/tests/integration/components/test_render_dropdown_widget.py index 8c09f1dceb..dfbb69b631 100644 --- a/tests/integration/components/test_render_dropdown_widget.py +++ b/tests/integration/components/test_render_dropdown_widget.py @@ -4,7 +4,7 @@ class TestRenderDropdownWidget(IntegrationTestCase): def setUp(self): super().setUp() - self.launchSurvey("test_dropdown_mandatory") + self.launchSurveyV2(schema_name="test_dropdown_mandatory") def test_dropdown_renders(self): self.assertInBody("Select an answer") diff --git a/tests/integration/components/test_render_percentage_widget.py b/tests/integration/components/test_render_percentage_widget.py index 2502a7f0b5..b68f23c524 100644 --- a/tests/integration/components/test_render_percentage_widget.py +++ b/tests/integration/components/test_render_percentage_widget.py @@ -5,10 +5,10 @@ class TestRenderPercentageWidget(IntegrationTestCase): def setUp(self): super().setUp() - self.launchSurvey("test_percentage") + self.launchSurveyV2(schema_name="test_percentage") def test_percentage_widget_has_icon(self): - self.assertInSelectorCSS("%", "abbr", {"class": "ons-input-type__type"}) + self.assertInSelectorCSS(">%", "abbr", {"class": "ons-input-type__type"}) def test_entering_invalid_number_displays_error(self): self.post({"answer": "not a percentage"}) @@ -32,6 +32,7 @@ def test_entering_float_displays_error(self): def test_entering_valid_percentage_redirects_to_summary(self): self.post({"answer": "50"}) + self.post({"answer-decimal": "5.5"}) self.assertStatusOK() self.assertInUrl(SUBMIT_URL_PATH) diff --git a/tests/integration/create_token.py b/tests/integration/create_token.py index 2afeab2dd3..a531b4833c 100644 --- a/tests/integration/create_token.py +++ b/tests/integration/create_token.py @@ -1,31 +1,86 @@ -import time +from copy import deepcopy +from time import time from uuid import uuid4 from sdc.crypto.encrypter import encrypt +from app.authentication.auth_payload_versions import AuthPayloadVersion +from app.data_models.metadata_proxy import TOP_LEVEL_METADATA_KEYS from app.keys import KEY_PURPOSE_AUTHENTICATION +from tests.app.parser.conftest import get_response_expires_at ACCOUNT_SERVICE_URL = "http://upstream.url" -PAYLOAD = { - "user_id": "integration-test", - "period_str": "April 2016", - "period_id": "201604", +TOP_LEVEL_KEYS = TOP_LEVEL_METADATA_KEYS + ["exp", "jti", "iat"] + +PAYLOAD_V2_BUSINESS = { + "version": AuthPayloadVersion.V2.value, + "survey_metadata": { + "data": { + "user_id": "integration-test", + "period_str": "April 2016", + "period_id": "201604", + "ru_ref": "12345678901A", + "ru_name": "Integration Testing", + "ref_p_start_date": "2016-04-01", + "ref_p_end_date": "2016-04-30", + "trad_as": "Integration Tests", + "employment_date": "1983-06-02", + "display_address": "68 Abingdon Road, Goathill", + } + }, "collection_exercise_sid": "789", - "ru_ref": "123456789012A", "response_id": "1234567890123456", - "ru_name": "Integration Testing", - "ref_p_start_date": "2016-04-01", - "ref_p_end_date": "2016-04-30", - "return_by": "2016-05-06", - "trad_as": "Integration Tests", - "employment_date": "1983-06-02", "language_code": "en", "roles": [], "account_service_url": ACCOUNT_SERVICE_URL, - "display_address": "68 Abingdon Road, Goathill", } +PAYLOAD_V2_SUPPLEMENTARY_DATA = { + "version": AuthPayloadVersion.V2.value, + "survey_metadata": { + "data": { + "user_id": "integration-test", + "period_str": "April 2016", + "period_id": "201604", + "ru_ref": "12345678901A", + "ru_name": "Integration Testing", + "ref_p_start_date": "2016-04-01", + "ref_p_end_date": "2016-04-30", + "trad_as": "Integration Tests", + "employment_date": "1983-06-02", + "display_address": "68 Abingdon Road, Goathill", + "sds_dataset_id": "44f1b432-9421-49e5-bd26-e63e18a30b69", + "survey_id": "123", + } + }, + "collection_exercise_sid": "789", + "response_id": "1234567890123456", + "language_code": "en", + "roles": [], + "account_service_url": ACCOUNT_SERVICE_URL, +} + +PAYLOAD_V2_SOCIAL = { + "version": AuthPayloadVersion.V2.value, + "survey_metadata": { + "data": { + "case_ref": "1000000000000001", + "qid": str(uuid4()), + }, + "receipting_keys": ["qid"], + }, + "collection_exercise_sid": "789", + "response_id": "1234567890123456", + "language_code": "en", + "roles": [], + "account_service_url": ACCOUNT_SERVICE_URL, +} + + +def populate_with_extra_payload_items(key, value, payload): + payload[key] = value + class TokenGenerator: def __init__(self, key_store, upstream_kid, sr_public_kid): @@ -34,50 +89,132 @@ def __init__(self, key_store, upstream_kid, sr_public_kid): self._sr_public_kid = sr_public_kid @staticmethod - def _get_payload_with_params(schema_name, survey_url=None, **extra_payload): - payload_vars = PAYLOAD.copy() + def _get_payload_with_params( + *, + schema_name=None, + schema_url=None, + cir_instrument_id=None, + payload=None, + **extra_payload, + ): + if payload is None: + payload = PAYLOAD_V2_BUSINESS + payload_vars = deepcopy(payload) payload_vars["tx_id"] = str(uuid4()) - payload_vars["schema_name"] = schema_name - if survey_url: - payload_vars["survey_url"] = survey_url - - payload_vars["iat"] = time.time() + if schema_name: + payload_vars["schema_name"] = schema_name + if schema_url: + payload_vars["schema_url"] = schema_url + if cir_instrument_id: + payload_vars["cir_instrument_id"] = cir_instrument_id + + payload_vars["iat"] = time() payload_vars["exp"] = payload_vars["iat"] + float(3600) # one hour from now payload_vars["jti"] = str(uuid4()) payload_vars["case_id"] = str(uuid4()) + payload_vars["response_expires_at"] = get_response_expires_at() for key, value in extra_payload.items(): - payload_vars[key] = value + if key in TOP_LEVEL_KEYS: + populate_with_extra_payload_items(key, value, payload_vars) + else: + populate_with_extra_payload_items( + key, value, payload_vars["survey_metadata"]["data"] + ) return payload_vars - def create_token(self, schema_name, **extra_payload): - payload_vars = self._get_payload_with_params(schema_name, None, **extra_payload) + def create_token_v2(self, schema_name, theme="default", **extra_payload): + payload_for_theme = ( + PAYLOAD_V2_SOCIAL if theme == "social" else PAYLOAD_V2_BUSINESS + ) + payload = self._get_payload_with_params( + schema_name=schema_name, payload=payload_for_theme, **extra_payload + ) + + return self.generate_token(payload) - return self.generate_token(payload_vars) + def create_supplementary_data_token(self, schema_name, **extra_payload): + payload = PAYLOAD_V2_SUPPLEMENTARY_DATA + + # iterate over a copy so items can be deleted + for key, value in list(extra_payload.items()): + if key in {"sds_dataset_id", "ru_ref", "survey_id"}: + payload["survey_metadata"]["data"][key] = value + del extra_payload[key] + + payload = self._get_payload_with_params( + schema_name=schema_name, payload=payload, **extra_payload + ) + + return self.generate_token(payload) + + def create_token_invalid_version(self, schema_name, **extra_payload): + payload = self._get_payload_with_params( + schema_name=schema_name, payload=PAYLOAD_V2_BUSINESS, **extra_payload + ) + + payload["version"] = "v3" + + return self.generate_token(payload) def create_token_without_jti(self, schema_name, **extra_payload): - payload_vars = self._get_payload_with_params(schema_name, None, **extra_payload) + payload_vars = self._get_payload_with_params( + schema_name=schema_name, schema_url=None, **extra_payload + ) del payload_vars["jti"] return self.generate_token(payload_vars) def create_token_without_case_id(self, schema_name, **extra_payload): - payload_vars = self._get_payload_with_params(schema_name, None, **extra_payload) + payload_vars = self._get_payload_with_params( + schema_name=schema_name, schema_url=None, **extra_payload + ) del payload_vars["case_id"] return self.generate_token(payload_vars) def create_token_without_trad_as(self, schema_name, **extra_payload): - payload_vars = self._get_payload_with_params(schema_name, None, **extra_payload) - del payload_vars["trad_as"] + payload_vars = self._get_payload_with_params( + schema_name=schema_name, schema_url=None, **extra_payload + ) + del payload_vars["survey_metadata"]["data"]["trad_as"] + + return self.generate_token(payload_vars) + + def create_token_v2_social_token_invalid_receipting_key( + self, schema_name, **extra_payload + ): + payload_vars = self._get_payload_with_params( + schema_name=schema_name, payload=PAYLOAD_V2_SOCIAL, **extra_payload + ) + del payload_vars["survey_metadata"]["data"]["qid"] + + return self.generate_token(payload_vars) + + def create_token_with_schema_url(self, schema_url, **extra_payload): + payload_vars = self._get_payload_with_params( + schema_url=schema_url, **extra_payload + ) + + return self.generate_token(payload_vars) + + def create_token_with_cir_instrument_id( + self, cir_instrument_id, payload=None, **extra_payload + ): + payload_vars = self._get_payload_with_params( + cir_instrument_id=cir_instrument_id, + payload=payload or PAYLOAD_V2_BUSINESS, + **extra_payload, + ) return self.generate_token(payload_vars) - def create_token_with_survey_url(self, schema_name, survey_url, **extra_payload): + def create_token_with_none_language_code(self, schema_name, **extra_payload): payload_vars = self._get_payload_with_params( - schema_name, survey_url, **extra_payload + schema_name=schema_name, **extra_payload ) + del payload_vars["language_code"] return self.generate_token(payload_vars) diff --git a/tests/integration/individual_response/test_individual_response.py b/tests/integration/individual_response/test_individual_response.py index 5439f133bc..4bbdc0de23 100644 --- a/tests/integration/individual_response/test_individual_response.py +++ b/tests/integration/individual_response/test_individual_response.py @@ -22,7 +22,11 @@ def setUp(self): self.DUMMY_MOBILE_NUMBER = "07700900258" super().setUp() - self.launchSurvey("test_individual_response", region_code="GB-ENG") + + self.launchSurveyV2( + schema_name="test_individual_response", + region_code="GB-ENG", + ) @property def individual_section_link(self): @@ -154,8 +158,8 @@ def _request_individual_response_by_text(self): class TestIndividualResponseOnHubDisabled(IndividualResponseTestCase): def setUp(self): super().setUp() - self.launchSurvey( - "test_individual_response_on_hub_disabled", region_code="GB-ENG" + self.launchSurveyV2( + schema_name="test_individual_response_on_hub_disabled", region_code="GB-ENG" ) def test_show_on_hub_false(self): diff --git a/tests/integration/integration_test_case.py b/tests/integration/integration_test_case.py index 1c3a92b913..2258d3d668 100644 --- a/tests/integration/integration_test_case.py +++ b/tests/integration/integration_test_case.py @@ -10,7 +10,7 @@ from mock import patch from sdc.crypto.key_store import KeyStore -from app.keys import KEY_PURPOSE_AUTHENTICATION, KEY_PURPOSE_SUBMISSION +from app.keys import KEY_PURPOSE_AUTHENTICATION, KEY_PURPOSE_SDS, KEY_PURPOSE_SUBMISSION from app.setup import create_app from app.utilities.json import json_loads from application import configure_logging @@ -23,6 +23,8 @@ EQ_SUBMISSION_SDX_PRIVATE_KEY = "2225f01580a949801274a5f3e6861947018aff5b" EQ_SUBMISSION_SR_PRIVATE_SIGNING_KEY = "fe425f951a0917d7acdd49230b23a5c405c28510" +EQ_SUPPLEMENTARY_DATA_PRIVATE_KEY = "df88fdad2612ae1e80571120e6c6371f55896696" + KEYS_FOLDER = "./tests/jwt-test-keys" @@ -34,6 +36,41 @@ def get_file_contents(filename, trim=False): return data +KEYS_DICT = { + "keys": { + EQ_USER_AUTHENTICATION_RRM_PRIVATE_KEY_KID: { + "purpose": KEY_PURPOSE_AUTHENTICATION, + "type": "private", + "value": get_file_contents("sdc-rrm-authentication-signing-private-v1.pem"), + }, + SR_USER_AUTHENTICATION_PUBLIC_KEY_KID: { + "purpose": KEY_PURPOSE_AUTHENTICATION, + "type": "public", + "value": get_file_contents( + "sdc-sr-authentication-encryption-public-v1.pem" + ), + }, + EQ_SUBMISSION_SDX_PRIVATE_KEY: { + "purpose": KEY_PURPOSE_SUBMISSION, + "type": "private", + "value": get_file_contents("sdc-sdx-submission-encryption-private-v1.pem"), + }, + EQ_SUBMISSION_SR_PRIVATE_SIGNING_KEY: { + "purpose": KEY_PURPOSE_SUBMISSION, + "type": "public", + "value": get_file_contents("sdc-sr-submission-signing-private-v1.pem"), + }, + EQ_SUPPLEMENTARY_DATA_PRIVATE_KEY: { + "purpose": KEY_PURPOSE_SDS, + "type": "private", + "value": get_file_contents( + "sdc-sds-supplementary_data-encryption-private-v1.pem" + ), + }, + } +} + + class IntegrationTestCase(unittest.TestCase): # pylint: disable=too-many-public-methods def setUp(self): # Cache for requests @@ -42,7 +79,8 @@ def setUp(self): self.last_csrf_token = None self.redirect_url = None self.last_response_headers = None - + self.last_cookie = None + self.key_store = None # Perform setup steps self._set_up_app() @@ -53,63 +91,25 @@ def test_app(self): def _set_up_app(self, setting_overrides=None): self._ds = patch("app.setup.datastore.Client", MockDatastore) self._ds.start() - self._redis = patch("app.setup.redis.Redis", fakeredis.FakeStrictRedis) self._redis.start() - configure_logging() - overrides = { "EQ_ENABLE_HTML_MINIFY": False, "EQ_SUBMISSION_CONFIRMATION_BACKEND": "log", } if setting_overrides: - overrides = overrides | setting_overrides - + overrides |= setting_overrides with patch( "google.auth._default._get_explicit_environ_credentials", return_value=(Mock(), "test-project-id"), ): self._application = create_app(overrides) - - self._key_store = KeyStore( - { - "keys": { - EQ_USER_AUTHENTICATION_RRM_PRIVATE_KEY_KID: { - "purpose": KEY_PURPOSE_AUTHENTICATION, - "type": "private", - "value": get_file_contents( - "sdc-rrm-authentication-signing-private-v1.pem" - ), - }, - SR_USER_AUTHENTICATION_PUBLIC_KEY_KID: { - "purpose": KEY_PURPOSE_AUTHENTICATION, - "type": "public", - "value": get_file_contents( - "sdc-sr-authentication-encryption-public-v1.pem" - ), - }, - EQ_SUBMISSION_SDX_PRIVATE_KEY: { - "purpose": KEY_PURPOSE_SUBMISSION, - "type": "private", - "value": get_file_contents( - "sdc-sdx-submission-encryption-private-v1.pem" - ), - }, - EQ_SUBMISSION_SR_PRIVATE_SIGNING_KEY: { - "purpose": KEY_PURPOSE_SUBMISSION, - "type": "public", - "value": get_file_contents( - "sdc-sr-submission-signing-private-v1.pem" - ), - }, - } - } - ) + self.key_store = KeyStore(KEYS_DICT) self.token_generator = TokenGenerator( - self._key_store, + self.key_store, EQ_USER_AUTHENTICATION_RRM_PRIVATE_KEY_KID, SR_USER_AUTHENTICATION_PUBLIC_KEY_KID, ) @@ -121,37 +121,41 @@ def tearDown(self): self._ds.stop() self._redis.stop() - def launchSurvey(self, schema_name="test_dates", **payload_kwargs): + def launchSupplementaryDataSurvey( + self, schema_name="test_supplementary_data", **payload_kwargs + ): """ Launch a survey as an authenticated user and follow re-directs :param schema_name: The name of the schema to load """ - token = self.token_generator.create_token( + token = self.token_generator.create_supplementary_data_token( schema_name=schema_name, **payload_kwargs ) - self.get("/session?token=" + token) - def dumpAnswers(self): + self.get(f"/session?token={token}") - self.get("/dump/answers") + def launchSurveyV2( + self, theme="default", schema_name="test_dates", **payload_kwargs + ): + """ + Launch a survey as an authenticated user and follow re-directs + :param schema_name: The name of the schema to load + """ + token = self.token_generator.create_token_v2( + theme=theme, schema_name=schema_name, **payload_kwargs + ) - # Then I get a 200 OK response - self.assertStatusOK() + self.get(f"/session?token={token}") - # And the JSON response contains the data I submitted - dump_answers = json_loads(self.getResponseData()) - return dump_answers + def dumpAnswers(self): + self.get("/dump/answers") + self.assertStatusOK() + return json_loads(self.getResponseData()) def dumpSubmission(self): - self.get("/dump/submission") - - # Then I get a 200 OK response self.assertStatusOK() - - # And the JSON response contains the data I submitted - dump_submission = json_loads(self.getResponseData()) - return dump_submission + return json_loads(self.getResponseData()) def dump_debug(self): self.get("/dump/debug") @@ -170,7 +174,9 @@ def get(self, url, follow_redirects=True, **kwargs): :param url: the URL to GET """ response = self._client.get(url, follow_redirects=follow_redirects, **kwargs) - + # As of Flask-Login 0.6.0 the session cookie is only sent when the session is modified + if "Set-Cookie" in response.headers: + self.last_cookie = response.headers["Set-Cookie"] self._cache_response(response) def post(self, post_data=None, url=None, action=None, **kwargs): @@ -250,9 +256,15 @@ def options(self, url, **kwargs): def getLinkById(self, identifier: str): return self.getHtmlSoup().find("a", id=identifier) + def getLinksByAttribute(self, attributes: dict[str, str]): + return [tag["href"] for tag in self.getHtmlSoup().findAll("a", attributes)] + def getSignOutButton(self): return self.getHtmlSoup().find("a", {"data-qa": "btn-save-sign-out"}) + def getSubmitButton(self): + return self.getHtmlSoup().find("button", {"data-qa": "btn-submit"}) + def saveAndSignOut(self): """ Sign out of eQ using the `Save and exit survey` button and do not follow redirects since the redirect is external @@ -311,7 +323,7 @@ def getCookie(self): """ Returns the last received response cookie session """ - cookie = self.last_response.headers["Set-Cookie"] + cookie = self.last_cookie cookie_session = cookie.split("session=.")[1].split(";")[0] decoded_cookie_session = decode_flask_cookie(cookie_session) return json_loads(decoded_cookie_session) @@ -320,7 +332,7 @@ def deleteCookie(self): """ Deletes the test client cookie """ - self._client.delete_cookie("localhost", "session") + self._client.delete_cookie(domain="localhost", key="session") def getHtmlSoup(self): """ @@ -330,13 +342,17 @@ def getHtmlSoup(self): """ return BeautifulSoup(self.getResponseData(), "html.parser") + @staticmethod + def row_selector(row_number): + return f".ons-summary__item:nth-of-type({row_number})" + # Extra Helper Assertions def assertInHead(self, content): self.assertInSelector(content, "head") # Extra Helper Assertions - def assertInBody(self, content): - self.assertInSelector(content, "body") + def assertInBody(self, contents): + self.assertInSelector(contents, "body") # Extra Helper Assertions def assertNotInHead(self, content): @@ -346,12 +362,15 @@ def assertNotInHead(self, content): def assertNotInBody(self, content): self.assertNotInSelector(content, "body") - def assertInSelector(self, content, selector): - data = self.getHtmlSoup().select(selector) - message = f"\n{content} not in \n{data}" + def assertInSelector(self, contents, selector): + contents = contents if isinstance(contents, list) else [contents] - # intentionally not using assertIn to avoid duplicating the output message - self.assertTrue(content in str(data), msg=message) + for content in contents: + data = self.getHtmlSoup().select(selector) + message = f"\n{content} not in \n{data}" + + # intentionally not using assertIn to avoid duplicating the output message + self.assertTrue(content in str(data), msg=message) def assertAnswerInSummary(self, answer, *, answer_id): # Get answer using data qa @@ -383,13 +402,11 @@ def assertNotInSelector(self, content, selector): self.assertFalse(content in str(data), msg=message) def assertNotInPage(self, content, message=None): - self.assertNotIn( member=str(content), container=self.getResponseData(), msg=str(message) ) def assertRegexPage(self, regex, message=None): - self.assertRegex( text=self.getResponseData(), expected_regex=str(regex), msg=str(message) ) @@ -413,6 +430,9 @@ def assertStatusNotFound(self): self.assertStatusCode(404) self.assertInBody("Page not found") + def assertException(self): + self.assertStatusCode(500) + def assertStatusCode(self, status_code): if self.last_response is not None: self.assertEqual(status_code, self.last_response.status_code) diff --git a/tests/integration/introduction/test_introduction.py b/tests/integration/introduction/test_introduction.py index 990e635cca..a1d668b9d2 100644 --- a/tests/integration/introduction/test_introduction.py +++ b/tests/integration/introduction/test_introduction.py @@ -6,15 +6,15 @@ class TestIntroduction(IntegrationTestCase): def test_mail_link_contains_ru_ref_in_subject(self): # Given a business survey - self.launchSurvey("test_introduction") + self.launchSurveyV2(schema_name="test_introduction") # When on the introduction page # Then the email link is present with the ru_ref in the subject - self.assertRegexPage(r'"mailto\:.+\?subject\=.+123456789012A"') + self.assertRegexPage(r'"mailto\:.+\?subject\=.+12345678901A"') def test_intro_description_displayed(self): # Given survey containing intro description - self.launchSurvey("test_introduction") + self.launchSurveyV2(schema_name="test_introduction") # When on the introduction page # Then description should be displayed @@ -24,7 +24,7 @@ def test_intro_description_displayed(self): def test_intro_description_not_displayed(self): # Given survey without introduction description - self.launchSurvey("test_textfield") + self.launchSurveyV2(schema_name="test_textfield") # When on the introduction page # Then description should not be displayed @@ -32,7 +32,7 @@ def test_intro_description_not_displayed(self): def test_intro_basis_for_completion_displayed(self): # Given survey with basis for completion - self.launchSurvey("test_introduction") + self.launchSurveyV2(schema_name="test_introduction") # When on the introduction page # Then basis for completion should be displayed @@ -40,7 +40,7 @@ def test_intro_basis_for_completion_displayed(self): def test_intro_basis_for_completion_not_displayed(self): # Given survey without basis for completion - self.launchSurvey("test_introduction") + self.launchSurveyV2(schema_name="test_introduction") # When on the introduction page # Then basis for completion should not be displayed @@ -48,7 +48,7 @@ def test_intro_basis_for_completion_not_displayed(self): def test_start_survey_sets_started_at(self): # Given survey with a start survey button - self.launchSurvey("test_introduction", roles=["dumper"]) + self.launchSurveyV2(schema_name="test_introduction", roles=["dumper"]) self.post(action="start_questionnaire") @@ -64,7 +64,7 @@ def test_start_survey_sets_started_at(self): def test_legal_basis_should_be_visible(self): # Given survey with legal_basis for completion - self.launchSurvey("test_introduction") + self.launchSurveyV2(schema_name="test_introduction") # When on the introduction page # Then legal_basis should be displayed @@ -72,15 +72,15 @@ def test_legal_basis_should_be_visible(self): def test_legal_basis_northern_ireland(self): # Given northernireland survey with legal_basis - self.launchSurvey("test_introduction") + self.launchSurveyV2(schema_name="test_introduction") # When on the introduction page # Then legal_basis should be displayed self.assertInBody("Your response is legally required") def test_contact_links(self): - self.launchSurvey("test_introduction") + self.launchSurveyV2(schema_name="test_introduction") self.assertInBody( 'If the company details or structure have changed contact us on 0300 1234 931 ' - 'or email surveys@ons.gov.uk' + 'or email surveys@ons.gov.uk' ) diff --git a/tests/integration/questionnaire/__init__.py b/tests/integration/questionnaire/__init__.py index f655dc2a3f..b5da4c7e6e 100644 --- a/tests/integration/questionnaire/__init__.py +++ b/tests/integration/questionnaire/__init__.py @@ -27,3 +27,9 @@ def get_link(self, action, position): selector = f"[data-qa='list-item-{action}-{position}-link']" selected = self.getHtmlSoup().select(selector) return selected[0].get("href") + + def get_list_item_change_link(self, answer_id, list_item_id): + """change link on calculated summary for list item""" + selector = f"[data-qa='{answer_id}-{list_item_id}-edit']" + selected = self.getHtmlSoup().select(selector) + return selected[0].get("href") diff --git a/tests/integration/questionnaire/test_questionnaire_array_type_definition.py b/tests/integration/questionnaire/test_questionnaire_array_type_definition.py new file mode 100644 index 0000000000..9cb3c9d2d1 --- /dev/null +++ b/tests/integration/questionnaire/test_questionnaire_array_type_definition.py @@ -0,0 +1,28 @@ +from tests.integration.integration_test_case import IntegrationTestCase +from tests.integration.questionnaire import SUBMIT_URL_PATH, THANK_YOU_URL_PATH + + +class TestQuestionnaireQuestionDefinition(IntegrationTestCase): + def test_question_definition(self): + # Given I launch a questionnaire with definitions + self.launchSurveyV2(schema_name="test_question_definition_array_type") + + # When I start the survey I am presented with the definitions title and content correctly + self.assertInBody( + "Do you connect a LiFePO4 battery to your photovoltaic system to store surplus energy?" + ) + + self.assertInBody("What is a photovoltaic system?") + self.assertInBody( + "A typical photovoltaic system employs solar panels, each comprising a number of solar cells, " + "which generate electrical power. PV installations may be ground-mounted, rooftop mounted or wall mounted. " + "The mount may be fixed, or use a solar tracker to follow the sun across the sky." + ) + + # When we continue we go to the summary page + self.post() + self.assertInUrl(SUBMIT_URL_PATH) + + # And Submit my answers + self.post() + self.assertInUrl(THANK_YOU_URL_PATH) diff --git a/tests/integration/questionnaire/test_questionnaire_calculated_summary.py b/tests/integration/questionnaire/test_questionnaire_calculated_summary.py index 0e4ab8faf1..e19f7cb356 100644 --- a/tests/integration/questionnaire/test_questionnaire_calculated_summary.py +++ b/tests/integration/questionnaire/test_questionnaire_calculated_summary.py @@ -1,11 +1,19 @@ -from tests.integration.integration_test_case import IntegrationTestCase +from tests.integration.questionnaire import QuestionnaireTestCase -class TestQuestionnaireCalculatedSummary(IntegrationTestCase): +class TestQuestionnaireCalculatedSummary(QuestionnaireTestCase): BASE_URL = "/questionnaire/" - def test_calculated_summary(self): - self.launchSurvey("test_calculated_summary") + def _add_list_items(self): + self.post() + self.post({"you-live-here": "Yes"}) + self.add_person("Marie Claire", "Doe") + self.post({"anyone-else": "Yes"}) + self.add_person("John", "Doe") + self.post({"anyone-else": "No"}) + self.post() + + def _complete_calculated_summary_path_with_skip(self): self.post({"first-number-answer": "10"}) self.post( { @@ -19,13 +27,8 @@ def test_calculated_summary(self): self.post({"skip-fourth-block-answer": "Yes"}) self.post({"fifth-percent-answer": "50", "fifth-number-answer": "50"}) self.post({"sixth-percent-answer": "60", "sixth-number-answer": "60"}) - self.assertInBody("Skipped Fourth") - self.assertInBody( - "We calculate the total of currency values entered to be ÂŖ80.00" - ) - def test_calculated_summary_no_skip(self): - self.launchSurvey("test_calculated_summary") + def _complete_calculated_summary_path_no_skip(self): self.post({"first-number-answer": "10"}) self.post( { @@ -41,7 +44,440 @@ def test_calculated_summary_no_skip(self): self.post({"fourth-and-a-half-number-answer-also-in-total": "50"}) self.post({"fifth-percent-answer": "50", "fifth-number-answer": "50"}) self.post({"sixth-percent-answer": "60", "sixth-number-answer": "60"}) - self.assertNotInBody("Skipped Fourth") + + def test_calculated_summary(self): + self.launchSurveyV2(schema_name="test_calculated_summary") + self._complete_calculated_summary_path_with_skip() + + self.assertInBody( + "We calculate the total of currency values entered to be ÂŖ80.00" + ) + self.assertEqual( + "Yes, I confirm this is correct", self.getSubmitButton().text.strip() + ) + + def test_calculated_summary_no_skip(self): + self.launchSurveyV2(schema_name="test_calculated_summary") + self._complete_calculated_summary_path_no_skip() self.assertInBody( "We calculate the total of currency values entered to be ÂŖ180.00" ) + + def test_new_calculated_summary(self): + self.launchSurveyV2(schema_name="test_new_calculated_summary") + self._complete_calculated_summary_path_with_skip() + self.assertInBody( + "We calculate the total of currency values entered to be ÂŖ80.00" + ) + + def test_calculated_summary_total_playback(self): + self.launchSurveyV2(schema_name="test_new_calculated_summary") + self._complete_calculated_summary_path_with_skip() + self.post() + self.post() + self.post() + self.post() + self.assertInBody("Total currency values: ÂŖ80.00") + + def test_new_calculated_summary_no_skip(self): + self.launchSurveyV2(schema_name="test_new_calculated_summary") + self._complete_calculated_summary_path_no_skip() + self.assertInBody( + "We calculate the total of currency values entered to be ÂŖ180.00" + ) + + def test_new_calculated_summary_repeating_section(self): + self.launchSurveyV2(schema_name="test_new_calculated_summary_repeating_section") + self._add_list_items() + self.post() + + self._complete_calculated_summary_path_with_skip() + self.assertInBody( + "We calculate the total of currency values entered to be ÂŖ80.00" + ) + + def test_new_calculated_summary_no_skip_repeating_section(self): + self.launchSurveyV2(schema_name="test_new_calculated_summary_repeating_section") + self._add_list_items() + self.post() + + self._complete_calculated_summary_path_no_skip() + self.assertInBody( + "We calculate the total of currency values entered to be ÂŖ180.00" + ) + + def test_calculated_summary_value_sources_across_sections(self): + self.launchSurveyV2( + schema_name="test_calculated_summary_cross_section_dependencies" + ) + + # Complete the first section + self.post() + self.post({"skip-first-block-answer": "No"}) + self.post({"first-number-answer": "10"}) + self.post({"first-and-a-half-number-answer-also-in-total": "20"}) + self.post({"second-number-answer-also-in-total": "30"}) + self.assertInBody( + "We calculate the total of currency values entered to be ÂŖ60.00" + ) + self.post() + self.post() + self.post() + + # Complete the second section + self.post( + { + "third-number-answer": "20", + "third-number-answer-also-in-total": "20", + } + ) + self.assertInBody( + "We calculate the total of currency values entered to be ÂŖ40.00" + ) + + # Check calculated summary value sources are displayed correctly for both the current and previous + # sections + self.post() + self.assertInBody("60 - calculated summary answer (previous section)") + self.assertInBody("40 - calculated summary answer (current section)") + self.post() + + self.assertInBody( + "Set minimum and maximum values based on your calculated summary total of ÂŖ60" + ) + self.post( + { + "set-minimum-answer": "40", + "set-maximum-answer": "70", + } + ) + self.assertInBody("Enter an answer more than or equal to ÂŖ60.00") + + def test_calculated_summary_value_sources_across_sections_repeating(self): + self.launchSurveyV2( + schema_name="test_new_calculated_summary_cross_section_dependencies_repeating" + ) + + # Add household members + self._add_list_items() + + # Complete the first section + self.post({"skip-first-block-answer": "No"}) + self.post({"first-number-answer": "10"}) + self.post({"first-and-a-half-number-answer-also-in-total": "20"}) + self.post({"second-number-answer-also-in-total": "30"}) + self.assertInBody( + "We calculate the total of currency values entered to be ÂŖ60.00" + ) + self.post() + self.post() + self.post() + + # Complete the second section + self.post( + { + "third-number-answer": "20", + "third-number-answer-also-in-total": "20", + } + ) + self.assertInBody( + "We calculate the total of currency values entered to be ÂŖ40.00" + ) + + # Check calculated summary value sources are displayed correctly for both the current and previous + # sections + self.post() + self.assertInBody("60 - calculated summary answer (previous section)") + self.assertInBody("40 - calculated summary answer (current section)") + self.post() + + self.assertInBody( + "Set minimum and maximum values based on your calculated summary total of ÂŖ60" + ) + self.post( + { + "set-minimum-answer": "40", + "set-maximum-answer": "70", + } + ) + self.assertInBody("Enter an answer more than or equal to ÂŖ60.00") + + def test_calculated_summary_repeating_answers_only(self): + """ + Tests a calculated summary with a dynamic answer source resolving to a list of repeating answers + """ + self.launchSurveyV2( + schema_name="test_new_calculated_summary_repeating_answers_only" + ) + + self.post({"any-transport-answer": "Yes"}) + self.post({"transport-name": "Bus"}) + self.post({"list-collector-answer": "Yes"}) + self.post({"transport-name": "Tube"}) + + # get the ids before finishing the collector + list_item_ids = self.get_list_item_ids() + assert len(list_item_ids) == 2 + self.post({"list-collector-answer": "No"}) + + self.post( + { + f"cost-of-transport-{list_item_ids[0]}": "100", + f"cost-of-transport-{list_item_ids[1]}": "200", + } + ) + self.assertInBody( + "We calculate the total monthly spending on public transport to be ÂŖ300.00. Is this correct?" + ) + + def test_new_calculated_summary_repeating_blocks(self): + """ + Tests a calculated summary with a repeating block answer id source resolving to a list of answers + """ + self.launchSurveyV2(schema_name="test_new_calculated_summary_repeating_blocks") + self.post({"answer-car": "100"}) + self.post({"answer-skip": "No"}) + self.post({"list-collector-answer": "Yes"}) + self.post({"transport-name": "Bus"}) + self.post( + { + "transport-company": "First", + "transport-cost": "30", + "transport-additional-cost": "5", + } + ) + self.post({"transport-count": "10"}) + self.post({"list-collector-answer": "Yes"}) + self.post({"transport-name": "Plane"}) + self.post( + { + "transport-company": "EasyJet", + "transport-cost": "0", + "transport-additional-cost": "265", + } + ) + self.post({"transport-count": "2"}) + list_item_ids = self.get_list_item_ids() + self.post({"list-collector-answer": "No"}) + self.assertInBody( + "We calculate the total monthly expenditure on transport to be ÂŖ400.00. Is this correct?" + ) + self.post() + self.assertInBody( + "We calculate the total journeys made per month to be 12. Is this correct?" + ) + + # check that using a change link and editing an answer takes you straight back to the relevant calculated summary + change_link = self.get_list_item_change_link( + "transport-count", list_item_ids[1] + ) + self.get(change_link) + self.post({"transport-count": "4"}) + self.assertInUrl("/calculated-summary-count/") + self.assertInBody( + "We calculate the total journeys made per month to be 14. Is this correct?" + ) + self.previous() + + # likewise for the other calculated summary + change_link = self.get_list_item_change_link("transport-cost", list_item_ids[0]) + self.get(change_link) + self.post( + { + "transport-company": "First", + "transport-cost": "300", + "transport-additional-cost": "50", + } + ) + self.assertInUrl("/calculated-summary-spending/") + self.assertInBody( + "We calculate the total monthly expenditure on transport to be ÂŖ715.00. Is this correct?" + ) + + # check that removing the list collector from the path updates the calculated summary correctly + self.previous() + self.previous() + self.post({"answer-skip": "Yes"}) + + # calculated summary count should now not be on the path + self.assertInUrl("/calculated-summary-spending/") + self.assertInBody( + "We calculate the total monthly expenditure on transport to be ÂŖ100.00. Is this correct?" + ) + # no list items should be there + self.assertNotInBody("Give details of your expenditure travelling by") + self.post() + # should be absent from section summary too + self.assertInUrl("/sections/section-1") + self.assertNotInBody("Name of transport") + + def test_calculated_summary_default_decimal_places(self): + """ + When multiple decimal limits are set in the schema but no decimals + are entered then we should default to two decimal places on the calculated summary page + and the playback page + """ + self.launchSurveyV2( + schema_name="test_calculated_and_grand_calculated_summary_decimals" + ) + self.post({"first-number-answer": "10"}) + self.post( + { + "second-number-answer": "20", + "second-number-answer-also-in-total": "20", + } + ) + self.post({"third-number-answer": "30"}) + self.post({"fourth-number-answer": "40"}) + self.assertInBody( + "We calculate the total of currency values entered to be ÂŖ120.00" + ) + self.post() + self.assertInBody("Total currency values: ÂŖ120.00") + + def test_calculated_summary_with_varying_decimal_places(self): + """ + When multiple decimal limits are set in the schema and a mixture of decimal + places are entered then we should use the largest number of decimal places that are below the decimal limit + on the calculated summary page and the playback page + """ + self.launchSurveyV2( + schema_name="test_calculated_and_grand_calculated_summary_decimals" + ) + self.post({"first-number-answer": "10.1"}) + self.post( + { + "second-number-answer": "20.12", + "second-number-answer-also-in-total": "20.123", + } + ) + self.post({"third-number-answer": "30.1234"}) + self.post({"fourth-number-answer": "40.12345"}) + self.assertInBody( + "We calculate the total of currency values entered to be ÂŖ120.58985" + ) + self.post() + self.assertInBody("Total currency values: ÂŖ120.58985") + + def test_placeholder_rendering_in_calculated_summary_label(self): + """ + Tests that a placeholder using the first_non_empty_item is rendered correctly on the calculated summary page + using the answer values that are on the path. In this instance it is the happy path where the user has entered + their own reporting dates which should be reflected on the calcualted summary label. + """ + self.launchSurveyV2( + schema_name="test_placeholder_dependencies_with_calculation_summaries" + ) + + self.post( + {"reporting-date-answer": "No, I need to report for a different period"} + ) + self.post( + { + "date-from-day": "1", + "date-from-month": "1", + "date-from-year": "2000", + "date-to-day": "1", + "date-to-month": "4", + "date-to-year": "2000", + } + ) + self.post({"undertake-rnd-answer": "Yes"}) + self.post() + self.post({"civil-research": "10", "defence": "10"}) + self.assertInUrl("/questionnaire/calc-summary-1/") + self.assertInBody( + "For the period 1 January 2000 to 1 April 2000 what was the expenditure on R&D for Integration Testing?" + ) + + def test_placeholders_rendering_in_calculated_summary_unhappy_path(self): + """ + Tests that a placeholder using the first_non_empty_item is rendered correctly on the calculated summary page + using the answer values that are on the path. In this instance it is the unhappy path where the user has entered + their own reporting dates, but has then gone back to the first section and changed their answer. In this instance + the dates displayed in the label should come from metadata rather than the dates entered by the user (which are no longer on the path) + """ + self.launchSurveyV2( + schema_name="test_placeholder_dependencies_with_calculation_summaries" + ) + + # Happy path journey + self.post( + {"reporting-date-answer": "No, I need to report for a different period"} + ) + self.post( + { + "date-from-day": "1", + "date-from-month": "1", + "date-from-year": "2000", + "date-to-day": "1", + "date-to-month": "4", + "date-to-year": "2000", + } + ) + self.post({"undertake-rnd-answer": "Yes"}) + self.post() + self.post({"civil-research": "10", "defence": "10"}) + self.assertInUrl("/questionnaire/calc-summary-1/") + self.assertInBody( + "For the period 1 January 2000 to 1 April 2000 what was the expenditure on R&D for Integration Testing?" + ) + self.assertInBody( + "We have calculated your total in-house expenditure on R&D for Integration Testing for the period 1 January 2000 to 1 April 2000 to be ÂŖ20. " + "Is this correct?" + ) + + # Complete the rest of the survey + self.post() + self.post({"innovation": "10", "software": "10"}) + self.post() + self.post() + + # Go back and change the answer and get back to the Calculated Summary page + self.get("/questionnaire/sections/reporting-period-section/") + self.get("/questionnaire/reporting-date/") + self.post({"reporting-date-answer": "Yes, I can report for this period"}) + self.get("/questionnaire/sections/questions-section/") + self.get("/questionnaire/how-much-rnd/") + self.post({"civil-research": "10", "defence": "100"}) + + # The placeholder dates should now be taken from metadata + self.assertInUrl("/questionnaire/calc-summary-1/") + self.assertInBody( + "For the period 1 April 2016 to 30 April 2016 what was the expenditure on R&D for Integration Testing?" + ) + self.assertInBody( + "We have calculated your total in-house expenditure on R&D for Integration Testing for the period 1 April 2016 to 30 April 2016 to be ÂŖ110. " + "Is this correct?" + ) + + def test_calculated_summary_repeating_sections_complete_after_adding_list_item( + self, + ): + self.launchSurveyV2(schema_name="test_calculated_summary_dependent_questions") + + self.post({"answer-1": "100"}) + self.post({"answer-2": "100"}) + self.post({"answer-3": "100"}) + self.post({"answer-4": "100"}) + self.post() + self.post({"additional-sites-answer": "Yes"}) + self.post({"business-name": "Ebay"}) + self.post({"any-other-additional-sites-answer": "No"}) + self.post() + self.assertInUrl("/number-of-employees-working-at-this-additional-site") + self.post( + {"number-full-time-employees": "1", "number-part-time-employees": "1"} + ) + self.post() + self.get("/questionnaire/sections/list-collector-section/") + self.post({"any-other-additional-sites-answer": "No"}) + self.assertInBody("Completed") + self.get( + "/questionnaire/additional_sites_name/add-block-business-name-trading-style-and-address-for-this-additional-site/" + ) + self.post({"business-name": "Amazon"}) + self.post({"any-other-additional-sites-answer": "No"}) + self.assertInUrl("/questionnaire") + self.assertInBody("Completed") + self.assertNotInBody("Partially completed") diff --git a/tests/integration/questionnaire/test_questionnaire_change_answer.py b/tests/integration/questionnaire/test_questionnaire_change_answer.py index 9ae29b40d8..3d65a4a9eb 100644 --- a/tests/integration/questionnaire/test_questionnaire_change_answer.py +++ b/tests/integration/questionnaire/test_questionnaire_change_answer.py @@ -4,7 +4,7 @@ class TestQuestionnaireChangeAnswer(IntegrationTestCase): def test_change_non_mandatory_date_from_answered_to_not_answered(self): # Given the test_dates questionnaire with a non-mandatory date answered. - self.launchSurvey("test_dates") + self.launchSurveyV2(schema_name="test_dates") post_data = [ { diff --git a/tests/integration/questionnaire/test_questionnaire_csrf.py b/tests/integration/questionnaire/test_questionnaire_csrf.py index 75e9a46f62..835c8e105c 100644 --- a/tests/integration/questionnaire/test_questionnaire_csrf.py +++ b/tests/integration/questionnaire/test_questionnaire_csrf.py @@ -7,7 +7,7 @@ def test_given_on_interstitial_page_when_submit_with_no_csrf_token_then_forbidde self, ): # Given - self.launchSurvey("test_interstitial_page") + self.launchSurveyV2(schema_name="test_interstitial_page") self.last_csrf_token = None # When @@ -21,7 +21,7 @@ def test_given_on_interstitial_page_when_submit_with_invalid_csrf_token_then_for self, ): # Given - self.launchSurvey("test_interstitial_page") + self.launchSurveyV2(schema_name="test_interstitial_page") self.last_csrf_token = "made-up-token" # When @@ -35,7 +35,7 @@ def test_given_on_introduction_page_when_submit_valid_token_then_redirect_to_nex self, ): # Given - self.launchSurvey("test_interstitial_page") + self.launchSurveyV2(schema_name="test_interstitial_page") # When self.post(action="start_questionnaire") @@ -48,7 +48,7 @@ def test_given_answered_question_when_change_answer_with_invalid_csrf_token_then self, ): # Given - self.launchSurvey("test_interstitial_page", roles=["dumper"]) + self.launchSurveyV2(schema_name="test_interstitial_page", roles=["dumper"]) self.post() self.post({"favourite-breakfast": "Muesli"}) @@ -66,7 +66,7 @@ def test_given_valid_answer_when_answer_with_invalid_csrf_token_then_answer_not_ self, ): # Given - self.launchSurvey("test_checkbox", roles=["dumper"]) + self.launchSurveyV2(schema_name="test_checkbox", roles=["dumper"]) self.post( { "mandatory-checkbox-answer": "Other", @@ -86,7 +86,7 @@ def test_given_valid_answer_when_answer_with_invalid_csrf_token_then_answer_not_ def test_given_csrf_attack_when_refresh_then_on_question(self): # Given - self.launchSurvey("test_interstitial_page", roles=["dumper"]) + self.launchSurveyV2(schema_name="test_interstitial_page", roles=["dumper"]) self.post() self.last_csrf_token = "made-up-token" self.post({"favourite-breakfast": "Pancakes"}) @@ -102,7 +102,7 @@ def test_given_csrf_attack_when_refresh_then_on_question(self): def test_given_csrf_attack_when_submit_new_answers_then_answers_saved(self): # Given - self.launchSurvey("test_interstitial_page", roles=["dumper"]) + self.launchSurveyV2(schema_name="test_interstitial_page", roles=["dumper"]) self.post() self.last_csrf_token = "made-up-token" self.post({"favourite-breakfast": "Muesli"}) diff --git a/tests/integration/questionnaire/test_questionnaire_custom_page_titles.py b/tests/integration/questionnaire/test_questionnaire_custom_page_titles.py index 54cc9292f9..bcaec567b0 100644 --- a/tests/integration/questionnaire/test_questionnaire_custom_page_titles.py +++ b/tests/integration/questionnaire/test_questionnaire_custom_page_titles.py @@ -1,9 +1,9 @@ -from . import QuestionnaireTestCase +from tests.integration.questionnaire import QuestionnaireTestCase class TestQuestionnaireCustomPageTitles(QuestionnaireTestCase): def test_custom_page_titles(self): - self.launchSurvey("test_custom_page_titles") + self.launchSurveyV2(schema_name="test_custom_page_titles") self.post() self.assertEqualPageTitle("Custom page title - Test Custom Page Titles") @@ -38,7 +38,7 @@ def test_custom_page_titles(self): ) def test_custom_repeating_page_titles(self): - self.launchSurvey("test_custom_page_titles") + self.launchSurveyV2(schema_name="test_custom_page_titles") self.post() self.post({"anyone-else": "Yes"}) self.post({"first-name": "Marie", "last-name": "Doe"}) diff --git a/tests/integration/questionnaire/test_questionnaire_detail_answer.py b/tests/integration/questionnaire/test_questionnaire_detail_answer.py index 42960ffa57..ac94932a55 100644 --- a/tests/integration/questionnaire/test_questionnaire_detail_answer.py +++ b/tests/integration/questionnaire/test_questionnaire_detail_answer.py @@ -5,7 +5,7 @@ class TestQuestionnaireDetailAnswer(IntegrationTestCase): BASE_URL = "/questionnaire/" def test_detail_answer(self): - self.launchSurvey("test_checkbox_detail_answer_multiple") + self.launchSurveyV2(schema_name="test_checkbox_detail_answer_multiple") self.post( { "mandatory-checkbox-answer": ["Ham", "Pineapple", "Your choice"], diff --git a/tests/integration/questionnaire/test_questionnaire_dynamic_answer_options.py b/tests/integration/questionnaire/test_questionnaire_dynamic_answer_options.py index 61f7e53e3d..f98a145af2 100644 --- a/tests/integration/questionnaire/test_questionnaire_dynamic_answer_options.py +++ b/tests/integration/questionnaire/test_questionnaire_dynamic_answer_options.py @@ -30,7 +30,7 @@ def complete_reference_date_question(self): def assert_dynamic_answer_options(self, schema_name): # Given I launch a schema with dynamic options with additional static option - self.launchSurvey(schema_name) + self.launchSurveyV2(schema_name=schema_name) # When I answer the questions using the dynamic options self.complete_reference_date_question() @@ -65,7 +65,7 @@ def assert_dynamic_answer_options(self, schema_name): def assert_dynamic_answer_options_no_answer_provided(self, schema_name): # Given I launch a schema with dynamic options with additional static option - self.launchSurvey(schema_name, roles=["dumper"]) + self.launchSurveyV2(schema_name=schema_name, roles=["dumper"]) # When I Save and continue without answering any questions self.complete_reference_date_question() @@ -128,8 +128,8 @@ def test_dynamic_answer_options_no_answer_provided(self): def test_static_answer_options(self): # Given I launch a schema with dynamic options with additional static option - self.launchSurvey( - "test_dynamic_answer_options_function_driven_with_static_options" + self.launchSurveyV2( + schema_name="test_dynamic_answer_options_function_driven_with_static_options" ) # When I answer the questions using the static options @@ -165,8 +165,8 @@ def test_static_answer_options(self): def test_dynamic_options_answer_cleared_on_dependency_change(self): # Given I launch a schema and submit an answer for a question which has dynamic options - self.launchSurvey( - "test_dynamic_answer_options_function_driven_with_static_options" + self.launchSurveyV2( + schema_name="test_dynamic_answer_options_function_driven_with_static_options" ) self.complete_reference_date_question() self.answer_checkbox_question(["2020-12-29", "2020-12-30"]) diff --git a/tests/integration/questionnaire/test_questionnaire_endpoints.py b/tests/integration/questionnaire/test_questionnaire_endpoints.py index 8ebd85efd3..e80dbdfefc 100644 --- a/tests/integration/questionnaire/test_questionnaire_endpoints.py +++ b/tests/integration/questionnaire/test_questionnaire_endpoints.py @@ -7,7 +7,7 @@ class TestQuestionnaireEndpoints(IntegrationTestCase): def test_invalid_section_id_raises_404(self): # Given - self.launchSurvey("test_hub_and_spoke") + self.launchSurveyV2(schema_name="test_hub_and_spoke") # When I navigate to the url for a section that does not exist self.get(f"{self.BASE_URL}/sections/invalid-section/") @@ -17,7 +17,7 @@ def test_invalid_section_id_raises_404(self): def test_get_invalid_questionnaire_location_raises_404(self): # Given - self.launchSurvey("test_introduction") + self.launchSurveyV2(schema_name="test_introduction") # When self.get(f"{self.BASE_URL}/test") @@ -27,7 +27,7 @@ def test_get_invalid_questionnaire_location_raises_404(self): def test_post_invalid_questionnaire_location_raises_404(self): # Given - self.launchSurvey("test_introduction") + self.launchSurveyV2(schema_name="test_introduction") # When self.post(url=f"{self.BASE_URL}/test") @@ -39,7 +39,7 @@ def test_post_on_questionnaire_route_without_hub_redirects_to_first_incomplete_l self, ): # Given - self.launchSurvey("test_textfield") + self.launchSurveyV2(schema_name="test_textfield") # When self.post(url="/questionnaire/") @@ -49,7 +49,7 @@ def test_post_on_questionnaire_route_without_hub_redirects_to_first_incomplete_l def test_get_thank_you_data_not_deleted_when_questionnaire_is_not_complete(self): # Given we start a survey - self.launchSurvey("test_percentage", roles=["dumper"]) + self.launchSurveyV2(schema_name="test_percentage", roles=["dumper"]) self.post({"answer": "99"}) # When we request the thank you page (without submitting the survey) @@ -62,7 +62,7 @@ def test_get_thank_you_data_not_deleted_when_questionnaire_is_not_complete(self) def test_get_thank_you_raises_404_when_questionnaire_is_not_complete(self): # Given we start a survey - self.launchSurvey("test_percentage", roles=["dumper"]) + self.launchSurveyV2(schema_name="test_percentage", roles=["dumper"]) # When we request the thank you page (without submitting the survey) self.get("submitted/thank-you") @@ -72,7 +72,7 @@ def test_get_thank_you_raises_404_when_questionnaire_is_not_complete(self): def test_when_on_thank_you_get_thank_you_returns_thank_you(self): # Given we complete the test_percentage survey and are on the thank you page - self.launchSurvey("test_percentage", roles=["dumper"]) + self.launchSurveyV2(schema_name="test_percentage", roles=["dumper"]) self.post({"answer": "99"}) self.post() diff --git a/tests/integration/questionnaire/test_questionnaire_grand_calculated_summary.py b/tests/integration/questionnaire/test_questionnaire_grand_calculated_summary.py new file mode 100644 index 0000000000..979ff3baa6 --- /dev/null +++ b/tests/integration/questionnaire/test_questionnaire_grand_calculated_summary.py @@ -0,0 +1,294 @@ +from tests.integration.questionnaire import QuestionnaireTestCase + + +class TestQuestionnaireGrandCalculatedSummary(QuestionnaireTestCase): + BASE_URL = "/questionnaire/" + + def test_grand_calculated_summary(self): + self.launchSurveyV2(schema_name="test_grand_calculated_summary") + # section-1 two types of unit questions + self.post({"q1-a1": 20, "q1-a2": 5}) + self.post({"q2-a1": 100, "q2-a2": 3}) + self.post() + self.post() + # section-2 two more of each question type + self.post({"q3-a1": 40, "q3-a2": 2}) + self.post({"q4-a1": 10, "q4-a2": 3}) + self.post() + self.post() + # check the two grand calculated summaries + self.assertInBody( + "We calculate the grand total weekly distance travelled to be 170 mi. Is this correct?" + ) + self.post() + self.assertInBody( + "We calculate the grand total journeys per week to be 13. Is this correct?" + ) + # check the submit button text + self.assertEqual( + "Yes, I confirm this is correct", self.getSubmitButton().text.strip() + ) + + def test_grand_calculated_summary_multiple_sections(self): + """ + Use the repeating answers schema to test the grand calculated summary which uses calculated summaries in multiple different sections + """ + self.launchSurveyV2( + schema_name="test_grand_calculated_summary_repeating_answers" + ) + # section 1 + self.post() + self.post({"q1-a1": 10, "q1-a2": 20}) + self.post({"q2-a1": 30, "q2-a2": 40}) + self.post() + self.post({"q3-a1": 50, "q3-a2": 60}) + self.post() + # confirm calculated and grand calculated summary + self.assertInBody( + "Calculated summary for food and clothing is calculated to be ÂŖ210.00. Is this correct?" + ) + self.post() + self.assertInBody( + "Grand Calculated Summary which should match the previous calculated summary is calculated to be ÂŖ210.00. Is this correct?" + ) + self.post() + self.post() + # section 2 + self.post() + self.post({"q4-a1": 100, "q4-a2": 200}) + self.post() + # grand calculated summary section with calculated summaries from multiple sections + self.post() + self.assertInBody( + "Grand Calculated Summary for shopping and entertainment is calculated to be ÂŖ510.00. Is this correct?" + ) + + def _complete_upto_grand_calculated_summary_cross_section_dependencies(self): + """ + Completes first two sections of the schema testing grand calculated summaries + depending on calculated summaries in other sections + """ + # Complete the first section + self.post() + self.post({"skip-answer-1": "Yes"}) + self.post({"second-number-answer-a": "30", "second-number-answer-b": "60"}) + self.assertInBody( + "We calculate your total monthly expenditure on household bills to be ÂŖ90.00. Is this correct?" + ) + self.post() + self.post() + # Complete the second section + self.post() + self.post({"third-number-answer-part-a": "70"}) + + def test_grand_calculated_summary_cross_section_dependencies_with_skip(self): + self.launchSurveyV2( + schema_name="test_grand_calculated_summary_cross_section_dependencies" + ) + self._complete_upto_grand_calculated_summary_cross_section_dependencies() + + # skip the calculated summary + self.post({"skip-answer-2": "Yes"}) + self.post({"tv-choice-answer": "Television"}) + self.post() + + # grand calculated summary which doesn't include skipped calculated summary + self.post() + self.assertInBody( + "The grand calculated summary is calculated to be ÂŖ90.00. Is this correct?" + ) + + def test_grand_calculated_summary_cross_section_dependencies_no_skip(self): + self.launchSurveyV2( + schema_name="test_grand_calculated_summary_cross_section_dependencies" + ) + self._complete_upto_grand_calculated_summary_cross_section_dependencies() + + # don't skip calculated summary, confirm it, and go to section summary + self.post({"skip-answer-2": "No"}) + self.post() + self.post({"tv-choice-answer": "Television"}) + self.post() + + # grand calculated summary will now include the previous calculated summary + self.post() + self.assertInBody( + "The grand calculated summary is calculated to be ÂŖ160.00. Is this correct?" + ) + + def test_grand_calculated_summary_cross_section_dependencies_extra_question(self): + self.launchSurveyV2( + schema_name="test_grand_calculated_summary_cross_section_dependencies" + ) + self._complete_upto_grand_calculated_summary_cross_section_dependencies() + + # edit question to unlock the extra one + self.previous() + self.post( + {"third-number-answer-part-a": "70", "third-number-answer-part-b": "20"} + ) + self.post({"fourth-number-answer": "40"}) + self.post({"skip-answer-2": "No"}) + self.post() + self.post({"tv-choice-answer": "Television"}) + self.post() + + # grand calculated summary will now include the extra question answer + self.post() + self.assertInBody( + "The grand calculated summary is calculated to be ÂŖ220.00. Is this correct?" + ) + + def _complete_upto_grand_calculated_summary_overlapping_answers( + self, radio_answer: str + ): + self.post() + self.post() + self.post({"q1-a1": "100", "q1-a2": "200"}) + self.post({"q2-a1": "10", "q2-a2": "20"}) + self.post() + self.post() + self.post({"radio-extra": radio_answer}) + if radio_answer != "No": + # in the no overlap case, the calculated summary is skipped entirely + self.post() + self.post() + self.post() + + def test_grand_calculated_summary_overlapping_answers_full_overlap(self): + self.launchSurveyV2( + schema_name="test_grand_calculated_summary_overlapping_answers" + ) + self._complete_upto_grand_calculated_summary_overlapping_answers( + "Yes, I am going to buy two of everything" + ) + self.assertInBody( + "Grand Calculated Summary of purchases this week comes to ÂŖ660.00. Is this correct?" + ) + + def test_grand_calculated_summary_overlapping_answers_partial_overlap(self): + self.launchSurveyV2( + schema_name="test_grand_calculated_summary_overlapping_answers" + ) + self._complete_upto_grand_calculated_summary_overlapping_answers( + "Yes, extra bread and cheese" + ) + self.assertInBody( + "Grand Calculated Summary of purchases this week comes to ÂŖ360.00. Is this correct?" + ) + + def test_grand_calculated_summary_overlapping_answers_no_overlap(self): + self.launchSurveyV2( + schema_name="test_grand_calculated_summary_overlapping_answers" + ) + self._complete_upto_grand_calculated_summary_overlapping_answers("No") + self.assertInBody( + "Grand Calculated Summary of purchases this week comes to ÂŖ330.00. Is this correct?" + ) + + def test_grand_calculated_summary_default_decimal_places(self): + """ + When multiple decimal limits are set in the schema but no decimals + are entered then we should default to two decimal places on the grand calculated summary page + """ + self.launchSurveyV2( + schema_name="test_calculated_and_grand_calculated_summary_decimals" + ) + self.post({"first-number-answer": "10"}) + self.post( + { + "second-number-answer": "20", + "second-number-answer-also-in-total": "20", + } + ) + self.post({"third-number-answer": "30"}) + self.post({"fourth-number-answer": "40"}) + self.post() + self.post() + self.post({"fifth-number-answer": "50"}) + self.post({"sixth-number-answer": "60"}) + self.assertInBody( + "We calculate the total of currency values entered to be ÂŖ110.00. Is this correct?" + ) + + def test_grand_calculated_summary_with_varying_decimal_places(self): + """ + When multiple decimal limits are set in the schema and a mixture of decimal + places are entered then we should use the largest number of decimal places that are below the decimal limit + on the grand calculated summary page + """ + self.launchSurveyV2( + schema_name="test_calculated_and_grand_calculated_summary_decimals" + ) + self.post({"first-number-answer": "10.1"}) + self.post( + { + "second-number-answer": "20.12", + "second-number-answer-also-in-total": "20.123", + } + ) + self.post({"third-number-answer": "30.1234"}) + self.post({"fourth-number-answer": "40.12345"}) + self.post() + self.post() + self.post({"fifth-number-answer": "50"}) + self.post({"sixth-number-answer": "60"}) + self.post() + self.assertInBody( + "We calculate the grand total to be ÂŖ230.58985. Is this correct?" + ) + + def test_grand_calculated_summary_inside_repeating_section(self): + """ + Happy path for a grand calculated summary inside a repeating section + """ + self.launchSurveyV2( + schema_name="test_grand_calculated_summary_inside_repeating_section" + ) + self.post() + self.post({"any-cost-answer": "No"}) + self.post({"finance-cost-answer": "150"}) + self.post() + self.post({"base-credit": "20", "base-debit": "30"}) + self.post() + self.post() + self.post({"any-vehicle-answer": "Yes"}) + self.post({"vehicle-name": "Car"}) + self.post({"list-collector-answer": "Yes"}) + self.post({"vehicle-name": "Motorbike"}) + self.post({"list-collector-answer": "No"}) + self.post() + self.post() + self.post({"vehicle-maintenance-cost": "100"}) + self.post({"vehicle-fuel-cost": "80"}) + self.assertInBody( + "We calculate the monthly running costs of your Car to be ÂŖ180.00. Is this correct?" + ) + self.post() + self.assertInBody( + "The total cost of owning and running your Car is calculated to be ÂŖ330.00. Is this correct?" + ) + self.post() + self.post({"pay-debit": "110", "pay-credit": "120", "pay-other": "100"}) + self.assertInBody("Monthly maintenance cost: ÂŖ100.00") + self.assertInBody("Monthly fuel cost: ÂŖ80.00") + self.assertInBody("Total base cost: ÂŖ150.00") + self.assertInBody("Total running cost: ÂŖ180.00") + self.assertInBody("Total owning and running cost: ÂŖ330.00") + self.assertInBody("Paid by debit card: ÂŖ110.00") + self.assertInBody("Paid by credit card: ÂŖ120.00") + self.assertInBody("Paid by other means: ÂŖ100.00") + self.post() + self.post() + self.post() + self.post({"vehicle-maintenance-cost": "40"}) + self.post({"vehicle-fuel-cost": "35"}) + self.assertInBody( + "We calculate the monthly running costs of your Motorbike to be ÂŖ75.00. Is this correct?" + ) + self.post() + self.assertInBody( + "The total cost of owning and running your Motorbike is calculated to be ÂŖ225.00. Is this correct?" + ) + self.post() + self.post({"pay-debit": "25", "pay-credit": "120", "pay-other": "80"}) diff --git a/tests/integration/questionnaire/test_questionnaire_html_escaping.py b/tests/integration/questionnaire/test_questionnaire_html_escaping.py index f893cc6af1..dc8b241fe1 100644 --- a/tests/integration/questionnaire/test_questionnaire_html_escaping.py +++ b/tests/integration/questionnaire/test_questionnaire_html_escaping.py @@ -7,7 +7,7 @@ class TestQuestionnaireHtmlEscaping(IntegrationTestCase): def test_quotes_in_textfield(self): - self.launchSurvey("test_textfield") + self.launchSurveyV2(schema_name="test_textfield") self.post({"name-answer": HTML_CONTENT}) self.get("/questionnaire/name-block") @@ -15,7 +15,7 @@ def test_quotes_in_textfield(self): assert ESCAPED_CONTENT in self.getResponseData() def test_quotes_in_textarea(self): - self.launchSurvey("test_textarea") + self.launchSurveyV2(schema_name="test_textarea") self.post({"answer": HTML_CONTENT}) self.get("/questionnaire/textarea-block") @@ -23,7 +23,9 @@ def test_quotes_in_textarea(self): assert ESCAPED_CONTENT in self.getResponseData() def test_quotes_in_detail_answer(self): - self.launchSurvey("test_radio_mandatory_with_detail_answer_mandatory") + self.launchSurveyV2( + schema_name="test_radio_mandatory_with_detail_answer_mandatory" + ) self.post( {"radio-mandatory-answer": "Other", "other-answer-mandatory": HTML_CONTENT} ) @@ -35,7 +37,7 @@ def test_quotes_in_detail_answer(self): def test_quotes_in_numeric_answers(self): testdata = [ ("test_numbers", "set-minimum"), - ("test_currency", "answer"), + ("test_currency", "answer-gbp"), ("test_percentage", "answer"), ("test_unit_patterns", "centimetres"), ("test_dates", "date-range-from-answer-day"), @@ -44,18 +46,18 @@ def test_quotes_in_numeric_answers(self): ] for schema, answer_id in testdata: with self.subTest(schema=schema, answer_id=answer_id): - self.launchSurvey(schema) + self.launchSurveyV2(schema_name=schema) self.post({answer_id: HTML_CONTENT}) assert ESCAPED_CONTENT in self.getResponseData() def test_textfield_summary(self): - self.launchSurvey("test_textfield") + self.launchSurveyV2(schema_name="test_textfield") self.post({"name-answer": HTML_CONTENT}) assert ESCAPED_CONTENT in self.getResponseData() def test_relationships(self): - self.launchSurvey("test_relationships") + self.launchSurveyV2(schema_name="test_relationships") self.post({"anyone-else": "Yes"}) self.post({"first-name": HTML_CONTENT, "last-name": "Jones"}) self.post({"anyone-else": "Yes"}) @@ -71,16 +73,16 @@ def test_relationships(self): # https://stackoverflow.com/questions/11224362/getattributename-unescapes-html # pylint: disable=line-too-long assert ( - 'data-title="Thinking of &#34;&gt;&lt;b&gt;some html&lt;/b&gt; Jones, Dave Jones is their <em>brother or sister</em>"' + 'data-title="Thinking of &#34;&gt;&lt;b&gt;some html&lt;/b&gt; Jones, Dave Jones is their <strong>brother or sister</strong>"' in self.getResponseData() ) assert ( - 'data-playback="Dave Jones is &#34;&gt;&lt;b&gt;some html&lt;/b&gt; Jones’ <em>brother or sister</em>"' + 'data-playback="Dave Jones is &#34;&gt;&lt;b&gt;some html&lt;/b&gt; Jones’ <strong>brother or sister</strong>"' in self.getResponseData() ) def test_composite_address(self): - self.launchSurvey("test_address") + self.launchSurveyV2(schema_name="test_address") self.post( { "address-mandatory-line1": "

    7 Evelyn Street

    ", @@ -94,7 +96,7 @@ def test_composite_address(self): ) def test_composite_address_summary(self): - self.launchSurvey("test_address") + self.launchSurveyV2(schema_name="test_address") self.post( { "address-mandatory-line1": "

    7 Evelyn Street

    ", @@ -107,7 +109,7 @@ def test_composite_address_summary(self): self.assertInBody("<p>7 Evelyn Street</p>") def test_list_collector(self): - self.launchSurvey("test_list_collector") + self.launchSurveyV2(schema_name="test_list_collector") self.post({"anyone-else": "Yes"}) self.post( { @@ -122,7 +124,7 @@ def test_list_collector(self): assert expected_remove_aria_label in self.getResponseData() def test_summary(self): - self.launchSurvey("test_submit_with_summary") + self.launchSurveyV2(schema_name="test_submit_with_summary") self.post({"radio-answer": "Bacon"}) self.post({"dessert-answer": HTML_CONTENT}) self.post({"dessert-confirmation-answer": "Yes"}) @@ -136,13 +138,13 @@ def test_summary(self): assert expected_change_aria_label in self.getResponseData() def test_radio_mandatory_error_with_placeholders(self): - self.launchSurvey("test_submit_with_summary") + self.launchSurveyV2(schema_name="test_submit_with_summary") self.post({"radio-answer": "Bacon"}) self.post({"dessert-answer": HTML_CONTENT}) self.post() expected_question_text = ( - f"Are you sure {ESCAPED_CONTENT} is your favourite?" + f"Are you sure {ESCAPED_CONTENT} is your favourite?" ) expected_error_message = f'Select an answer to ‘Are you sure {ESCAPED_CONTENT} is your favourite?’' assert expected_question_text in self.getResponseData() diff --git a/tests/integration/questionnaire/test_questionnaire_hub.py b/tests/integration/questionnaire/test_questionnaire_hub.py index a7440d65c3..2024246c87 100644 --- a/tests/integration/questionnaire/test_questionnaire_hub.py +++ b/tests/integration/questionnaire/test_questionnaire_hub.py @@ -5,7 +5,7 @@ class TestQuestionnaireHub(IntegrationTestCase): def test_navigation_to_hub_route_when_hub_not_enabled(self): # Given the hub is not enabled - self.launchSurvey("test_checkbox") + self.launchSurveyV2(schema_name="test_checkbox") # When I navigate to the hub url self.get(HUB_URL_PATH) @@ -16,7 +16,7 @@ def test_navigation_to_hub_route_when_hub_not_enabled(self): def test_redirect_to_hub_when_section_complete(self): # Given the hub is enabled - self.launchSurvey("test_hub_and_spoke") + self.launchSurveyV2(schema_name="test_hub_and_spoke") # When I complete a section self.post() @@ -27,7 +27,7 @@ def test_redirect_to_hub_when_section_complete(self): def test_hub_section_url_when_hub_not_enabled(self): # Given the hub is not enabled - self.launchSurvey("test_checkbox") + self.launchSurveyV2(schema_name="test_checkbox") # When I navigate to the url for a hub's section self.get("/questionnaire/sections/default-section/") @@ -37,7 +37,7 @@ def test_hub_section_url_when_hub_not_enabled(self): def test_section_url_when_hub_enabled_and_section_not_started(self): # Given the hub is enabled - self.launchSurvey("test_hub_and_spoke") + self.launchSurveyV2(schema_name="test_hub_and_spoke") # When I navigate to a url for a hub's section self.get("questionnaire/sections/employment-section/") @@ -47,7 +47,7 @@ def test_section_url_when_hub_enabled_and_section_not_started(self): def test_hub_section_url_when_hub_enabled_and_section_in_progress(self): # Given the hub is enabled and a section is in-progress - self.launchSurvey("test_hub_and_spoke") + self.launchSurveyV2(schema_name="test_hub_and_spoke") self.post() self.post({"employment-status-answer-exclusive": "None of these apply"}) self.get(HUB_URL_PATH) @@ -62,7 +62,7 @@ def test_hub_section_url_when_hub_enabled_and_section_in_progress(self): def test_hub_section_url_when_hub_enabled_and_section_complete(self): # Given the hub is enabled and a section is complete - self.launchSurvey("test_hub_and_spoke") + self.launchSurveyV2(schema_name="test_hub_and_spoke") self.get("/questionnaire/sections/accommodation-section/") self.post() self.post() @@ -76,7 +76,7 @@ def test_hub_section_url_when_hub_enabled_and_section_complete(self): self.assertEqualUrl("/questionnaire/sections/accommodation-section/") def test_hub_inaccessible_if_sections_required_and_incomplete(self): - self.launchSurvey("test_hub_complete_sections") + self.launchSurveyV2(schema_name="test_hub_complete_sections") self.get(HUB_URL_PATH) @@ -84,7 +84,7 @@ def test_hub_inaccessible_if_sections_required_and_incomplete(self): self.assertEqualUrl("/questionnaire/employment-status/") def test_hub_accessible_if_sections_required_and_complete(self): - self.launchSurvey("test_hub_complete_sections") + self.launchSurveyV2(schema_name="test_hub_complete_sections") self.post({"employment-status-answer": "Working as an employee"}) self.post() @@ -95,7 +95,7 @@ def test_hub_accessible_if_sections_required_and_complete(self): def test_hub_displays_repeating_sections_with_valid_urls(self): # Given the hub is enabled and a section is complete - self.launchSurvey("test_repeating_sections_with_hub_and_spoke") + self.launchSurveyV2(schema_name="test_repeating_sections_with_hub_and_spoke") # Go to first section self.post() @@ -120,7 +120,7 @@ def test_hub_displays_repeating_sections_with_valid_urls(self): # Visitors self.post({"visitors-anyone-else": "Yes"}) - self.post({"first-name": "Joe", "last-name": "Public"}) + self.post({"visitor-first-name": "Joe", "visitor-last-name": "Public"}) # Go back to hub self.post({"visitors-anyone-else": "No"}) @@ -140,7 +140,7 @@ def test_hub_displays_repeating_sections_with_valid_urls(self): self.get(first_repeating_section_url) self.post({"proxy-answer": "Yes"}) - self.assertInBody("What is John Doe’s date of birth?") + self.assertInBody("What is John Doe’s date of birth?") self.get(HUB_URL_PATH) @@ -149,16 +149,16 @@ def test_hub_displays_repeating_sections_with_valid_urls(self): self.get(second_repeating_section_url) self.post({"proxy-answer": "Yes"}) - self.assertInBody("What is Anna Doe’s date of birth?") + self.assertInBody("What is Anna Doe’s date of birth?") # Go to visitors visitor_repeating_section_url = section_urls[3].attrs["href"] self.get(visitor_repeating_section_url) - self.assertInBody("What is Joe Public’s date of birth?") + self.assertInBody("What is Joe Public’s date of birth?") def test_hub_section_required_but_enabled_false(self): # Given the hub is enabled and there are two required sections - self.launchSurvey("test_new_hub_section_required_and_enabled") + self.launchSurveyV2(schema_name="test_hub_section_required_and_enabled") # When I answer 'No' to the first section, meaning the second section is not enabled self.post({"household-relationships-answer": "No"}) @@ -170,10 +170,57 @@ def test_hub_section_required_but_enabled_false(self): def test_hub_section_required_but_enabled_true(self): # Given the hub is enabled and there are two required sections - self.launchSurvey("test_new_hub_section_required_and_enabled") + self.launchSurveyV2(schema_name="test_hub_section_required_and_enabled") # When I answer 'Yes' to the first section, meaning the second section is enabled self.post({"household-relationships-answer": "Yes"}) # Then I should be redirected to the second section self.assertEqualUrl("/questionnaire/relationships-count/") + + def test_hub_section_enabled_and_accessible_with_repeating_sections(self): + # Given the hub is enabled and there are two required sections (and one is for a repeat) + self.launchSurveyV2(schema_name="test_hub_section_required_with_repeat") + + # When I answer I complete the first section and the repeating section + self.post({"you-live-here": "Yes"}) + self.post({"first-name": "John", "last-name": "Doe"}) + self.post({"anyone-else": "No"}) + self.post() + self.post({"proxy-answer": "Yes"}) + self.post( + { + "date-of-birth-answer-day": 1, + "date-of-birth-answer-month": 2, + "date-of-birth-answer-year": 1999, + } + ) + self.post() + + # Then I should see the hub + self.assertEqualUrl(HUB_URL_PATH) + + def test_hub_section_is_inaccessible_when_all_repeats_not_complete(self): + # Given the hub is enabled and there are two required sections (and one is for a repeat) + self.launchSurveyV2(schema_name="test_hub_section_required_with_repeat") + + # When I complete the first section and the first repeat, but not the second repeat + self.post({"you-live-here": "Yes"}) + self.post({"first-name": "John", "last-name": "Doe"}) + self.post({"anyone-else": "Yes"}) + self.post({"first-name": "Jane", "last-name": "Doe"}) + self.post({"anyone-else": "No"}) + self.post() + self.post({"proxy-answer": "Yes"}) + self.post( + { + "date-of-birth-answer-day": 1, + "date-of-birth-answer-month": 2, + "date-of-birth-answer-year": 1999, + } + ) + self.post() + + # Then the hub should not yet be accessible + self.get(HUB_URL_PATH) + self.assertInUrl("/proxy") diff --git a/tests/integration/questionnaire/test_questionnaire_instructions.py b/tests/integration/questionnaire/test_questionnaire_instructions.py index 836c668c7e..2086fad032 100644 --- a/tests/integration/questionnaire/test_questionnaire_instructions.py +++ b/tests/integration/questionnaire/test_questionnaire_instructions.py @@ -5,12 +5,12 @@ class TestQuestionnaireInstructions(IntegrationTestCase): BASE_URL = "/questionnaire/" def test_interstitial_instruction(self): - self.launchSurvey("test_instructions") + self.launchSurveyV2(schema_name="test_instructions") self.post(action="start_questionnaire") self.assertInBody("Just pause for a second") def test_question_instruction(self): - self.launchSurvey("test_instructions") + self.launchSurveyV2(schema_name="test_instructions") self.post(action="start_questionnaire") self.post() self.assertInBody("Tell us about what you eat") diff --git a/tests/integration/questionnaire/test_questionnaire_interstitial.py b/tests/integration/questionnaire/test_questionnaire_interstitial.py index 461414291c..d660f0ef1c 100644 --- a/tests/integration/questionnaire/test_questionnaire_interstitial.py +++ b/tests/integration/questionnaire/test_questionnaire_interstitial.py @@ -6,13 +6,13 @@ class TestQuestionnaireInterstitial(IntegrationTestCase): BASE_URL = "/questionnaire/" def test_interstitial_page_button_text_is_continue(self): - self.launchSurvey("test_interstitial_page") + self.launchSurveyV2(schema_name="test_interstitial_page") self.post(action="start_questionnaire") self.post({"favourite-breakfast": "Cereal"}) self.assertInBody("Continue") def test_interstitial_can_continue_and_submit(self): - self.launchSurvey("test_interstitial_page") + self.launchSurveyV2(schema_name="test_interstitial_page") self.post(action="start_questionnaire") self.post({"favourite-breakfast": "Cereal"}) self.post() @@ -23,16 +23,12 @@ def test_interstitial_can_continue_and_submit(self): self.assertInUrl(THANK_YOU_URL_PATH) def test_interstitial_definition(self): - self.launchSurvey("test_interstitial_definition") + self.launchSurveyV2(schema_name="test_interstitial_definition") self.assertInBody("Successfully") - self.assertInBody("Questionnaire") self.assertInBody("In a way that accomplishes a desired aim or result") - self.assertInBody( - "A set of printed or written questions with a choice of answers, devised for the purposes of a survey or statistical study" - ) def test_interstitial_content_variant_definition(self): - self.launchSurvey("test_interstitial_definition") + self.launchSurveyV2(schema_name="test_interstitial_definition") self.post() self.post({"content-variant-definition-answer": "Answer"}) diff --git a/tests/integration/questionnaire/test_questionnaire_last_viewed_guidance.py b/tests/integration/questionnaire/test_questionnaire_last_viewed_guidance.py index b73f03ba87..da304b55a1 100644 --- a/tests/integration/questionnaire/test_questionnaire_last_viewed_guidance.py +++ b/tests/integration/questionnaire/test_questionnaire_last_viewed_guidance.py @@ -11,14 +11,14 @@ def __init__(self, *args, **kwargs): def test_not_shown_on_survey_launch(self): # Given - self.launchSurvey("test_last_viewed_question_guidance") + self.launchSurveyV2(schema_name="test_last_viewed_question_guidance") # Then last viewed question guidance should not be shown self._assert_last_viewed_question_guidance_not_shown() def test_not_shown_on_linear_journey(self): # Given - self.launchSurvey("test_last_viewed_question_guidance") + self.launchSurveyV2(schema_name="test_last_viewed_question_guidance") # When I complete the journey as normal, without resuming self.post() @@ -28,8 +28,9 @@ def test_not_shown_on_linear_journey(self): def test_not_shown_on_section_resume_first_block_in_new_section(self): # Given - self.launchSurvey( - "test_last_viewed_question_guidance", reponse_id=self.response_id + self.launchSurveyV2( + schema_name="test_last_viewed_question_guidance", + reponse_id=self.response_id, ) # When I sign out and resume on the first block of a new section @@ -38,8 +39,9 @@ def test_not_shown_on_section_resume_first_block_in_new_section(self): self._post_you_live_here_answer() self._post_list_collector_answers() self.signOut() - self.launchSurvey( - "test_last_viewed_question_guidance", reponse_id=self.response_id + self.launchSurveyV2( + schema_name="test_last_viewed_question_guidance", + reponse_id=self.response_id, ) # Then the last viewed question guidance is not shown @@ -47,14 +49,16 @@ def test_not_shown_on_section_resume_first_block_in_new_section(self): def test_not_shown_on_resume_section_not_started(self): # Given - self.launchSurvey( - "test_last_viewed_question_guidance", reponse_id=self.response_id + self.launchSurveyV2( + schema_name="test_last_viewed_question_guidance", + reponse_id=self.response_id, ) # When I sign out without starting the section and I resume the survey self.signOut() - self.launchSurvey( - "test_last_viewed_question_guidance", reponse_id=self.response_id + self.launchSurveyV2( + schema_name="test_last_viewed_question_guidance", + reponse_id=self.response_id, ) # Then the last viewed question guidance should not be shown @@ -62,15 +66,17 @@ def test_not_shown_on_resume_section_not_started(self): def test_shown_on_resume_section_in_progress(self): # Given - self.launchSurvey( - "test_last_viewed_question_guidance", reponse_id=self.response_id + self.launchSurveyV2( + schema_name="test_last_viewed_question_guidance", + reponse_id=self.response_id, ) # When I sign out after I have started the section and I resume the survey self.post() self.signOut() - self.launchSurvey( - "test_last_viewed_question_guidance", reponse_id=self.response_id + self.launchSurveyV2( + schema_name="test_last_viewed_question_guidance", + reponse_id=self.response_id, ) # Then the last viewed guidance should be shown @@ -81,16 +87,18 @@ def test_shown_on_resume_section_in_progress(self): def test_shown_on_section_in_progress_resume_primary_person_list_collector(self): # Given - self.launchSurvey( - "test_last_viewed_question_guidance", reponse_id=self.response_id + self.launchSurveyV2( + schema_name="test_last_viewed_question_guidance", + reponse_id=self.response_id, ) # When I sign out and resume on a primary person list collector self.post() self._post_address_confirmation_answer() self.signOut() - self.launchSurvey( - "test_last_viewed_question_guidance", reponse_id=self.response_id + self.launchSurveyV2( + schema_name="test_last_viewed_question_guidance", + reponse_id=self.response_id, ) # Then the last viewed guidance is shown @@ -103,8 +111,9 @@ def test_shown_on_section_in_progress_resume_primary_person_list_collector_add_p self, ): # Given - self.launchSurvey( - "test_last_viewed_question_guidance", reponse_id=self.response_id + self.launchSurveyV2( + schema_name="test_last_viewed_question_guidance", + reponse_id=self.response_id, ) # When I sign out and resume on a primary person list collector add person page @@ -112,8 +121,9 @@ def test_shown_on_section_in_progress_resume_primary_person_list_collector_add_p self._post_address_confirmation_answer() self._post_you_live_here_answer() self.signOut() - self.launchSurvey( - "test_last_viewed_question_guidance", reponse_id=self.response_id + self.launchSurveyV2( + schema_name="test_last_viewed_question_guidance", + reponse_id=self.response_id, ) # Then the last viewed guidance is shown on the parent page @@ -124,8 +134,9 @@ def test_shown_on_section_in_progress_resume_primary_person_list_collector_add_p def test_shown_on_section_in_progress_resume_list_collector(self): # Given - self.launchSurvey( - "test_last_viewed_question_guidance", reponse_id=self.response_id + self.launchSurveyV2( + schema_name="test_last_viewed_question_guidance", + reponse_id=self.response_id, ) # When I sign out and resume on a list collector @@ -134,8 +145,9 @@ def test_shown_on_section_in_progress_resume_list_collector(self): self._post_you_live_here_answer() self._post_primary_person_answer() self.signOut() - self.launchSurvey( - "test_last_viewed_question_guidance", reponse_id=self.response_id + self.launchSurveyV2( + schema_name="test_last_viewed_question_guidance", + reponse_id=self.response_id, ) # Then the last viewed guidance is shown @@ -146,8 +158,9 @@ def test_shown_on_section_in_progress_resume_list_collector(self): def test_shown_on_section_in_progress_resume_list_collector_add_person(self): # Given - self.launchSurvey( - "test_last_viewed_question_guidance", reponse_id=self.response_id + self.launchSurveyV2( + schema_name="test_last_viewed_question_guidance", + reponse_id=self.response_id, ) # When I sign out and resume on a list collector add person @@ -157,8 +170,9 @@ def test_shown_on_section_in_progress_resume_list_collector_add_person(self): self._post_primary_person_answer() self.post({"anyone-else": "Yes"}) self.signOut() - self.launchSurvey( - "test_last_viewed_question_guidance", reponse_id=self.response_id + self.launchSurveyV2( + schema_name="test_last_viewed_question_guidance", + reponse_id=self.response_id, ) # Then the last viewed guidance is shown on the parent page @@ -169,8 +183,9 @@ def test_shown_on_section_in_progress_resume_list_collector_add_person(self): def test_not_shown_on_section_in_progress_resume_relationships(self): # Given - self.launchSurvey( - "test_last_viewed_question_guidance", reponse_id=self.response_id + self.launchSurveyV2( + schema_name="test_last_viewed_question_guidance", + reponse_id=self.response_id, ) # When I sign out and resume on a relationship question @@ -182,8 +197,9 @@ def test_not_shown_on_section_in_progress_resume_relationships(self): self.post() self.post() self.signOut() - self.launchSurvey( - "test_last_viewed_question_guidance", reponse_id=self.response_id + self.launchSurveyV2( + schema_name="test_last_viewed_question_guidance", + reponse_id=self.response_id, ) # Then the last viewed guidance is not shown @@ -191,14 +207,14 @@ def test_not_shown_on_section_in_progress_resume_relationships(self): def test_not_shown_on_survey_launch_hub_not_available(self): # Given - self.launchSurvey("test_last_viewed_question_guidance_hub") + self.launchSurveyV2(schema_name="test_last_viewed_question_guidance_hub") # When the hub is not available, then last viewed guidance should not be shown self._assert_last_viewed_question_guidance_not_shown() def test_not_shown_on_section_not_started_hub(self): # Given - self.launchSurvey("test_last_viewed_question_guidance_hub") + self.launchSurveyV2(schema_name="test_last_viewed_question_guidance_hub") # When clicking on a link from the hub to a section not started self._posts_for_hub_required_section_to_complete() @@ -209,7 +225,7 @@ def test_not_shown_on_section_not_started_hub(self): def test_shown_on_section_in_progress_hub_using_link_from_hub(self): # Given - self.launchSurvey("test_last_viewed_question_guidance_hub") + self.launchSurveyV2(schema_name="test_last_viewed_question_guidance_hub") # When clicking on a link from the hub to a section that is in progress self._posts_for_hub_required_section_to_complete() @@ -222,7 +238,7 @@ def test_shown_on_section_in_progress_hub_using_link_from_hub(self): def test_shown_on_section_in_progress_hub_using_continue_from_hub(self): # Given - self.launchSurvey("test_last_viewed_question_guidance_hub") + self.launchSurveyV2(schema_name="test_last_viewed_question_guidance_hub") # When clicking on continue from the hub to a section that is in progress self._posts_for_hub_required_section_to_complete() diff --git a/tests/integration/questionnaire/test_questionnaire_list_change_evaluates_sections.py b/tests/integration/questionnaire/test_questionnaire_list_change_evaluates_sections.py index bb68a8d70c..fbcbb6e1a7 100644 --- a/tests/integration/questionnaire/test_questionnaire_list_change_evaluates_sections.py +++ b/tests/integration/questionnaire/test_questionnaire_list_change_evaluates_sections.py @@ -1,4 +1,4 @@ -from . import QuestionnaireTestCase +from tests.integration.questionnaire import QuestionnaireTestCase class TestQuestionnaireListChangeEvaluatesSections(QuestionnaireTestCase): @@ -11,7 +11,7 @@ def get_link(self, action, position): return filtered[0].get("href") def test_without_primary_person(self): - self.launchSurvey("test_list_change_evaluates_sections") + self.launchSurveyV2(schema_name="test_list_change_evaluates_sections") self.get("/questionnaire/sections/who-lives-here/") self.assertEqualUrl("/questionnaire/primary-person-list-collector/") @@ -30,9 +30,11 @@ def test_without_primary_person(self): self.post() self.assertEqualUrl("/questionnaire/") - self.get("questionnaire/people/add-person/?return_to=section-summary") + self.get("questionnaire/people/add-person") self.add_person("John", "Doe") self.post({"anyone-else": "No"}) + self.assertEqualUrl("/questionnaire/sections/who-lives-here/") + self.post() self.assertEqualUrl("/questionnaire/") self.assertInSelector( @@ -43,7 +45,7 @@ def test_without_primary_person(self): self.assertEqualUrl("/questionnaire/own-or-rent/?resume=True") def test_with_primary_person(self): - self.launchSurvey("test_list_change_evaluates_sections") + self.launchSurveyV2(schema_name="test_list_change_evaluates_sections") self.get("/questionnaire/sections/accommodation-section/") self.post() diff --git a/tests/integration/questionnaire/test_questionnaire_list_collector.py b/tests/integration/questionnaire/test_questionnaire_list_collector.py index 619edd880b..d78c486049 100644 --- a/tests/integration/questionnaire/test_questionnaire_list_collector.py +++ b/tests/integration/questionnaire/test_questionnaire_list_collector.py @@ -1,4 +1,6 @@ -from . import SUBMIT_URL_PATH, QuestionnaireTestCase +from tests.integration.questionnaire import SUBMIT_URL_PATH, QuestionnaireTestCase + +# pylint: disable=too-many-public-methods class TestQuestionnaireListCollector(QuestionnaireTestCase): @@ -8,28 +10,28 @@ def get_add_someone_link(self): return selected[0].get("href") def test_invalid_add_block_url(self): - self.launchSurvey("test_list_collector") + self.launchSurveyV2(schema_name="test_list_collector") self.get("/questionnaire/people/123/add-person") self.assertStatusNotFound() def test_invalid_list_name(self): - self.launchSurvey("test_list_collector") + self.launchSurveyV2(schema_name="test_list_collector") self.get("/questionnaire/invalid-list-name/add-person/") self.assertStatusNotFound() def test_invalid_list_item_id_for_edit_block(self): - self.launchSurvey("test_list_collector") + self.launchSurveyV2(schema_name="test_list_collector") self.get("/questionnaire/people/123/edit-person") self.assertStatusNotFound() def test_happy_path(self): - self.launchSurvey("test_list_collector") + self.launchSurveyV2(schema_name="test_list_collector") self.assertInBody("Does anyone else live here?") @@ -103,7 +105,7 @@ def test_happy_path(self): self.assertEqualUrl("/questionnaire/list-collector/") def test_list_collector_submission(self): - self.launchSurvey("test_list_collector") + self.launchSurveyV2(schema_name="test_list_collector") self.post(action="start_questionnaire") @@ -150,7 +152,7 @@ def test_list_collector_submission(self): self.assertInUrl("thank-you") def test_optional_list_collector_submission(self): - self.launchSurvey("test_list_collector") + self.launchSurveyV2(schema_name="test_list_collector") self.post(action="start_questionnaire") @@ -169,7 +171,7 @@ def test_optional_list_collector_submission(self): self.assertInUrl(SUBMIT_URL_PATH) def test_list_summary_on_question(self): - self.launchSurvey("test_list_summary_on_question") + self.launchSurveyV2(schema_name="test_list_summary_on_question") self.post(action="start_questionnaire") @@ -194,7 +196,7 @@ def test_list_summary_on_question(self): self.assertInBody("Marie Claire Doe") def test_questionnaire_summary_with_custom_section_summary(self): - self.launchSurvey("test_list_summary_on_question") + self.launchSurveyV2(schema_name="test_list_summary_on_question") self.post(action="start_questionnaire") @@ -217,7 +219,7 @@ def test_questionnaire_summary_with_custom_section_summary(self): self.assertNotInBody("No, all household members are unrelated") def test_cancel_text_displayed_on_add_block_if_exists(self): - self.launchSurvey("test_list_collector") + self.launchSurveyV2(schema_name="test_list_collector") self.post(action="start_questionnaire") @@ -226,7 +228,7 @@ def test_cancel_text_displayed_on_add_block_if_exists(self): self.assertInBody("Don’t need to add anyone else?") def test_cancel_text_displayed_on_edit_block_if_exists(self): - self.launchSurvey("test_list_collector") + self.launchSurveyV2(schema_name="test_list_collector") self.post(action="start_questionnaire") @@ -241,7 +243,7 @@ def test_cancel_text_displayed_on_edit_block_if_exists(self): self.assertInBody("Don’t need to change anything?") def test_warning_text_displayed_on_remove_block_if_exists(self): - self.launchSurvey("test_list_collector") + self.launchSurveyV2(schema_name="test_list_collector") self.post(action="start_questionnaire") @@ -261,7 +263,7 @@ def test_warning_text_displayed_on_remove_block_if_exists(self): def test_list_collector_return_to_when_section_summary_cant_be_displayed(self): # Given I have completed a section and returned to a list_collector from the section summary - self.launchSurvey("test_relationships", roles=["dumper"]) + self.launchSurveyV2(schema_name="test_relationships", roles=["dumper"]) self.add_person("Marie", "Doe") @@ -283,7 +285,7 @@ def test_list_collector_return_to_when_section_summary_cant_be_displayed(self): def test_adding_person_using_second_list_collector_when_no_people( self, ): - self.launchSurvey("test_list_collector_two_list_collectors") + self.launchSurveyV2(schema_name="test_list_collector_two_list_collectors") self.assertInBody("Does anyone live at your address?") @@ -320,3 +322,287 @@ def test_adding_person_using_second_list_collector_when_no_people( self.get(first_person_remove_link) self.assertInBody("Are you sure you want to remove this person?") + + def test_adding_from_the_summary_page_adds_the_return_to_param_to_the_url( + self, + ): + self.launchSurveyV2(schema_name="test_list_collector") + + self.assertInBody("Does anyone else live here?") + + self.post({"anyone-else": "Yes"}) + + self.add_person("Marie Claire", "Doe") + + self.assertInSelector("Marie Claire Doe", "[data-qa='list-item-1-label']") + + self.post({"anyone-else": "No"}) + + self.post() + + self.post({"another-anyone-else": "No"}) + + self.assertInBody("List Collector Summary") + + # Make another mistake + + add_link = self.get_add_someone_link() + + self.get(add_link) + + self.assertInUrl("?return_to=section-summary") + + def test_removing_from_the_summary_page_adds_the_return_to_param_to_the_url( + self, + ): + self.launchSurveyV2(schema_name="test_list_collector") + + self.assertInBody("Does anyone else live here?") + + self.post({"anyone-else": "Yes"}) + + self.add_person("Marie Claire", "Doe") + + self.assertInSelector("Marie Claire Doe", "[data-qa='list-item-1-label']") + + self.post({"anyone-else": "No"}) + + self.post() + + self.post({"another-anyone-else": "No"}) + + self.assertInBody("List Collector Summary") + + # Make another mistake + + remove_link = self.get_link("remove", 1) + + self.get(remove_link) + + self.assertInUrl("?return_to=section-summary") + + def test_changing_item_from_the_summary_page_adds_the_return_to_param_to_the_url( + self, + ): + self.launchSurveyV2(schema_name="test_list_collector") + + self.assertInBody("Does anyone else live here?") + + self.post({"anyone-else": "Yes"}) + + self.add_person("Marie Claire", "Doe") + + self.assertInSelector("Marie Claire Doe", "[data-qa='list-item-1-label']") + + self.post({"anyone-else": "No"}) + + self.post() + + self.post({"another-anyone-else": "No"}) + + self.assertInBody("List Collector Summary") + + change_link = self.get_link("change", 1) + + self.get(change_link) + + self.assertInUrl("?return_to=section-summary") + + def test_adding_from_the_summary_page_and_then_removing_from_parent_page_keeps_return_to_url_param( + self, + ): + self.launchSurveyV2(schema_name="test_list_collector") + + self.assertInBody("Does anyone else live here?") + + self.post({"anyone-else": "Yes"}) + + self.add_person("Marie Claire", "Doe") + + self.assertInSelector("Marie Claire Doe", "[data-qa='list-item-1-label']") + + self.post({"anyone-else": "No"}) + + self.post() + + self.post({"another-anyone-else": "No"}) + + self.assertInBody("List Collector Summary") + + # Add another one form the summary + + add_link = self.get_add_someone_link() + + self.get(add_link) + + self.add_person("Don", "Page") + + self.assertInSelector("Don Page", "[data-qa='list-item-2-label']") + + remove_link = self.get_link("remove", 2) + + self.get(remove_link) + + self.assertInUrl("?return_to=section-summary") + + self.post({"remove-confirmation": "Yes"}) + + self.assertInUrl("?return_to=section-summary") + + def test_adding_from_the_summary_page_and_then_changing_from_parent_page_keeps_return_to_url_param( + self, + ): + self.launchSurveyV2(schema_name="test_list_collector") + + self.assertInBody("Does anyone else live here?") + + self.post({"anyone-else": "Yes"}) + + self.add_person("Marie Claire", "Doe") + + self.assertInSelector("Marie Claire Doe", "[data-qa='list-item-1-label']") + + self.post({"anyone-else": "No"}) + + self.post() + + self.post({"another-anyone-else": "No"}) + + self.assertInBody("List Collector Summary") + + # Add another one form the summary + + add_link = self.get_add_someone_link() + + self.get(add_link) + + self.add_person("Don", "Page") + + self.assertInSelector("Don Page", "[data-qa='list-item-2-label']") + + change_link = self.get_link("change", 2) + + self.get(change_link) + + self.assertInUrl("?return_to=section-summary") + + self.post({"first-name": "Another", "last-name": "Name"}) + + self.assertInUrl("?return_to=section-summary") + + def test_adding_from_the_summary_page_and_then_clicking_previous_link_from_edit_question_block_persists_return_to_url_param( + self, + ): + self.launchSurveyV2(schema_name="test_list_collector") + + self.assertInBody("Does anyone else live here?") + + self.post({"anyone-else": "Yes"}) + + self.add_person("Marie Claire", "Doe") + + self.assertInSelector("Marie Claire Doe", "[data-qa='list-item-1-label']") + + self.post({"anyone-else": "No"}) + + self.post() + + self.post({"another-anyone-else": "No"}) + + self.assertInBody("List Collector Summary") + + # Add another one form the summary + + add_link = self.get_add_someone_link() + + self.get(add_link) + + self.add_person("Don", "Page") + + self.assertInSelector("Don Page", "[data-qa='list-item-2-label']") + + change_link = self.get_link("change", 2) + + self.get(change_link) + + self.previous() + + self.assertInUrl("?return_to=section-summary") + + def test_adding_from_the_summary_page_and_then_clicking_previous_link_from_remove_question_block_persists_return_to_url_param( + self, + ): + self.launchSurveyV2(schema_name="test_list_collector") + + self.assertInBody("Does anyone else live here?") + + self.post({"anyone-else": "Yes"}) + + self.add_person("Marie Claire", "Doe") + + self.assertInSelector("Marie Claire Doe", "[data-qa='list-item-1-label']") + + self.post({"anyone-else": "No"}) + + self.post() + + self.post({"another-anyone-else": "No"}) + + self.assertInBody("List Collector Summary") + + # Add another one form the summary + + add_link = self.get_add_someone_link() + + self.get(add_link) + + self.add_person("Don", "Page") + + self.assertInSelector("Don Page", "[data-qa='list-item-2-label']") + + remove_link = self.get_link("remove", 2) + + self.get(remove_link) + + self.previous() + + self.assertInUrl("?return_to=section-summary") + + def test_adding_from_the_summary_page_and_then_adding_again_from_list_collector_persists_the_return_to_url_param( + self, + ): + self.launchSurveyV2(schema_name="test_list_collector") + + self.assertInBody("Does anyone else live here?") + + self.post({"anyone-else": "Yes"}) + + self.add_person("Marie Claire", "Doe") + + self.assertInSelector("Marie Claire Doe", "[data-qa='list-item-1-label']") + + self.post({"anyone-else": "No"}) + + self.post() + + self.post({"another-anyone-else": "No"}) + + self.assertInBody("List Collector Summary") + + # Add another one form the summary + + add_link = self.get_add_someone_link() + + self.get(add_link) + + self.add_person("Don", "Page") + + self.assertInSelector("Don Page", "[data-qa='list-item-2-label']") + + self.post({"anyone-else": "Yes"}) + + self.assertInUrl("?return_to=section-summary") + + self.add_person("Another", "Person") + + self.assertInUrl("?return_to=section-summary") diff --git a/tests/integration/questionnaire/test_questionnaire_list_collector_content.py b/tests/integration/questionnaire/test_questionnaire_list_collector_content.py new file mode 100644 index 0000000000..9d6416e142 --- /dev/null +++ b/tests/integration/questionnaire/test_questionnaire_list_collector_content.py @@ -0,0 +1,176 @@ +from tests.integration.questionnaire import QuestionnaireTestCase + + +class TestQuestionnaireListCollectorContent(QuestionnaireTestCase): + def get_add_someone_link(self): + selector = "[data-qa='add-item-link']" + selected = self.getHtmlSoup().select(selector) + return selected[0].get("href") + + def add_company(self, name: str, number: str, authorised_insurer: str): + self.post( + { + "company-or-branch-name": name, + "registration-number": number, + "authorised-insurer-radio": authorised_insurer, + } + ) + + def test_happy_path(self): + self.launchSurveyV2(schema_name="test_list_collector_content_page") + + self.post({"any-companies-or-branches-answer": "Yes"}) + self.add_company("Company A", "123", "No") + self.post({"any-other-companies-or-branches-answer": "Yes"}) + self.add_company("Company B", "456", "No") + self.post({"any-other-companies-or-branches-answer": "No"}) + self.post() + self.post() + + self.post({"responsible-party-answer": "Yes"}) + self.assertInUrl("/questionnaire/list-collector-content/") + self.assertInBody( + "You have previously reported the following companies. Press continue to updated registration and trading information." + ) + + self.post() + + self.assertInUrl("/companies-repeating-block-1/") + self.assertInBody("Give details about Company A") + + self.post( + { + "registration-number-repeating-block": "123", + "registration-date-repeating-block-day": "1", + "registration-date-repeating-block-month": "1", + "registration-date-repeating-block-year": "1990", + } + ) + + self.assertInUrl("/companies-repeating-block-2/") + self.assertInBody("Give details about how Company A") + + self.post( + { + "authorised-trader-uk-radio-repeating-block": "Yes", + "authorised-trader-eu-radio-repeating-block": "No", + } + ) + self.post() + + self.assertInBody("Give details about Company B") + self.assertInUrl("/companies-repeating-block-1/") + + self.post( + { + "registration-number-repeating-block": "456", + "registration-date-repeating-block-day": "1", + "registration-date-repeating-block-month": "1", + "registration-date-repeating-block-year": "1990", + } + ) + + self.assertInBody("Give details about how Company B") + + self.post( + { + "authorised-trader-uk-radio-repeating-block": "Yes", + "authorised-trader-eu-radio-repeating-block": "No", + } + ) + + self.assertInBody( + "You have previously reported the following companies. Press continue to updated registration and trading information." + ) + + self.post() + + self.assertInUrl("questionnaire/sections/section-list-collector-contents/") + self.assertInBody("List Collector Contents") + + self.post() + + self.assertInUrl("/questionnaire/") + + def test_hub_section_in_progress(self): + self.launchSurveyV2(schema_name="test_list_collector_content_page") + + self.post({"any-companies-or-branches-answer": "Yes"}) + self.add_company("Company A", "123", "No") + self.post({"any-other-companies-or-branches-answer": "Yes"}) + self.add_company("Company B", "456", "No") + self.post({"any-other-companies-or-branches-answer": "No"}) + self.post() + self.post() + + self.post({"responsible-party-answer": "Yes"}) + self.assertInUrl("/questionnaire/list-collector-content/") + self.assertInBody( + "You have previously reported the following companies. Press continue to updated registration and trading information." + ) + + self.post() + + self.assertInUrl("/companies-repeating-block-1/") + self.assertInBody("Give details about Company A") + + self.post( + { + "registration-number-repeating-block": "123", + "registration-date-repeating-block-day": "1", + "registration-date-repeating-block-month": "1", + "registration-date-repeating-block-year": "1990", + } + ) + self.previous() + self.previous() + self.previous() + self.previous() + + self.assertInBody("Partially completed") + + def test_hub_section_in_progress_when_one_complete(self): + self.launchSurveyV2(schema_name="test_list_collector_content_page") + + self.post({"any-companies-or-branches-answer": "Yes"}) + self.add_company("Company A", "123", "No") + self.post({"any-other-companies-or-branches-answer": "Yes"}) + self.add_company("Company B", "456", "No") + self.post({"any-other-companies-or-branches-answer": "No"}) + self.post() + self.post() + + self.post({"responsible-party-answer": "Yes"}) + self.assertInUrl("/questionnaire/list-collector-content/") + self.assertInBody( + "You have previously reported the following companies. Press continue to updated registration and trading information." + ) + + self.post() + + self.assertInUrl("/companies-repeating-block-1/") + + self.assertInBody("Give details about Company A") + + self.post( + { + "registration-number-repeating-block": "123", + "registration-date-repeating-block-day": "1", + "registration-date-repeating-block-month": "1", + "registration-date-repeating-block-year": "1990", + } + ) + + self.assertInUrl("/companies-repeating-block-2/") + self.assertInBody("Give details about how Company A") + + self.post( + { + "authorised-trader-uk-radio-repeating-block": "Yes", + "authorised-trader-eu-radio-repeating-block": "No", + } + ) + self.previous() + self.previous() + + self.assertInBody("Partially completed") diff --git a/tests/integration/questionnaire/test_questionnaire_list_collector_primary_person.py b/tests/integration/questionnaire/test_questionnaire_list_collector_primary_person.py index 49750bb9d9..2eb9bcaf87 100644 --- a/tests/integration/questionnaire/test_questionnaire_list_collector_primary_person.py +++ b/tests/integration/questionnaire/test_questionnaire_list_collector_primary_person.py @@ -2,19 +2,19 @@ import re import string -from . import QuestionnaireTestCase +from tests.integration.questionnaire import QuestionnaireTestCase class TestQuestionnaireListCollector(QuestionnaireTestCase): def test_invalid_list_on_primary_person_collector(self): - self.launchSurvey("test_list_collector_primary_person") + self.launchSurveyV2(schema_name="test_list_collector_primary_person") self.get("/questionnaire/invalid/123423/add-or-edit-person/") self.assertStatusNotFound() def test_invalid_list_item_id_for_primary_person_add_block(self): - self.launchSurvey("test_list_collector_primary_person") + self.launchSurveyV2(schema_name="test_list_collector_primary_person") self.post({"you-live-here": "Yes"}) self.assertInUrl("add-or-edit-primary-person/") @@ -24,7 +24,7 @@ def test_invalid_list_item_id_for_primary_person_add_block(self): self.assertStatusNotFound() def test_non_primary_person_list_item_id_for_primary_person_add_block(self): - self.launchSurvey("test_list_collector_primary_person") + self.launchSurveyV2(schema_name="test_list_collector_primary_person") # Add a non-primary person self.post({"you-live-here": "No"}) @@ -52,7 +52,7 @@ def test_non_primary_person_list_item_id_for_primary_person_add_block(self): self.assertStatusNotFound() def test_adding_then_removing_primary_person(self): - self.launchSurvey("test_list_collector_primary_person") + self.launchSurveyV2(schema_name="test_list_collector_primary_person") self.post({"you-live-here": "Yes"}) @@ -83,7 +83,7 @@ def test_adding_then_removing_primary_person(self): self.assertInBody("James May") def test_cannot_remove_primary_person_from_list_collector(self): - self.launchSurvey("test_list_collector_primary_person") + self.launchSurveyV2(schema_name="test_list_collector_primary_person") self.post({"you-live-here": "Yes"}) @@ -112,7 +112,9 @@ def test_changing_answer_from_no_to_yes_on_primary_person_list_collector_resumes response_id = random.choices(string.digits, k=16) # Given I initially answer 'No' to the primary person list collector - self.launchSurvey("test_list_collector_primary_person", reponse_id=response_id) + self.launchSurveyV2( + schema_name="test_list_collector_primary_person", reponse_id=response_id + ) self.post({"you-live-here": "No"}) # When I change my answer to 'Yes' and sign out @@ -121,14 +123,16 @@ def test_changing_answer_from_no_to_yes_on_primary_person_list_collector_resumes self.signOut() # Then on resuming, I am returned to the primary-person-list-collector - self.launchSurvey("test_list_collector_primary_person", reponse_id=response_id) + self.launchSurveyV2( + schema_name="test_list_collector_primary_person", reponse_id=response_id + ) self.assertInUrl("/questionnaire/primary-person-list-collector/") def test_section_summary_with_primary_no_driving_question_on_path( self, ): - self.launchSurvey( - "test_list_collector_primary_and_collector_with_driving_question" + self.launchSurveyV2( + schema_name="test_list_collector_primary_and_collector_with_driving_question" ) self.assertInBody("Start section") diff --git a/tests/integration/questionnaire/test_questionnaire_list_collector_repeating_blocks.py b/tests/integration/questionnaire/test_questionnaire_list_collector_repeating_blocks.py new file mode 100644 index 0000000000..198b959f20 --- /dev/null +++ b/tests/integration/questionnaire/test_questionnaire_list_collector_repeating_blocks.py @@ -0,0 +1,506 @@ +from datetime import date + +from tests.integration.integration_test_case import IntegrationTestCase + +# pylint: disable=too-many-public-methods + + +class TestQuestionnaireListCollectorRepeatingBlocks(IntegrationTestCase): + def launch_repeating_blocks_test_survey(self): + self.launchSurveyV2( + schema_name="test_list_collector_repeating_blocks_section_summary" + ) + self.post({"responsible-party-answer": "Yes"}) + + def add_company(self, company_name: str): + self.assertInUrl("/companies/add-company/") + self.post({"company-or-branch-name": company_name}) + + def add_company_from_list_collector( + self, company_name: str, is_driving: bool = False + ): + if is_driving: + self.assertInUrl("/questionnaire/any-companies-or-branches/") + self.post({"any-companies-or-branches-answer": "Yes"}) + else: + self.assertInUrl("/questionnaire/any-other-companies-or-branches/") + self.post({"any-other-companies-or-branches-answer": "Yes"}) + self.add_company(company_name) + + def post_repeating_block_1(self, registration_number: int, registration_date: date): + self.assertInUrl("/companies/") + self.assertInUrl("/companies-repeating-block-1/") + self.post( + { + "registration-number": registration_number, + "registration-date-day": registration_date.day, + "registration-date-month": registration_date.month, + "registration-date-year": registration_date.year, + } + ) + + def post_repeating_block_2(self, trader_uk: str, trader_eu: str | None = None): + self.assertInUrl("/companies/") + self.assertInUrl("/companies-repeating-block-2/") + self.post( + { + "authorised-trader-uk-radio": trader_uk, + "authorised-trader-eu-radio": trader_eu, + } + ) + + def add_company_and_repeating_blocks( + self, + company_name: str, + registration_number: int, + registration_date: date, + trader_uk: str, + trader_eu: str | None = None, + is_driving: bool = False, + ): + self.add_company_from_list_collector( + company_name=company_name, is_driving=is_driving + ) + self.post_repeating_block_1( + registration_number=registration_number, registration_date=registration_date + ) + self.post_repeating_block_2(trader_uk=trader_uk, trader_eu=trader_eu) + + def assert_company_completed(self, company_number: int, label_number: int): + selector = f"[data-qa='list-item-{label_number}-label']" + self.assertInSelector(f"Company{company_number}", selector) + self.assertInSelector("ons-summary__item-title-icon--check", selector) + + def assert_company_incomplete(self, company_number: int, label_number: int): + selector = f"[data-qa='list-item-{label_number}-label']" + self.assertInSelector(f"Company{company_number}", selector) + self.assertNotInSelector("ons-summary__item-title-icon--check", selector) + + def assert_list_item_answers_in_summary(self): + self.assertInSelector("Company1", "[class='ons-summary__item']") + + def get_list_item_link(self, action, position): + selector = f"[data-qa='list-item-{action}-{position}-link']" + selected = self.getHtmlSoup().select(selector) + return selected[0].get("href") + + def get_link(self, selector: str): + return self.getHtmlSoup().select(selector)[0]["href"] + + def get_list_item_ids(self): + result = self.getHtmlSoup().select("[data-list-item-id]") + return [list_item["data-list-item-id"] for list_item in result] + + def click_edit_link(self, answer_id: str, position: int): + list_item_ids = self.get_list_item_ids() + selector = f"[data-qa='{answer_id}-{list_item_ids[position]}-edit']" + selected = self.getHtmlSoup().select(selector) + edit_link = selected[0]["href"] + self.get(edit_link) + + def click_add_link(self): + add_link = self.get_link("[data-qa='add-item-link']") + self.get(add_link) + + def click_cancel_link(self): + cancel_link = self.get_link("[id='cancel-and-return']") + self.get(cancel_link) + + def add_three_companies(self): + # Add first company + self.add_company_and_repeating_blocks( + company_name="Company1", + registration_number=123, + registration_date=date(2023, 1, 1), + trader_uk="Yes", + trader_eu="Yes", + is_driving=True, + ) + + # Add second company + self.add_company_and_repeating_blocks( + company_name="Company2", + registration_number=456, + registration_date=date(2023, 2, 2), + trader_uk="Yes", + ) + + # Add third company + self.add_company_and_repeating_blocks( + company_name="Company3", + registration_number=789, + registration_date=date(2023, 3, 3), + trader_uk="No", + trader_eu="Yes", + ) + + self.assert_company_completed(1, 1) + self.assert_company_completed(2, 2) + self.assert_company_completed(3, 3) + + def test_invalid_invalid_list_item_id(self): + self.launch_repeating_blocks_test_survey() + self.get( + "/questionnaire/companies/non-existing-list-item-id/companies-repeating-block-1/" + ) + self.assertStatusNotFound() + + def test_happy_path(self): + self.launch_repeating_blocks_test_survey() + + # Add some items to the list + self.add_three_companies() + + # Remove item 2 + remove_link = self.get_list_item_link("remove", 2) + self.get(remove_link) + self.assertInBody("Are you sure you want to remove this company or UK branch?") + + # Cancel + self.post({"remove-confirmation": "No"}) + self.assertEqualUrl("/questionnaire/any-other-companies-or-branches/") + + # Remove again + self.get(remove_link) + self.post({"remove-confirmation": "Yes"}) + + # Check list item 3 has moved to second position + self.assert_company_completed(3, 2) + + # Test the previous link + edit_link_1 = self.get_list_item_link("change", 1) + remove_link_1 = self.get_list_item_link("remove", 1) + + self.get(edit_link_1) + self.assertInUrl("/edit-company/") + self.previous() + self.assertEqualUrl("/questionnaire/any-other-companies-or-branches/") + + self.get(remove_link_1) + self.assertInUrl("/remove-company/") + self.previous() + self.assertEqualUrl("/questionnaire/any-other-companies-or-branches/") + + # Submit survey + self.post({"any-other-companies-or-branches-answer": "No"}) + self.post({"any-other-trading-details-answer": "No other details"}) + self.post() + self.post() + self.assertInBody( + "Thank you for completing the Test a List Collector with Repeating Blocks and Section Summary Items" + ) + + def test_incomplete_repeating_blocks(self): + self.launch_repeating_blocks_test_survey() + + # Add first company - only add block and first repeating block + self.add_company_from_list_collector(company_name="Company1", is_driving=True) + self.post_repeating_block_1( + registration_number=123, registration_date=date(2023, 1, 1) + ) + self.previous() # return to previous repeating block + self.previous() # return to edit block + self.previous() # return to list collector + + # Add second company - complete + self.add_company_and_repeating_blocks( + company_name="Company2", + registration_number=456, + registration_date=date(2023, 2, 2), + trader_uk="Yes", + ) + + # Add third company - only the add block + self.add_company_from_list_collector(company_name="Company3") + self.click_cancel_link() # Return to edit block + self.click_cancel_link() # Return to the list collector + + # Add fourth company - complete + self.add_company_and_repeating_blocks( + company_name="Company4", + registration_number=101, + registration_date=date(2023, 4, 4), + trader_uk="No", + ) + + # Assert completeness + self.assert_company_incomplete(1, 1) + self.assert_company_completed(2, 2) + self.assert_company_incomplete(3, 3) + self.assert_company_completed(4, 4) + + # Attempt to move along path after list collector - will route to first incomplete block of first incomplete item + self.post({"any-other-companies-or-branches-answer": "No"}) + + # Should be routed to incomplete block 2 of item 1 + self.post_repeating_block_2(trader_uk="Yes", trader_eu="Yes") + self.assert_company_completed(1, 1) + self.post({"any-other-companies-or-branches-answer": "No"}) + + # Should be routed to incomplete block 1 of item 3 + self.post_repeating_block_1( + registration_number=789, registration_date=date(2023, 3, 3) + ) + self.post_repeating_block_2(trader_uk="Yes", trader_eu="No") + self.assert_company_completed(3, 3) + + # Can now submit the survey as all items are complete + self.post({"any-other-companies-or-branches-answer": "No"}) + self.post({"any-other-trading-details-answer": "No other details"}) + self.post() + self.post() + self.assertInBody( + "Thank you for completing the Test a List Collector with Repeating Blocks and Section Summary Items" + ) + + def test_adding_from_the_summary_page_adds_the_return_to_param_to_the_url( + self, + ): + self.launch_repeating_blocks_test_survey() + + # Add first company + self.add_company_and_repeating_blocks( + company_name="Company1", + registration_number=123, + registration_date=date(2023, 1, 1), + trader_uk="Yes", + trader_eu="Yes", + is_driving=True, + ) + + # Assert list items complete + self.assert_company_completed(1, 1) + + # Navigate to section summary + self.post({"any-other-companies-or-branches-answer": "No"}) + self.post({"any-other-trading-details-answer": "No other details"}) + self.assertInUrl("/sections/section-companies/") + + # Add second company - from section summary + self.click_add_link() + self.assertInUrl("?return_to=section-summary") + self.add_company(company_name="Company2") + self.post_repeating_block_1( + registration_number=456, registration_date=date(2023, 2, 2) + ) + self.post_repeating_block_2(trader_uk="Yes", trader_eu="No") + self.assert_company_completed(2, 2) + + # Navigate to the submit page summary + self.post({"any-other-companies-or-branches-answer": "No"}) + self.post() + self.assertInUrl("/submit/") + self.click_add_link() + self.assertInUrl("?return_to=final-summary") + + # Add third company - from submit summary + self.add_company(company_name="Company3") + self.post_repeating_block_1( + registration_number=789, registration_date=date(2023, 3, 3) + ) + self.post_repeating_block_2(trader_uk="No") + self.assert_company_completed(3, 3) + + # Submit + self.post({"any-other-companies-or-branches-answer": "No"}) + self.post({"any-other-trading-details-answer": "No other details"}) + self.post() + self.post() + self.assertInBody( + "Thank you for completing the Test a List Collector with Repeating Blocks and Section Summary Items" + ) + + def test_removing_from_the_summary_page_adds_the_return_to_param_to_the_url( + self, + ): + self.launch_repeating_blocks_test_survey() + + # Add some items to the list + self.add_three_companies() + + # Navigate to section summary + self.post({"any-other-companies-or-branches-answer": "No"}) + self.post({"any-other-trading-details-answer": "No other details"}) + + # Remove item 3 + remove_link = self.get_list_item_link("remove", 3) + self.get(remove_link) + self.assertInUrl("?return_to=section-summary") + self.assertInBody("Are you sure you want to remove this company or UK branch?") + self.post({"remove-confirmation": "Yes"}) + + # Navigate to submit summary + self.assertInUrl("/sections/section-companies/") + self.post() + + # Remove item 2 + remove_link = self.get_list_item_link("remove", 2) + self.get(remove_link) + self.assertInUrl("?return_to=final-summary") + self.assertInBody("Are you sure you want to remove this company or UK branch?") + self.post({"remove-confirmation": "Yes"}) + + # Submit + self.post() + self.post() + self.assertInBody( + "Thank you for completing the Test a List Collector with Repeating Blocks and Section Summary Items" + ) + + def test_edit_repeating_block_from_the_summary_page_adds_the_return_to_param_to_the_url( + self, + ): + self.launch_repeating_blocks_test_survey() + + # Add some items to the list + self.add_three_companies() + + # Navigate to section summary + self.post({"any-other-companies-or-branches-answer": "No"}) + self.post({"any-other-trading-details-answer": "No other details"}) + self.assertInUrl("/sections/section-companies/") + + # Edit item 3 + self.click_edit_link("registration-number", 2) + self.assertInUrl("?return_to=section-summary") + self.post_repeating_block_1( + registration_number=000, registration_date=date(2023, 5, 5) + ) + self.assertInUrl("/sections/section-companies/") + + # Remove item 2 + self.click_edit_link("authorised-trader-uk-radio", 2) + self.assertInUrl("?return_to=section-summary") + self.post_repeating_block_2(trader_uk="No", trader_eu="No") + self.assertInUrl("/sections/section-companies/") + + # Submit + self.post() + self.post() + self.assertInBody( + "Thank you for completing the Test a List Collector with Repeating Blocks and Section Summary Items" + ) + + def test_edit_incomplete_repeating_block_from_summary_page_routes_to_next_repeating_block( + self, + ): + self.launch_repeating_blocks_test_survey() + + # add incomplete item with answers for first repeating block + self.add_company_from_list_collector(company_name="Company4", is_driving=True) + self.post_repeating_block_1( + registration_number=123, registration_date=date(2023, 1, 1) + ) + + # go back to edit block and change the name + self.click_cancel_link() + self.click_cancel_link() + self.assertInUrl("/edit-company/") + self.post({"company-or-branch-name": "Company5"}) + + # should jump straight to repeating block 2 as this is the first incomplete one + self.assertInUrl("/companies-repeating-block-2/") + + def test_adding_incomplete_list_item_from_summary_returns_to_list_collector_not_summary( + self, + ): + self.launch_repeating_blocks_test_survey() + + # Add some items to the list + self.add_three_companies() + + # Navigate to section summary + self.post({"any-other-companies-or-branches-answer": "No"}) + self.post({"any-other-trading-details-answer": "No other details"}) + self.assertInUrl("/sections/section-companies/") + + # Add incomplete item from section summary add link + self.click_add_link() + self.add_company(company_name="Company4") + self.click_cancel_link() # cancel and go back to edit block + self.click_cancel_link() # cancel and go back to list collector + self.assertInUrl("/any-other-companies-or-branches/") + self.assert_company_incomplete(4, 4) + + # Complete the incomplete item + self.post({"any-other-companies-or-branches-answer": "No"}) + self.post_repeating_block_1( + registration_number=101, registration_date=date(2023, 4, 4) + ) + self.post_repeating_block_2(trader_uk="Yes", trader_eu="Yes") + self.assert_company_completed(4, 4) + + # Navigate to submit summary + self.post({"any-other-companies-or-branches-answer": "No"}) + self.post() + + # Add incomplete item from submit summary add link + self.click_add_link() + self.add_company(company_name="Company5") + self.click_cancel_link() + self.click_cancel_link() + self.assertInUrl("/any-other-companies-or-branches/") + self.assert_company_incomplete(5, 5) + + # Complete the incomplete item + self.post({"any-other-companies-or-branches-answer": "No"}) + self.post_repeating_block_1( + registration_number=121, registration_date=date(2023, 5, 5) + ) + self.post_repeating_block_2(trader_uk="No", trader_eu="No") + self.assert_company_completed(5, 5) + + # Submit + self.post({"any-other-companies-or-branches-answer": "No"}) + self.post() + self.post() + self.assertInBody( + "Thank you for completing the Test a List Collector with Repeating Blocks and Section Summary Items" + ) + + def test_edit_repeating_block_from_submit_page_returns_to_submit_page( + self, + ): + self.launch_repeating_blocks_test_survey() + + # Add some items and progress to submit page + self.add_three_companies() + self.post({"any-other-companies-or-branches-answer": "No"}) + self.post({"any-other-trading-details-answer": "No other details"}) + self.post() + self.assertInUrl("/submit/") + + # click an edit link + self.click_edit_link("registration-number", 2) + + # previous link should return to submit page + self.previous() + self.assertInUrl("/submit/") + + def test_previous_link_in_two_list_collectors_with_repeating_blocks_returns_to_previous_location( + self, + ): + self.launchSurveyV2(schema_name="test_list_collector_repeating_blocks_with_hub") + self.post({"responsible-party-answer": "Yes"}) + + # Add some items to first list collector in first section + self.add_three_companies() + self.post({"any-other-companies-or-branches-answer": "No"}) + self.post({"any-other-trading-details-answer": "No other details"}) + + # Progress through the first section summary and the hub to second section + self.post() + self.post() + + # Answer initial questions in the second section + self.post({"responsible-party-business-answer": "Yes"}) + self.post({"any-businesses-or-branches-answer": "Yes"}) + + # Add item name using second list collector + self.post({"business-or-branch-name": "Business1"}) + + # Navigate back using previous link + self.previous() + + # previous link should return to second list collector's edit first item page + self.assertInUrl("/edit-business/") diff --git a/tests/integration/questionnaire/test_questionnaire_locale.py b/tests/integration/questionnaire/test_questionnaire_locale.py new file mode 100644 index 0000000000..c745c35cbc --- /dev/null +++ b/tests/integration/questionnaire/test_questionnaire_locale.py @@ -0,0 +1,21 @@ +from tests.integration.integration_test_case import IntegrationTestCase + + +class TestSession(IntegrationTestCase): + def test_none_language_code_defaults_to_en(self): + # Given a questionnaire with an answer of type mass-tonne launches with no language code in the runner claims + token = self.token_generator.create_token_with_none_language_code( + "test_unit_patterns" + ) + self.get(url=f"/session?token={token}") + + # Skip to the mass-tonnes answer + self.post() + self.post() + self.post() + self.post() + + # Then the cookie_session[language_code] will have defaulted to DEFAULT_LANGUAGE_CODE (en), and the tooltip will show "tonnes" not "metric tons" + self.assertInBody("Weight Units") + self.assertInSelector("tonnes", "[id='mass-tonne-type']") + self.assertNotInSelector("metric tons", "[id='mass-tonne-type']") diff --git a/tests/integration/questionnaire/test_questionnaire_page_titles.py b/tests/integration/questionnaire/test_questionnaire_page_titles.py index 9b296ba069..8468cfb7cc 100644 --- a/tests/integration/questionnaire/test_questionnaire_page_titles.py +++ b/tests/integration/questionnaire/test_questionnaire_page_titles.py @@ -4,13 +4,13 @@ class TestQuestionnairePageTitles(IntegrationTestCase): def test_introduction_has_introduction_in_page_title(self): # Given, When - self.launchSurvey("test_submit_with_custom_submission_text") + self.launchSurveyV2(schema_name="test_submit_with_custom_submission_text") # Then self.assertEqualPageTitle("Introduction - Submit without summary") def test_should_have_question_in_page_title_when_loading_questionnaire(self): # Given - self.launchSurvey("test_submit_with_custom_submission_text") + self.launchSurveyV2(schema_name="test_submit_with_custom_submission_text") # When self.post(action="start_questionnaire") # Then @@ -20,7 +20,7 @@ def test_should_have_question_in_page_title_when_loading_questionnaire(self): def test_should_have_question_in_page_title_on_submit_page(self): # Given - self.launchSurvey("test_submit_with_custom_submission_text") + self.launchSurveyV2(schema_name="test_submit_with_custom_submission_text") # When self.post(action="start_questionnaire") self.post({"breakfast-answer": ""}) @@ -29,9 +29,10 @@ def test_should_have_question_in_page_title_on_submit_page(self): def test_should_have_question_in_page_title_on_submit_page_with_summary(self): # Given - self.launchSurvey("test_percentage") + self.launchSurveyV2(schema_name="test_percentage") # When self.post({"answer": ""}) + self.post({"answer-decimal": ""}) # Then self.assertEqualPageTitle( "Check your answers and submit - Percentage Field Demo" @@ -39,7 +40,7 @@ def test_should_have_question_in_page_title_on_submit_page_with_summary(self): def test_should_have_survey_in_page_title_on_thank_you(self): # Given - self.launchSurvey("test_submit_with_custom_submission_text") + self.launchSurveyV2(schema_name="test_submit_with_custom_submission_text") self.post(action="start_questionnaire") self.post({"breakfast-answer": ""}) # When submit @@ -51,7 +52,7 @@ def test_should_have_survey_in_page_title_on_thank_you(self): def test_session_timed_out_page_title(self): # Given - self.launchSurvey("test_submit_with_custom_submission_text") + self.launchSurveyV2(schema_name="test_submit_with_custom_submission_text") # When self.get("/session-expired") # Then @@ -59,7 +60,7 @@ def test_session_timed_out_page_title(self): def test_should_have_content_title_in_page_title_on_interstitial(self): # Given - self.launchSurvey("test_interstitial_page") + self.launchSurveyV2(schema_name="test_interstitial_page") self.post(action="start_questionnaire") # When self.post({"favourite-breakfast": ""}) @@ -68,14 +69,14 @@ def test_should_have_content_title_in_page_title_on_interstitial(self): def test_html_stripped_from_page_titles(self): # Given - self.launchSurvey("test_markup") + self.launchSurveyV2(schema_name="test_markup") # When # Then self.assertEqualPageTitle("This is a title with emphasis - Markup test") def test_should_have_question_title_in_page_title_on_question(self): # Given - self.launchSurvey("test_checkbox") + self.launchSurveyV2(schema_name="test_checkbox") # When # Then self.assertEqualPageTitle( @@ -84,8 +85,9 @@ def test_should_have_question_title_in_page_title_on_question(self): def test_should_not_use_names_in_question_page_titles(self): # Given - self.launchSurvey( - "test_placeholder_full", display_address="68 Abingdon Road, Goathill" + self.launchSurveyV2( + schema_name="test_placeholder_full", + display_address="68 Abingdon Road, Goathill", ) # When self.post({"first-name": "Kevin", "last-name": "Bacon"}) @@ -96,16 +98,14 @@ def test_content_page_should_use_nested_content_text_in_page_title_if_it_exists( self, ): # Given - self.launchSurvey("test_interstitial_page_title") + self.launchSurveyV2(schema_name="test_interstitial_page_title") # When # Then - self.assertEqualPageTitle( - "This is the content title â€Ļ - Interstitial Page Titles" - ) + self.assertEqualPageTitle("Your RU name: â€Ļ - Interstitial Page Titles") def test_should_have_error_in_page_title_when_fail_validation(self): # Given - self.launchSurvey("test_checkbox") + self.launchSurveyV2(schema_name="test_checkbox") # When self.post() # Then diff --git a/tests/integration/questionnaire/test_questionnaire_piping.py b/tests/integration/questionnaire/test_questionnaire_piping.py index f9a38b351c..a4f54ff23f 100644 --- a/tests/integration/questionnaire/test_questionnaire_piping.py +++ b/tests/integration/questionnaire/test_questionnaire_piping.py @@ -6,7 +6,7 @@ def test_given_quotes_in_answer_when_piped_into_page_then_html_escaped_quotes_on self, ): # Given - self.launchSurvey("test_multiple_piping") + self.launchSurveyV2(schema_name="test_multiple_piping") self.post(action="start_questionnaire") self.post({"address-line-1": "44 hill side"}) self.post() @@ -21,13 +21,13 @@ def test_given_quotes_in_answer_when_piped_into_page_then_html_escaped_quotes_on # Using raw response data rather than assertInSelectorCSS as otherwise the # content will be unescaped by BeautifulSoup assert ( - "Does Joe Bloggs "Junior" live at 44 hill side" + "Does Joe Bloggs "Junior" live at 44 hill side" in self.getResponseData() ) def test_given_html_in_answer_when_piped_into_page_then_html_escaped_on_page(self): # Given - self.launchSurvey("test_multiple_piping") + self.launchSurveyV2(schema_name="test_multiple_piping") self.post(action="start_questionnaire") self.post({"address-line-1": "44 hill side"}) self.post() @@ -42,7 +42,7 @@ def test_given_html_in_answer_when_piped_into_page_then_html_escaped_on_page(sel # Using raw response data rather than assertInSelectorCSS as otherwise the # content will be unescaped by BeautifulSoup assert ( - "Does Joe Bloggs <b>Junior</b> live at 44 hill side" + "Does Joe Bloggs <b>Junior</b> live at 44 hill side" in self.getResponseData() ) @@ -50,7 +50,7 @@ def test_given_backslash_in_answer_when_piped_into_page_then_backslash_on_page( self, ): # Given - self.launchSurvey("test_multiple_piping") + self.launchSurveyV2(schema_name="test_multiple_piping") self.post(action="start_questionnaire") self.post({"address-line-1": "44 hill side"}) self.post() @@ -66,7 +66,7 @@ def test_given_backslash_in_answer_when_piped_into_page_then_backslash_on_page( def test_answer_piped_into_option(self): # Given - self.launchSurvey("test_multiple_piping") + self.launchSurveyV2(schema_name="test_multiple_piping") self.post(action="start_questionnaire") self.post({"address-line-1": "44 hill side", "town-city": "newport"}) self.post() @@ -87,7 +87,7 @@ def test_answer_piped_into_option_on_validation_error(self): the option label on the form it is rendered with a validation error """ # Given - self.launchSurvey("test_multiple_piping") + self.launchSurveyV2(schema_name="test_multiple_piping") self.post(action="start_questionnaire") self.post({"address-line-1": "44 hill side", "town-city": "newport"}) self.post() diff --git a/tests/integration/questionnaire/test_questionnaire_placeholders.py b/tests/integration/questionnaire/test_questionnaire_placeholders.py index 21069b0012..f22e227f40 100644 --- a/tests/integration/questionnaire/test_questionnaire_placeholders.py +++ b/tests/integration/questionnaire/test_questionnaire_placeholders.py @@ -4,8 +4,9 @@ class TestPlaceholders(IntegrationTestCase): def test_title_placeholders_rendered_in_summary(self): - self.launchSurvey( - "test_placeholder_full", display_address="68 Abingdon Road, Goathill" + self.launchSurveyV2( + schema_name="test_placeholder_full", + display_address="68 Abingdon Road, Goathill", ) self.assertInBody("Please enter a name") self.post({"first-name": "Kevin", "last-name": "Bacon"}) @@ -33,24 +34,32 @@ def test_title_placeholders_rendered_in_summary(self): self.assertInBody("68 Abingdon Road, Goathill") def test_placeholders_rendered_in_pages(self): - self.launchSurvey("test_placeholder_transform") + self.launchSurveyV2(schema_name="test_placeholder_transform") self.assertInBody( "For Integration Testing (Integration Tests), please enter the total retail turnover" ) self.post({"total-retail-turnover-answer": 2000}) self.assertInBody( - "Of the ÂŖ2,000.00 total retail turnover, what was the value of internet sales?" + "Of the ÂŖ2,000.00 total retail turnover, what was the value of internet sales?" ) self.post({"total-retail-turnover-internet-sales-answer": 3000}) self.post({"total-items-answer": 2}) - self.assertInBody("Do you want to add a 3rd item?") + self.assertInBody("Do you want to add a 3rd item?") self.post({"add-item-question": "Yes"}) + self.post({"training-percentage": 1}) + + self.post() + + self.post({"average-distance": 1}) + + self.post() + self.assertInUrl(SUBMIT_URL_PATH) self.assertInBody( "For Integration Testing (Integration Tests), please enter the total retail turnover" @@ -76,13 +85,21 @@ def test_conditional_trad_as_without_trad_as_in_token(self): self.post({"add-item-question": "No"}) + self.post({"training-percentage": 1}) + + self.post() + + self.post({"average-distance": 1}) + + self.post() + self.assertInUrl(SUBMIT_URL_PATH) self.assertInBody( "For Integration Testing, please enter the total retail turnover" ) def test_placeholder_address_selector_rendered_in_page(self): - self.launchSurvey("test_address") + self.launchSurveyV2(schema_name="test_address") self.post( { diff --git a/tests/integration/questionnaire/test_questionnaire_plurals.py b/tests/integration/questionnaire/test_questionnaire_plurals.py index 1c022c18a1..aa53c5cd71 100644 --- a/tests/integration/questionnaire/test_questionnaire_plurals.py +++ b/tests/integration/questionnaire/test_questionnaire_plurals.py @@ -3,7 +3,7 @@ class TestQuestionnairePlurals(IntegrationTestCase): def test_plural_page(self): - self.launchSurvey("test_plural_forms") + self.launchSurveyV2(schema_name="test_plural_forms") self.post({"number-of-people-answer": 0}) diff --git a/tests/integration/questionnaire/test_questionnaire_previous_link.py b/tests/integration/questionnaire/test_questionnaire_previous_link.py index ce571d70d7..d235438309 100644 --- a/tests/integration/questionnaire/test_questionnaire_previous_link.py +++ b/tests/integration/questionnaire/test_questionnaire_previous_link.py @@ -5,14 +5,14 @@ class TestQuestionnairePreviousLink(IntegrationTestCase): def test_previous_link_doesnt_appear_on_introduction(self): # Given - self.launchSurvey("test_submit_with_custom_submission_text") + self.launchSurveyV2(schema_name="test_submit_with_custom_submission_text") # When we open the introduction # Then previous link does not appear self.assertNotInBody("Previous") def test_previous_link_appears_on_the_submit_page(self): # Given - self.launchSurvey("test_submit_with_custom_submission_text") + self.launchSurveyV2(schema_name="test_submit_with_custom_submission_text") # When we proceed through the questionnaire self.post(action="start_questionnaire") @@ -23,7 +23,7 @@ def test_previous_link_appears_on_the_submit_page(self): def test_previous_link_appears_on_the_submit_page_with_summary(self): # Given - self.launchSurvey("test_submit_with_summary") + self.launchSurveyV2(schema_name="test_submit_with_summary") # When we proceed through the questionnaire self.post() @@ -35,7 +35,7 @@ def test_previous_link_appears_on_the_submit_page_with_summary(self): def test_previous_link_doesnt_appear_on_thank_you(self): # Given - self.launchSurvey("test_submit_with_custom_submission_text") + self.launchSurveyV2(schema_name="test_submit_with_custom_submission_text") # When ee proceed through the questionnaire self.post(action="start_questionnaire") @@ -46,7 +46,7 @@ def test_previous_link_doesnt_appear_on_thank_you(self): def test_previous_link_appears_on_questions_preceded_by_another_question(self): # Given a survey with multiple questions - self.launchSurvey("test_checkbox") + self.launchSurveyV2(schema_name="test_checkbox") # When I answer a question self.assertInUrl("mandatory-checkbox") @@ -57,7 +57,7 @@ def test_previous_link_appears_on_questions_preceded_by_another_question(self): def test_previous_link_appears_on_the_first_question_preceded_by_the_hub(self): # Given a survey with a hub enabled - self.launchSurvey("test_hub_and_spoke") + self.launchSurveyV2(schema_name="test_hub_and_spoke") # When I answer go to the first question in a section self.assertInUrl("/") diff --git a/tests/integration/questionnaire/test_questionnaire_progress_value_source_blocks.py b/tests/integration/questionnaire/test_questionnaire_progress_value_source_blocks.py new file mode 100644 index 0000000000..35facdfac3 --- /dev/null +++ b/tests/integration/questionnaire/test_questionnaire_progress_value_source_blocks.py @@ -0,0 +1,208 @@ +from tests.integration.integration_test_case import IntegrationTestCase +from tests.integration.questionnaire import SUBMIT_URL_PATH + + +class TestQuestionnaireProgressValueSourceBlocks(IntegrationTestCase): + def go_to_section(self, section_id): + self.get(f"/questionnaire/sections/{section_id}/") + + def test_skip_condition_block_not_complete(self): + """ + Test that a block is skipped if the progress value source is not complete + """ + + self.launchSurveyV2(schema_name="test_progress_value_source_blocks") + + self.assertInBody("Section 1 Question 1") + self.post({"s1-b1-q1-a1": 0}) + + # Routes to block 3 because answer to block 1 is 0 + self.assertInBody("Section 1 Question 3") + self.post({"s1-b3-q1-a1": 1}) + + # Block 4 is skipped because block 2 is not complete + self.assertInBody("Section 1 Question 5") + + def test_routing_condition_block_not_complete(self): + """ + Test that routes to proper block if the progress value source is not complete + """ + + self.launchSurveyV2(schema_name="test_progress_value_source_blocks") + + self.assertInBody("Section 1 Question 1") + self.post({"s1-b1-q1-a1": 0}) + + # Routes to block 3 because answer to block 1 is 0 + self.assertInBody("Section 1 Question 3") + self.post({"s1-b3-q1-a1": 1}) + + # Block 4 is skipped because block 2 is not complete + self.assertInBody("Section 1 Question 5") + self.post({"s1-b5-q1-a1": 1}) + + # Routes to block 7 because answer to block 2 is not complete + self.assertInBody("Section 1 Question 7") + self.post({"s1-b7-q1-a1": 1}) + + def test_block_value_source_dependencies_updated(self): + """ + Test that the block value source dependencies are updated when a dependent block progress changes + """ + + self.launchSurveyV2(schema_name="test_progress_value_source_blocks") + + self.assertInBody("Section 1 Question 1") + self.post({"s1-b1-q1-a1": 0}) + + # Routes to block 3 because answer to block 1 is 0 + self.assertInBody("Section 1 Question 3") + self.post({"s1-b3-q1-a1": 1}) + + # Block 4 is skipped because block 2 is not complete + self.assertInBody("Section 1 Question 5") + self.post({"s1-b5-q1-a1": 1}) + + # Routes to block 7 because answer to block 2 is not complete + self.assertInBody("Section 1 Question 7") + self.post({"s1-b7-q1-a1": 1}) + + self.assertInBody("Check your answers and submit") + + # Change block 1 answer to 1 + self.get( + "/questionnaire/s1-b1/?return_to=final-summary&return_to_answer_id=s1-b1-q1-a1#s1-b1-q1-a1" + ) + self.assertInBody("Section 1 Question 1") + self.post({"s1-b1-q1-a1": 1}) + + # Routes to block 2 because answer to block 1 is now 1 + self.assertInBody("Section 1 Question 2") + self.post({"s1-b2-q1-a1": 1}) + + # Routes to block 4 as it is the next incomplete block and no longer skipped because answer to block 1 is now 1. + self.assertInBody("Section 1 Question 4") + self.post({"s1-b4-q1-a1": 1}) + + # Routes to block 6 as its the next incomplete block and no longer skipped because block 4 is answered + self.assertInBody("Section 1 Question 6") + self.post({"s1-b6-q1-a1": 1}) + + # Redirects to the hub + self.assertInUrl(SUBMIT_URL_PATH) + + # Question 2, 4 and 6 are now visible because block 2 is complete (dependencies updated) + + self.assertInSelector("Section 1 Question 2", self.row_selector(2)) + self.assertInSelector("1", self.row_selector(2)) + self.assertInSelector("Change", self.row_selector(2)) + + self.assertInSelector("Section 1 Question 4", self.row_selector(4)) + self.assertInSelector("1", self.row_selector(4)) + self.assertInSelector("Change", self.row_selector(4)) + + self.assertInSelector("Section 1 Question 6", self.row_selector(6)) + self.assertInSelector("1", self.row_selector(6)) + self.assertInSelector("Change", self.row_selector(6)) + + def test_block_value_source_dependencies_removed_from_path(self): + """ + Test that the block value source dependencies are updated when a dependent block progress changes and gets removed from path + """ + + self.launchSurveyV2(schema_name="test_progress_value_source_blocks") + + self.assertInBody("Section 1 Question 1") + self.post({"s1-b1-q1-a1": 1}) + + self.assertInBody("Section 1 Question 2") + self.post({"s1-b2-q1-a1": 1}) + + self.assertInBody("Section 1 Question 3") + self.post({"s1-b3-q1-a1": 1}) + + self.assertInBody("Section 1 Question 4") + self.post({"s1-b4-q1-a1": 1}) + + self.assertInBody("Section 1 Question 5") + self.post({"s1-b5-q1-a1": 1}) + + self.assertInBody("Section 1 Question 6") + self.post({"s1-b6-q1-a1": 1}) + + self.assertInBody("Section 1 Question 7") + self.post({"s1-b7-q1-a1": 1}) + + self.assertInBody("Check your answers and submit") + + # Change block 1 answer to 0 + self.get( + "/questionnaire/s1-b1/?return_to=final-summary&return_to_answer_id=s1-b1-q1-a1#s1-b1-q1-a1" + ) + self.assertInBody("Section 1 Question 1") + self.post({"s1-b1-q1-a1": 0}) + + # Redirects to the hub + self.assertInUrl(SUBMIT_URL_PATH) + + # Questions 2, 4 and 6 are not visible because they aren't on the path anymore although they've been answered earlier + self.assertNotInBody("Section 1 Question 2") + self.assertNotInBody("Section 1 Question 4") + self.assertNotInBody("Section 1 Question 6") + + def test_block_value_source_cross_section_dependencies_removed_from_path(self): + """ + Test that the block value source dependencies are updated when a dependent block progress changes and gets removed from path + """ + + self.launchSurveyV2( + schema_name="test_progress_value_source_blocks_cross_section" + ) + + self.post() + + self.assertInBody("Section 1 Question 1") + self.post({"s1-b1-q1-a1": 1}) + + self.assertInBody("Section 1 Question 2") + self.post({"s1-b2-q1-a1": 1}) + + self.assertInBody("Section 1 Question 3") + self.post({"s1-b3-q1-a1": 1}) + + self.assertInBody("Section 1 Question 4") + self.post({"s1-b4-q1-a1": 1}) + + self.post() + self.post() + + self.assertInBody("Section 2 Question 5") + self.post({"s2-b5-q1-a1": 1}) + + self.assertInBody("Section 2 Question 6") + self.post({"s2-b6-q1-a1": 1}) + + self.assertInBody("Section 2 Question 7") + self.post({"s2-b7-q1-a1": 1}) + + self.post() + self.assertEqualUrl("/questionnaire/") + + # Change block 1 answer to 0 + self.get("/questionnaire/s1-b1/") + self.assertInBody("Section 1 Question 1") + self.post({"s1-b1-q1-a1": 0}) + + # Questions 2, 4 in Section 1 are not visible because they aren't on the path anymore + self.assertEqualUrl("/questionnaire/sections/section-1/") + self.assertNotInBody("Section 1 Question 2") + self.assertNotInBody("Section 1 Question 4") + self.post() + + # Redirects to the hub + self.assertEqualUrl("/questionnaire/") + + # Question 6 in Section 2 is not visible because it is not on the path anymore + self.go_to_section("section-2") + self.assertEqualUrl("/questionnaire/sections/section-2/") + self.assertNotInBody("Section 2 Question 6") diff --git a/tests/integration/questionnaire/test_questionnaire_progress_value_source_calculated_summary.py b/tests/integration/questionnaire/test_questionnaire_progress_value_source_calculated_summary.py new file mode 100644 index 0000000000..c1ab6823ec --- /dev/null +++ b/tests/integration/questionnaire/test_questionnaire_progress_value_source_calculated_summary.py @@ -0,0 +1,421 @@ +from tests.integration.integration_test_case import IntegrationTestCase + + +class TestQuestionnaireProgressValueSource(IntegrationTestCase): + def john_doe_link(self): + return self.getHtmlSoup().find("a", {"data-qa": "hub-row-section-3-1-link"})[ + "href" + ] + + def james_bond_link(self): + return self.getHtmlSoup().find("a", {"data-qa": "hub-row-section-3-2-link"})[ + "href" + ] + + def section_one_link(self): + return self.getHtmlSoup().find("a", {"data-qa": "hub-row-section-1-link"})[ + "href" + ] + + def add_person(self, first_name, last_name): + self.assertEqualUrl("/questionnaire/people/add-person/") + self.post({"first-name": first_name, "last-name": last_name}) + + def assert_section_status(self, section_index, status, other_labels=None): + self.assertInSelector(status, self.row_selector(section_index)) + if other_labels and len(other_labels) > 0: + for other_label in other_labels: + self.assertInSelector(other_label, self.row_selector(section_index)) + + def answer_dob(self, payload=None): + if not payload: + payload = { + "date-of-birth-answer-year": "1998", + "date-of-birth-answer-month": "12", + "date-of-birth-answer-day": 12, + } + self.post(payload) + + def go_to_section(self, section_id): + self.get(f"/questionnaire/sections/{section_id}/") + + def go_to_hub(self): + self.get("/questionnaire/") + + # pylint: disable=locally-disabled, too-many-statements + def test_happy_path(self): + self.launchSurveyV2(schema_name="test_progress_value_source_calculated_summary") + + self.assertInBody("Choose another section to complete") + self.assertInBody("Calculated Summary") + self.assertInBody("Skippable random question + List collector") + + # 1. Complete the first section + self.go_to_section("section-1") + self.assertInUrl("/questionnaire/first-number-block/") + self.assertInBody("First Number Question Title") + self.post({"first-number-answer": 1}) + + self.assertInUrl("/questionnaire/second-number-block") + self.assertInBody("Second Number Question Title") + self.post({"second-number-answer": 1}) + + # 2. Should be on the calculated summary page + self.assertEqualUrl("/questionnaire/calculated-summary-block/") + + self.assertInSelector("First answer label", self.row_selector(1)) + self.assertInSelector("ÂŖ1.00", self.row_selector(1)) + self.assertInSelector("Change", self.row_selector(1)) + + self.assertInSelector("Second answer label", self.row_selector(2)) + self.assertInSelector("ÂŖ1.00", self.row_selector(2)) + self.assertInSelector("Change", self.row_selector(2)) + + self.post() + self.post() + + # 3. Should be on the hub page + self.assertEqualUrl("/questionnaire/") + self.assertInBody("Choose another section to complete") + + # 4. 1st section should be marked as complete + self.assert_section_status(1, "Completed", ["View answers"]) + + # 5. Complete the second section + # 6. Random question shows + self.go_to_section("section-2") + self.assertInUrl("/questionnaire/s2-b1/") + self.assertInBody("Skippable random question") + self.post({"s2-b1-q1-a1": 1}) + + # 7. Add two people + self.assertEqualUrl("/questionnaire/list-collector/") + self.post({"anyone-else": "Yes"}) + self.add_person("John", "Doe") + + self.assertEqualUrl("/questionnaire/list-collector/") + self.post({"anyone-else": "Yes"}) + self.add_person("James", "Bond") + + self.post({"anyone-else": "No"}) + + # 8. Two new sections should be available on the hub + self.assert_section_status(3, "Not started", ["John Doe"]) + self.assert_section_status(4, "Not started", ["James Bond"]) + + # 9. Complete the John Doe section, random question shows + self.get(self.john_doe_link()) + + self.assertInBody("John Doe") + + self.answer_dob() + + self.assertInBody("Random question about") + self.post({"other-answer": 1}) + + self.assertInBody("Another random question about") + self.post({"other-answer-2": 1}) + + self.assertInUrl("/questionnaire/sections/section-3/") + + self.post() + + # 10. John Doe section should be marked as complete + self.assert_section_status(3, "Completed", ["John Doe"]) + self.assert_section_status(4, "Not started", ["James Bond"]) + + # 11. Complete the James Bond section, random question shows + self.get(self.james_bond_link()) + + self.assertInBody("James Bond") + + self.answer_dob() + + self.assertInBody("Random question about") + self.post({"other-answer": 1}) + + self.assertInBody("Another random question about") + self.post({"other-answer-2": 1}) + + self.assertInUrl("/questionnaire/sections/section-3/") + + self.post() + + # 12. James Bond section should be marked as complete + self.assert_section_status(4, "Completed", ["James Bond"]) + + # pylint: disable=locally-disabled, too-many-statements + def test_calculated_summary_first_incomplete_then_complete(self): + self.launchSurveyV2(schema_name="test_progress_value_source_calculated_summary") + + # 1. Start completing the first section + self.go_to_section("section-1") + self.post({"first-number-answer": 1}) + + self.post({"second-number-answer": 1}) + + self.assertEqualUrl("/questionnaire/calculated-summary-block/") + + # 2. Go back to the hub BEFORE completing the section + self.go_to_hub() + + # 3. Section 1 should show as partially complete + self.assert_section_status(1, "Partially completed", ["Continue with section"]) + + # 4. Complete the second section + self.go_to_section("section-2") + + self.assertNotInBody("Skippable random question") + self.assertEqualUrl("/questionnaire/list-collector/") + + self.post({"anyone-else": "Yes"}) + self.add_person("John", "Doe") + self.post({"anyone-else": "Yes"}) + self.add_person("James", "Bond") + self.post({"anyone-else": "No"}) + + # 5. Section 2 should show as complete + self.assert_section_status(2, "Completed") + + # 6. Complete the John Doe section. Random question DOES NOT show because section 1 is not complete + self.get(self.john_doe_link()) + + self.answer_dob() + + self.assertNotInBody("Random question about") + self.post() + + self.assertEqualUrl("/questionnaire/") + + # 7. On the hub, John Doe section shows as completted + self.assert_section_status(3, "Completed", ["John Doe"]) + + # 8. Complete the James Bond section. Random question DOES NOT show because section 1 is not complete + self.get(self.james_bond_link()) + + self.assertInBody("James Bond") + + self.answer_dob() + + self.assertNotInBody("Random question about") + self.post() + + self.assertEqualUrl("/questionnaire/") + + # 9. On the hub, James Bond section shows as completed + self.assert_section_status(4, "Completed", ["James Bond"]) + + # 10. Go back to calculated summary (section 1) and complete it + self.get("/questionnaire/calculated-summary-block/?resume=True") + self.assertInBody("We calculate the total of currency values entered to be") + self.post() + self.post() + + # 11. Dependent sections should have been updated to partially completed + self.assert_section_status(1, "Completed") + self.assert_section_status( + 2, "Partially completed", ["Skippable random question + List collector"] + ) + self.assert_section_status(3, "Partially completed", ["John Doe"]) + self.assert_section_status(4, "Partially completed", ["James Bond"]) + + # 12. Go back to section 2 and complete it + # Random question SHOWS because section 1 is now completed + self.go_to_section("section-2") + self.assertInBody("Skippable random question") + self.post({"s2-b1-q1-a1": 1}) + + # 13. Section 2 shows as completed on the hub + self.assertEqualUrl("/questionnaire/") + self.assertInBody("Choose another section to complete") + + self.assert_section_status(2, "Completed") + + # 14. Go back to John Doe section and complete it + # Random questions show because section 1 is now complete + self.get(self.john_doe_link()) + self.assertInBody("Random question about") + self.post({"other-answer": 1}) + self.assertInBody("Another random question about") + self.post({"other-answer-2": 1}) + self.post() + + self.assertEqualUrl("/questionnaire/") + + # 15. Go back to James Bond section and complete it + # Random questions show because section 1 is now complete + self.get(self.james_bond_link()) + self.assertInBody("Random question about") + self.post({"other-answer": 1}) + self.assertInBody("Another random question about") + self.post({"other-answer-2": 1}) + self.post() + + self.assertEqualUrl("/questionnaire/") + + # 16. All sections should show as completed on the hub + self.assert_section_status(3, "Completed", ["John Doe"]) + self.assert_section_status(4, "Completed", ["James Bond"]) + + def test_happy_path_then_make_calculated_summary_incomplete(self): + self.launchSurveyV2(schema_name="test_progress_value_source_calculated_summary") + + # 1. Complete section 1 + self.go_to_section("section-1") + self.post({"first-number-answer": 1}) + + self.post({"second-number-answer": 1}) + + self.post() + self.post() + + # 2. Complete section 2 and add two people + self.go_to_section("section-2") + self.post({"s2-b1-q1-a1": 1}) + + self.post({"anyone-else": "Yes"}) + self.add_person("John", "Doe") + + self.post({"anyone-else": "Yes"}) + self.add_person("James", "Bond") + + self.post({"anyone-else": "No"}) + + # 3. Complete John Doe section + self.get(self.john_doe_link()) + + self.answer_dob() + + self.post({"other-answer": 1}) + + self.post({"other-answer-2": 1}) + + self.post() + + # 4. Complete James Bond section + self.get(self.james_bond_link()) + + self.answer_dob() + + self.post({"other-answer": 1}) + + self.post({"other-answer-2": 1}) + + self.post() + + # END OF HAPPY PATH + + # 5. Go back to calculated summary and make it incomplete + self.get("/questionnaire/calculated-summary-block/") + # Edit first answer + first_answer_link = self.getHtmlSoup().find( + "a", {"data-qa": "first-number-answer-edit"} + )["href"] + self.get(first_answer_link) + self.post({"first-number-answer": 2}) + + # Don't complete the calculated summary, go back to the hub + self.go_to_hub() + + # 6. Section 1 should show as partially completed on the hub + # Other sections should show as completed + self.assert_section_status(1, "Partially completed") + self.assert_section_status(2, "Completed") + self.assert_section_status(3, "Completed") + self.assert_section_status(4, "Completed") + + self.get("/questionnaire/sections/section-2/") + # No random question + self.assertInBody("Does anyone else live here?") + + self.go_to_hub() + + self.get(self.john_doe_link()) + self.assertNotInBody("Random question about") + + self.go_to_hub() + + self.get(self.james_bond_link()) + self.assertNotInBody("Random question about") + + def test_progress_value_source_with_backward_chained_dependencies(self): + self.launchSurveyV2( + schema_name="test_progress_value_source_calculated_summary_extended" + ) + self.post() + + # 1. Complete section 7 + self.go_to_section("section-7") + self.post({"s7-b3-q1-a1": 1}) + + # Check the section is complete + self.assertEqualUrl("/questionnaire/") + self.assert_section_status(6, "Completed") + + # 2. Complete section 5, will change the status of section 7 to partially completed + self.go_to_section("section-5") + self.post({"s5-b2-q1-a1": 1}) + + # Check the section is complete + self.assertEqualUrl("/questionnaire/") + self.assert_section_status(4, "Completed") + self.assert_section_status(6, "Partially completed") + + # 2. Complete section 1, this will change the status of section 5 to partially completed + # and section 7 should once again be complete + self.go_to_section("section-1") + self.post({"first-number-answer": 1}) + + self.post({"second-number-answer": 1}) + + self.post() + + # Check the section is complete + self.assertEqualUrl("/questionnaire/") + self.assert_section_status(1, "Completed") + self.assert_section_status(4, "Partially completed") + self.assert_section_status(6, "Completed") + + def test_progress_value_source_with_chained_dependencies(self): + self.launchSurveyV2( + schema_name="test_progress_value_source_calculated_summary_extended" + ) + self.post() + + # 1. Complete section 8, 9, 10, 11 and 12 + self.go_to_section("section-12") + self.post({"s12-b2-q1-a1": 1}) + + self.go_to_section("section-11") + self.post({"s11-b2-q1-a1": 1}) + + self.go_to_section("section-8") + self.post({"s8-b3-q1-a1": 1}) + + self.go_to_section("section-9") + self.post({"s9-b2-q1-a1": 1}) + + self.go_to_section("section-10") + self.post({"s10-b2-q1-a1": 1}) + + # Check that sections 8, 9 and 10 are complete, and 11 and 12 are partially complete + self.assertEqualUrl("/questionnaire/") + self.assert_section_status(7, "Completed") + self.assert_section_status(8, "Completed") + self.assert_section_status(9, "Completed") + self.assert_section_status(10, "Partially completed") + self.assert_section_status(11, "Partially completed") + + # 2. Update the second section, this should make sections + self.go_to_section("section-2") + self.post({"s2-b1-q1-a1": 1}) + self.post({"anyone-else": "No"}) + + # Check that section 11 and 12 are complete, and 8, 9 and 10 are partially complete + self.assertEqualUrl("/questionnaire/") + self.assert_section_status(2, "Completed") + self.assert_section_status(7, "Partially completed") + self.assert_section_status(8, "Partially completed") + self.assert_section_status(9, "Partially completed") + self.assert_section_status(10, "Completed") + self.assert_section_status(11, "Completed") diff --git a/tests/integration/questionnaire/test_questionnaire_progress_value_source_in_repeating_sections.py b/tests/integration/questionnaire/test_questionnaire_progress_value_source_in_repeating_sections.py new file mode 100644 index 0000000000..6a31ef4c69 --- /dev/null +++ b/tests/integration/questionnaire/test_questionnaire_progress_value_source_in_repeating_sections.py @@ -0,0 +1,544 @@ +from tests.integration.integration_test_case import IntegrationTestCase + + +class TestQuestionnaireProgressValueSourceInRepeatingSections(IntegrationTestCase): + def answer_dob(self, payload=None): + if not payload: + payload = { + "date-of-birth-answer-year": "1998", + "date-of-birth-answer-month": "12", + "date-of-birth-answer-day": 12, + } + self.post(payload) + + def answer_dob_second_repeat(self, payload=None): + if not payload: + payload = { + "second-date-of-birth-answer-year": "1998", + "second-date-of-birth-answer-month": "12", + "second-date-of-birth-answer-day": 12, + } + self.post(payload) + + def john_doe_link(self): + return self.getHtmlSoup().find("a", {"data-qa": "hub-row-section-2-1-link"})[ + "href" + ] + + def james_bond_link(self): + return self.getHtmlSoup().find("a", {"data-qa": "hub-row-section-2-2-link"})[ + "href" + ] + + def jane_doe_link(self): + return self.getHtmlSoup().find("a", {"data-qa": "hub-row-section-4-1-link"})[ + "href" + ] + + def add_person(self, first_name, last_name): + self.assertInBody("What is the name of the person?") + self.post({"first-name": first_name, "last-name": last_name}) + + def add_person_second_list_collector(self, first_name, last_name): + self.assertInBody("What is the name of the person?") + self.post({"second-first-name": first_name, "second-last-name": last_name}) + + def assert_section_status(self, section_index, status, other_labels=None): + self.assertInSelector(status, self.row_selector(section_index)) + if other_labels and len(other_labels): + for other_label in other_labels: + self.assertInSelector(other_label, self.row_selector(section_index)) + + def go_to_section(self, section_id): + self.get(f"/questionnaire/sections/{section_id}/") + + def go_to_hub(self): + self.get("/questionnaire/") + + def test_disable_block_in_repeating_section_if_block_source_progress_not_completed( + self, + ): + """ + Test that a block inside a repeating section is disabled if the progress value source + from a block in another section is not completed + """ + + self.launchSurveyV2( + schema_name="test_progress_block_value_source_repeating_sections" + ) + + self.assertInBody("Choose another section to complete") + + # 1. First section shows as not started + self.assertInSelector("List collector + random question", self.row_selector(1)) + self.assert_section_status(1, "Not started") + + # 2. Start completing section 1 and add 2 people + # Don't answer the random question enabler + self.go_to_section("section-1") + self.assertInBody("Does anyone else live here?") + self.post({"anyone-else": "Yes"}) + + self.add_person("John", "Doe") + + self.assertInBody("Does anyone else live here?") + self.assertInSelector("John Doe", self.row_selector(1)) + self.post({"anyone-else": "Yes"}) + + self.add_person("James", "Bond") + + self.assertInBody("Does anyone else live here?") + self.assertInSelector("James Bond", self.row_selector(2)) + self.post({"anyone-else": "No"}) + + self.post() + + # 3. Assert random question is there + self.assertInBody("Random question enabler") + + # 4. Go back to the hub and leave random question block incomplete + self.go_to_hub() + + # 5. The two repeating sections should show as "not started" + self.assertInBody("Choose another section to complete") + self.assert_section_status(2, "Not started", ["John Doe"]) + self.assert_section_status(3, "Not started", ["James Bond"]) + + # 5. Complete John Doe section + # Random question doesn't show + self.get(self.john_doe_link()) + self.assertInBody("John Doe") + self.answer_dob() + + # Assert random question not there + self.assertNotInBody("Random question about") + + def test_disable_block_in_repeating_section_if_section_source_progress_not_completed( + self, + ): + """ + Test that a block inside a repeating section is disabled if the progress value source + from a block in another section is not completed + """ + + self.launchSurveyV2( + schema_name="test_progress_section_value_source_repeating_sections" + ) + + self.assertInBody("Choose another section to complete") + + # 1. First section shows as not started + self.assertInSelector("List collector + random question", self.row_selector(1)) + self.assert_section_status(1, "Not started") + + # 2. Start completing section 1 and add 2 people + # Don't answer the random question enabler + self.go_to_section("section-1") + self.assertInBody("Does anyone else live here?") + self.post({"anyone-else": "Yes"}) + + self.add_person("John", "Doe") + + self.assertInBody("Does anyone else live here?") + self.assertInSelector("John Doe", self.row_selector(1)) + self.post({"anyone-else": "Yes"}) + + self.add_person("James", "Bond") + + self.assertInBody("Does anyone else live here?") + self.assertInSelector("James Bond", self.row_selector(2)) + self.post({"anyone-else": "No"}) + + self.post() + + # 3. Assert random question is there + self.assertInBody("Random question enabler") + + # 4. Go back to the hub and leave random question block incomplete + self.go_to_hub() + + # 5. The two repeating sections should show as "not started" + self.assertInBody("Choose another section to complete") + self.assert_section_status(2, "Not started", ["John Doe"]) + self.assert_section_status(3, "Not started", ["James Bond"]) + + # 5. Complete John Doe section + # Random question doesn't show + self.get(self.john_doe_link()) + self.assertInBody("John Doe") + self.answer_dob() + + # Assert random question not there + self.assertNotInBody("Random question about") + + def test_enable_block_in_repeating_section_if_block_source_progress_is_completed( + self, + ): + """ + Test that a block inside a repeating section is enabled if the progress value source + from a block in another section is completeted + """ + + self.launchSurveyV2( + schema_name="test_progress_block_value_source_repeating_sections" + ) + + self.assertInBody("Choose another section to complete") + + # 1. Complete 1st section and add 2 people + # And answer the random question enabler + self.go_to_section("section-1") + self.assertInBody("Does anyone else live here?") + self.post({"anyone-else": "Yes"}) + + self.add_person("John", "Doe") + + self.assertInBody("Does anyone else live here?") + self.assertInSelector("John Doe", self.row_selector(1)) + self.post({"anyone-else": "Yes"}) + + self.add_person("James", "Bond") + + self.assertInBody("Does anyone else live here?") + self.assertInSelector("James Bond", self.row_selector(2)) + self.post({"anyone-else": "No"}) + + self.post() + + self.assertInBody("Random question enabler") + self.post({"random-question-enabler-answer": 1}) + + # Go back to the hub + self.go_to_hub() + self.assertInBody("Choose another section to complete") + + # Assert first section completed + self.assert_section_status(1, "Completed", ["List collector + random question"]) + + # 2. Complete John Doe section + self.get(self.john_doe_link()) + self.assertInBody("John Doe") + self.answer_dob() + + # 3. Assert random question shows up + self.assertInBody("Random question") + + def test_enable_block_in_repeating_section_if_section_source_progress_is_completed( + self, + ): + """ + Test that a block inside a repeating section is enabled if the progress value source + from a block in another section is completeted + """ + self.launchSurveyV2( + schema_name="test_progress_section_value_source_repeating_sections" + ) + + self.assertInBody("Choose another section to complete") + + # 1. Complete 1st section and add 2 people + # And answer the random question enabler + self.go_to_section("section-1") + self.assertInBody("Does anyone else live here?") + self.post({"anyone-else": "Yes"}) + + self.add_person("John", "Doe") + + self.assertInBody("Does anyone else live here?") + self.assertInSelector("John Doe", self.row_selector(1)) + self.post({"anyone-else": "Yes"}) + + self.add_person("James", "Bond") + + self.assertInBody("Does anyone else live here?") + self.assertInSelector("James Bond", self.row_selector(2)) + self.post({"anyone-else": "No"}) + + self.post() + + self.assertInBody("Random question enabler") + self.post({"random-question-enabler-answer": 1}) + + self.post() + + # Go back to the hub + self.go_to_hub() + self.assertInBody("Choose another section to complete") + + # Assert first section completed + self.assert_section_status(1, "Completed", ["List collector + random question"]) + + # 2. Complete John Doe section + self.get(self.john_doe_link()) + self.assertInBody("John Doe") + self.answer_dob() + + # 3. Assert random question shows up + self.assertInBody("Random question") + + def test_block_progress_dependencies_updated_in_repeating_sections(self): + """ + Test that dependency blocks inside repeating sections are updated properly + """ + + self.launchSurveyV2( + schema_name="test_progress_block_value_source_repeating_sections" + ) + + self.assertInBody("Choose another section to complete") + + # 1. Complete 1st section and add 2 people + # Don't answer the random question enabler + self.go_to_section("section-1") + self.assertInBody("Does anyone else live here?") + self.post({"anyone-else": "Yes"}) + + self.add_person("John", "Doe") + + self.assertInBody("Does anyone else live here?") + self.assertInSelector("John Doe", self.row_selector(1)) + self.post({"anyone-else": "Yes"}) + + self.add_person("James", "Bond") + + self.assertInBody("Does anyone else live here?") + self.assertInSelector("James Bond", self.row_selector(2)) + self.post({"anyone-else": "No"}) + + self.post() + + self.assertInBody("Random question enabler") + + # 2. Go back to the hub and leave random question block incomplete + self.go_to_hub() + + # 3. Repeating sections show as "Not started" + self.assertInBody("Choose another section to complete") + self.assert_section_status(2, "Not started", ["John Doe"]) + self.assert_section_status(3, "Not started", ["James Bond"]) + + # 4. Complete John Doe section + self.get(self.john_doe_link()) + self.assertInBody("John Doe") + self.answer_dob() + + # 5. Assert random question not there + self.assertNotInBody("Random question about") + + # 6. Go back to section 1 and complete random question + self.go_to_section("section-1") + self.assertInBody("Random question enabler") + self.post({"random-question-enabler-answer": 1}) + + # 7. Go back to the hub + self.go_to_hub() + + # 8. Assert sections 1 is completed and repeating sections are partially completed + self.assertInBody("Choose another section to complete") + self.assert_section_status(1, "Completed", ["List collector + random question"]) + self.assert_section_status( + 2, "Partially completed", ["John Doe", "Continue with section"] + ) + self.assert_section_status(3, "Not started", ["James Bond"]) + + # 9. Go back to John Doe section + self.get(self.john_doe_link()) + + # 10. Assert prompts for random question + self.assertInBody("Random question about") + self.post({"other-answer": 1}) + + # 12. Assert it goes to the section summary + self.assertInUrl("questionnaire/sections/section-2") + + # 13. Assert answer was provided + self.assertInSelector("Random question about", self.row_selector(2)) + self.assertNotInSelector("No answer provided", self.row_selector(2)) + + # 14. Go back to summary + self.go_to_hub() + + # 15. Assert John Doe section is completed + self.assert_section_status(2, "Completed", ["John Doe"]) + + # 15. Edit James Bond section + self.get(self.james_bond_link()) + self.assertInBody("James Bond") + self.answer_dob() + + # 16. Assert random question shows up + self.assertInBody("Random question about") + + def test_section_progress_dependencies_updated_in_repeating_sections(self): + """ + Test that dependency blocks inside repeating sections are updated properly + """ + + self.launchSurveyV2( + schema_name="test_progress_section_value_source_repeating_sections" + ) + + self.assertInBody("Choose another section to complete") + + # 1. Complete 1st section and add 2 people + # Don't answer the random question enabler + self.go_to_section("section-1") + self.assertInBody("Does anyone else live here?") + self.post({"anyone-else": "Yes"}) + + self.add_person("John", "Doe") + + self.assertInBody("Does anyone else live here?") + self.assertInSelector("John Doe", self.row_selector(1)) + self.post({"anyone-else": "Yes"}) + + self.add_person("James", "Bond") + + self.assertInBody("Does anyone else live here?") + self.assertInSelector("James Bond", self.row_selector(2)) + self.post({"anyone-else": "No"}) + + self.post() + + self.assertInBody("Random question enabler") + + # 2. Go back to the hub and leave random question block incomplete + self.go_to_hub() + + # 3. Repeating sections show as "Not started" + self.assertInBody("Choose another section to complete") + self.assert_section_status(2, "Not started", ["John Doe"]) + self.assert_section_status(3, "Not started", ["James Bond"]) + + # 4. Complete John Doe section + self.get(self.john_doe_link()) + self.assertInBody("John Doe") + self.answer_dob() + + # 5. Assert random question not there + self.assertNotInBody("Random question about") + + # 6. Go back to section 1 and complete random question + self.go_to_section("section-1") + self.assertInBody("Random question enabler") + self.post({"random-question-enabler-answer": 1}) + + # 7. Go back to the hub + self.go_to_hub() + + # 8. Assert sections 1 is completed and repeating sections are partially completed + self.assertInBody("Choose another section to complete") + self.assert_section_status(1, "Completed", ["List collector + random question"]) + self.assert_section_status( + 2, "Partially completed", ["John Doe", "Continue with section"] + ) + self.assert_section_status(3, "Not started", ["James Bond"]) + + # 9. Go back to John Doe section + self.get(self.john_doe_link()) + + # 10. Assert prompts for random question + self.assertInBody("Random question about") + self.post({"other-answer": 1}) + + # 12. Assert it goes to the section summary + self.assertInUrl("questionnaire/sections/section-2") + + # 13. Assert answer was provided + self.assertInSelector("Random question about", self.row_selector(2)) + self.assertNotInSelector("No answer provided", self.row_selector(2)) + + # 14. Go back to summary + self.go_to_hub() + + # 15. Assert John Doe section is completed + self.assert_section_status(2, "Completed", ["John Doe"]) + + # 15. Edit James Bond section + self.get(self.james_bond_link()) + self.assertInBody("James Bond") + self.answer_dob() + + # 16. Assert random question shows up + self.assertInBody("Random question about") + + def test_section_progress_dependencies_updated_in_repeating_sections_with_chained_dependencies( + self, + ): + """ + Test that dependency blocks inside repeating sections are updated properly when there are chained dependencies + """ + self.launchSurveyV2( + schema_name="test_progress_value_source_repeating_sections_chained_dependencies" + ) + + self.assertInBody("Choose another section to complete") + + # 1. Complete 3rd section and add 2 people + # And answer the random question enabler + self.go_to_section("section-3") + self.assertInBody("Does anyone else live here?") + self.post({"second-anyone-else": "Yes"}) + + self.add_person_second_list_collector("Jane", "Doe") + + self.assertInBody("Does anyone else live here?") + self.assertInSelector("Jane Doe", self.row_selector(1)) + self.post({"second-anyone-else": "Yes"}) + + self.add_person_second_list_collector("Marie", "Clare") + + self.assertInBody("Does anyone else live here?") + self.assertInSelector("Marie Clare", self.row_selector(2)) + self.post({"second-anyone-else": "No"}) + + self.post() + + self.assertInBody("Random question enabler") + self.post({"second-random-question-enabler-answer": 1}) + + self.post() + + # Go back to the hub + self.go_to_hub() + + # 2. The two repeating sections should show as "not started" + self.assertInBody("Choose another section to complete") + self.assert_section_status(4, "Not started", ["Jane Doe"]) + self.assert_section_status(5, "Not started", ["Marie Clare"]) + + # 3. Complete Jane Doe section + self.get(self.jane_doe_link()) + self.assertInBody("Jane Doe") + self.answer_dob_second_repeat() + self.post({"second-other-answer": 1}) + + # Go back to the hub + self.go_to_hub() + + # 4. The Jane Doe section should now be Complete + self.assertInBody("Choose another section to complete") + self.assert_section_status(4, "Completed", ["Jane Doe"]) + self.assert_section_status(5, "Not started", ["Marie Clare"]) + + # 5. Complete section 2 + self.go_to_section("section-2") + self.post({"s2-b2-q1-a1": 1}) + + # 6. Section-2 and should be complete but the Jane Doe section should be partially completed + self.assertInBody("Choose another section to complete") + self.assert_section_status(2, "Completed") + self.assert_section_status(4, "Partially completed", ["Jane Doe"]) + self.assert_section_status(5, "Not started", ["Marie Clare"]) + + # 5. Complete section 1 + self.go_to_section("section-1") + self.post({"s1-b1-q1-a1": 1}) + + # 6. Section 1 and the Jane Doe section should now be but complete + # but Section-2 should be partially completed + self.assertInBody("Choose another section to complete") + self.assert_section_status(1, "Completed") + self.assert_section_status(2, "Partially completed") + self.assert_section_status(4, "Completed", ["Jane Doe"]) + self.assert_section_status(5, "Not started", ["Marie Clare"]) diff --git a/tests/integration/questionnaire/test_questionnaire_progress_value_source_section_enabled.py b/tests/integration/questionnaire/test_questionnaire_progress_value_source_section_enabled.py new file mode 100644 index 0000000000..0e8078ab74 --- /dev/null +++ b/tests/integration/questionnaire/test_questionnaire_progress_value_source_section_enabled.py @@ -0,0 +1,184 @@ +from tests.integration.integration_test_case import IntegrationTestCase + + +class TestQuestionnaireProgressValueSource(IntegrationTestCase): + def assert_section_status(self, section_index, status, other_labels=None): + self.assertInSelector(status, self.row_selector(section_index)) + if other_labels and len(other_labels) > 0: + for other_label in other_labels: + self.assertInSelector(other_label, self.row_selector(section_index)) + + def go_to_section(self, section_id): + self.get(f"/questionnaire/sections/{section_id}/") + + def go_to_hub(self): + self.get("/questionnaire/") + + def test_enable_section_by_progress_linear_flow(self): + """ + Test that a section is enabled by progress value source + In a linear flow with no hub + """ + + self.launchSurveyV2( + schema_name="test_progress_value_source_section_enabled_no_hub" + ) + + self.assertInBody("Section 1 Question 1") + self.post({"s1-b1-q1-a1": 1}) + + self.assertInBody("Section 1 Question 2") + self.post({"s1-b2-q1-a1": 1}) + + self.assertInBody("Section 2 Question 1") + self.post({"s2-b1-q1-a1": 1}) + + def test_enable_section_by_progress_hub_flow(self): + """ + Test that a section is enabled by progress value source + In a hub flow + """ + + self.launchSurveyV2( + schema_name="test_progress_value_source_section_enabled_hub" + ) + + # 1. Only section 1 shows on the hub + self.assertInBody("Choose another section to complete") + self.assertInBody("Section 1") + self.assertNotInBody("Section 2") + + # 2. Complete section 1 + self.go_to_section("section-1") + self.assertInBody("Section 1 Question 1") + self.post({"s1-b1-q1-a1": 1}) + + self.assertInUrl("/questionnaire/s1-b2") + self.post({"s1-b2-q1-a1": 1}) + + # 3. Assert section 1 completed on the hub + self.assertEqualUrl("/questionnaire/") + + self.assert_section_status(1, "Completed", ["View answers"]) + + # 4. Assert section 2 is now available on the hub + self.assert_section_status(2, "Not started", ["Start section"]) + + # 5. Complete section 2 + self.go_to_section("section-2") + self.assertInUrl("/questionnaire/s2-b1") + self.post({"s2-b1-q1-a1": 1}) + + self.assertEqualUrl("/questionnaire/") + + # 6. Assert section 2 completed on the hub + self.assert_section_status(1, "Completed", ["View answers"]) + self.assert_section_status(2, "Completed", ["View answers"]) + + self.assertInBody("Submit survey") + + def test_value_source_dependency_enable_section_by_progress_hub_flow(self): + """ + Test that dependencies that rely on a section's progress + are updated when the section progress changes + """ + + self.launchSurveyV2( + schema_name="test_progress_value_source_section_enabled_hub" + ) + + self.assertInBody("Choose another section to complete") + self.assertInBody("Section 1") + self.assertNotInBody("Section 2") + + # 1. Start section 1 + self.go_to_section("section-1") + self.assertInBody("Section 1 Question 1") + self.post({"s1-b1-q1-a1": 1}) + + self.assertInBody("Section 1 Question 2") + + # 2. Leave section 1 incomplete + self.get("/questionnaire/") + + # 3. Assert that section 2 is not enabled + self.assertNotInBody("Section 2") + + # 4. Assert section 1 is in progress + self.assert_section_status(1, "Partially completed", ["Continue with section"]) + + # 5. Go back to section 1 and complete it + self.get("/questionnaire/sections/section-1/?resume=True") + + self.assertInBody("Section 1 Question 2") + self.post({"s1-b2-q1-a1": 1}) + + self.assertEqualUrl("/questionnaire/") + + # 6. Assert section 1 completed on the hub + self.assert_section_status(1, "Completed", ["View answers"]) + + # 7. Assert that section 2 is now enabled + self.assert_section_status(2, "Not started", ["Start section"]) + + def test_enable_section_by_progress_hub_complex_happy_path(self): + self.launchSurveyV2( + schema_name="test_progress_value_source_section_enabled_hub_complex" + ) + + self.assertInBody("Choose another section to complete") + self.assertInBody("Section 1") + self.assertInBody("Section 3") + self.assertNotInBody("Section 2") + + # 1. Complete section 1 + self.go_to_section("section-1") + self.assertInBody("Section 1 Question 1") + self.post({"s1-b1-q1-a1": 1}) + + self.assertInBody("Section 1 Question 2") + self.post({"s1-b2-q1-a1": 1}) + + # 2. Complete section 2 with all the questions + self.assertInBody("Choose another section to complete") + self.assertInBody("Section 2") + self.go_to_section("section-2") + + self.assertInBody("Section 2 Question 1") + self.post({"s2-b1-q1-a1": 1}) + + self.assertInBody("Section 2 Question 2") + self.post({"s2-b2-q1-a1": 0}) + + self.assertInBody("Section 2 Question 3") + self.post({"s2-b3-q1-a1": 0}) + + # 3. Assert section 3 is on the path + self.assertInBody("Choose another section to complete") + self.assertInBody("Section 3") + self.go_to_section("section-3") + self.assertInUrl("/questionnaire/s3-b1") + + # 4. Complete section 3 + self.assertInBody("Section 3 Question 1") + self.post({"s3-b1-q1-a1": 0}) + + # 5. Complete section 4 + self.assertInBody("Choose another section to complete") + self.assertInBody("Section 4") + self.go_to_section("section-4") + self.assertInUrl("/questionnaire/s4-b1") + + self.assertInBody("Section 4 Question 1") + self.post({"s4-b1-q1-a1": 0}) + + # 6. Assert all sections have been completed + + for sel in ( + self.row_selector(1), + self.row_selector(2), + self.row_selector(3), + self.row_selector(4), + ): + self.assertInSelector("Completed", sel) + self.assertInSelector("View answers", sel) diff --git a/tests/integration/questionnaire/test_questionnaire_question_definition.py b/tests/integration/questionnaire/test_questionnaire_question_definition.py index 0a2f0ddce2..83c502a22b 100644 --- a/tests/integration/questionnaire/test_questionnaire_question_definition.py +++ b/tests/integration/questionnaire/test_questionnaire_question_definition.py @@ -5,11 +5,11 @@ class TestQuestionnaireQuestionDefinition(IntegrationTestCase): def test_question_definition(self): # Given I launch a questionnaire with definitions - self.launchSurvey("test_question_definition") + self.launchSurveyV2(schema_name="test_question_definition") # When I start the survey I am presented with the definitions title and content correctly self.assertInBody( - "Do you connect a LiFePO4 battery to your photovoltaic system to store surplus energy?" + "Do you connect a LiFePO4 battery to your photovoltaic system to store surplus energy?" ) self.assertInBody("What is a photovoltaic system?") @@ -19,19 +19,6 @@ def test_question_definition(self): "The mount may be fixed, or use a solar tracker to follow the sun across the sky." ) - self.assertInBody("Why use LiFePO4 batteries?") - self.assertInBody("3 Benefits of LifePO4 batteries.") - self.assertInBody( - "LifePO4 batteries have a life span 10 times longer than that of traditional lead acid batteries. " - "This dramatically reduces the need for battery changes." - ) - self.assertInBody( - "Lithium iron phosphate batteries operate with much lower resistance and consequently recharge at a faster rate." - ) - self.assertInBody( - "LifeP04 lightweight batteries are lighter than lead acid batteries, usually weighing about 1/4 less." - ) - # When we continue we go to the summary page self.post() self.assertInUrl(SUBMIT_URL_PATH) diff --git a/tests/integration/questionnaire/test_questionnaire_question_guidance.py b/tests/integration/questionnaire/test_questionnaire_question_guidance.py index a379bc1afe..5157ea2bc2 100644 --- a/tests/integration/questionnaire/test_questionnaire_question_guidance.py +++ b/tests/integration/questionnaire/test_questionnaire_question_guidance.py @@ -5,7 +5,7 @@ class TestQuestionnaireQuestionGuidance(IntegrationTestCase): def test_question_guidance(self): # Given I launch a questionnaire with various guidance - self.launchSurvey("test_question_guidance") + self.launchSurveyV2(schema_name="test_question_guidance") self.post(action="start_questionnaire") # When I start the survey I am presented with the title guidance correctly diff --git a/tests/integration/questionnaire/test_questionnaire_question_variants.py b/tests/integration/questionnaire/test_questionnaire_question_variants.py index d09eefcb9d..42e4522de1 100644 --- a/tests/integration/questionnaire/test_questionnaire_question_variants.py +++ b/tests/integration/questionnaire/test_questionnaire_question_variants.py @@ -7,27 +7,17 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def test_non_proxy_answer_shows_non_proxy_title(self): - self.launchSurvey("test_variants_question") + self.launchSurveyV2(schema_name="test_variants_question") self.complete_first_section(proxy=False) def test_proxy_answer_shows_proxy_title(self): - self.launchSurvey("test_variants_question") + self.launchSurveyV2(schema_name="test_variants_question") self.complete_first_section(proxy=True) - def test_new_non_proxy_answer_shows_non_proxy_title(self): - self.launchSurvey("test_new_variants_question") - - self.complete_first_section(proxy=False) - - def test_new_proxy_answer_shows_proxy_title(self): - self.launchSurvey("test_new_variants_question") - - self.complete_first_section(proxy=True) - - def test_new_summaries_proxy(self): - self.launchSurvey("test_new_variants_question") + def test_summaries_proxy(self): + self.launchSurveyV2(schema_name="test_variants_question") self.complete_first_section(proxy=True) @@ -53,7 +43,7 @@ def complete_first_section(self, proxy=False): self.post({"first-name-answer": "Linus", "last-name-answer": "Torvalds"}) - self.assertInBody("Are you Linus Torvalds?") + self.assertInBody("Are you Linus Torvalds?") proxy_answer = "No, I am answering on their behalf" if proxy else "Yes, I am" @@ -62,7 +52,9 @@ def complete_first_section(self, proxy=False): self.post({"proxy-answer": proxy_answer}) expected_question = ( - "What age is Linus Torvalds?" if proxy else "What is your age?" + "What age is Linus Torvalds?" + if proxy + else "What is your age?" ) self.assertInBody(expected_question) @@ -70,7 +62,9 @@ def complete_first_section(self, proxy=False): self.post({"age-answer": "49"}) expected_question = ( - "Linus Torvalds is over 16?" if proxy else "You are over 16?" + "Linus Torvalds is over 16?" + if proxy + else "You are over 16?" ) self.assertInBody(expected_question) diff --git a/tests/integration/questionnaire/test_questionnaire_radio_voluntary.py b/tests/integration/questionnaire/test_questionnaire_radio_voluntary.py index fd73f08bf3..e456e10f82 100644 --- a/tests/integration/questionnaire/test_questionnaire_radio_voluntary.py +++ b/tests/integration/questionnaire/test_questionnaire_radio_voluntary.py @@ -5,7 +5,7 @@ class TestQuestionnaireRadioVoluntary(IntegrationTestCase): BASE_URL = "/questionnaire/" def test_radio_voluntary(self): - self.launchSurvey("test_radio_voluntary") + self.launchSurveyV2(schema_name="test_radio_voluntary") self.post({"radio-voluntary-true-answer": "Coffee"}) self.previous() self.post(action="clear_radios") @@ -17,7 +17,7 @@ class TestQuestionnaireRepeatingSectionRadioVoluntary(IntegrationTestCase): BASE_URL = "/questionnaire/" def test_clear_radios(self): - self.launchSurvey("test_radio_voluntary_with_repeating_sections") + self.launchSurveyV2(schema_name="test_radio_voluntary_with_repeating_sections") self.post() self.post({"anyone-lives-here": "Yes"}) self.post({"first-name": "James", "last-name": "May"}) diff --git a/tests/integration/questionnaire/test_questionnaire_redirect_to_list_add_question_action_checkbox.py b/tests/integration/questionnaire/test_questionnaire_redirect_to_list_add_question_action_checkbox.py index 05b92b001a..b6e3e2ff23 100644 --- a/tests/integration/questionnaire/test_questionnaire_redirect_to_list_add_question_action_checkbox.py +++ b/tests/integration/questionnaire/test_questionnaire_redirect_to_list_add_question_action_checkbox.py @@ -1,4 +1,4 @@ -from . import QuestionnaireTestCase +from tests.integration.questionnaire import QuestionnaireTestCase class TestQuestionnaireListCollector(QuestionnaireTestCase): @@ -6,7 +6,9 @@ def test_add_list_question_displayed_before_list_collector_and_return_to_in_url( self, ): # Given - self.launchSurvey("test_answer_action_redirect_to_list_add_block_checkbox") + self.launchSurveyV2( + schema_name="test_answer_action_redirect_to_list_add_block_checkbox" + ) # When self.post({"anyone-usually-live-at-answer": ["I think so", "No"]}) @@ -18,7 +20,9 @@ def test_add_list_question_displayed_before_list_collector_and_return_to_in_url( def test_previous_link_when_list_empty_with_return_to_query_string(self): # Given - self.launchSurvey("test_answer_action_redirect_to_list_add_block_checkbox") + self.launchSurveyV2( + schema_name="test_answer_action_redirect_to_list_add_block_checkbox" + ) self.post({"anyone-usually-live-at-answer": ["I think so", "No"]}) # When @@ -29,7 +33,9 @@ def test_previous_link_when_list_empty_with_return_to_query_string(self): def test_previous_link_when_list_not_empty(self): # Given - self.launchSurvey("test_answer_action_redirect_to_list_add_block_checkbox") + self.launchSurveyV2( + schema_name="test_answer_action_redirect_to_list_add_block_checkbox" + ) self.post({"anyone-usually-live-at-answer": ["I think so", "No"]}) self.add_person("John", "Doe") self.post({"anyone-else-live-at-answer": "Yes"}) @@ -44,7 +50,9 @@ def test_previous_link_return_to_list_collector_when_invalid_return_to_block_id( self, ): # Given - self.launchSurvey("test_answer_action_redirect_to_list_add_block_checkbox") + self.launchSurveyV2( + schema_name="test_answer_action_redirect_to_list_add_block_checkbox" + ) self.post({"anyone-usually-live-at-answer": ["I think so"]}) url_with_invalid_return_to = self.last_url + "-invalid" diff --git a/tests/integration/questionnaire/test_questionnaire_redirect_to_list_add_question_action_radio.py b/tests/integration/questionnaire/test_questionnaire_redirect_to_list_add_question_action_radio.py index d47564c80b..09030e7f13 100644 --- a/tests/integration/questionnaire/test_questionnaire_redirect_to_list_add_question_action_radio.py +++ b/tests/integration/questionnaire/test_questionnaire_redirect_to_list_add_question_action_radio.py @@ -1,4 +1,4 @@ -from . import QuestionnaireTestCase +from tests.integration.questionnaire import QuestionnaireTestCase class TestQuestionnaireListCollector(QuestionnaireTestCase): @@ -6,7 +6,9 @@ def test_add_list_question_displayed_before_list_collector_and_return_to_in_url( self, ): # Given - self.launchSurvey("test_answer_action_redirect_to_list_add_block_radio") + self.launchSurveyV2( + schema_name="test_answer_action_redirect_to_list_add_block_radio" + ) # When self.post({"anyone-usually-live-at-answer": "Yes"}) @@ -18,7 +20,9 @@ def test_add_list_question_displayed_before_list_collector_and_return_to_in_url( def test_previous_link_when_list_empty_with_return_to_query_string(self): # Given - self.launchSurvey("test_answer_action_redirect_to_list_add_block_radio") + self.launchSurveyV2( + schema_name="test_answer_action_redirect_to_list_add_block_radio" + ) self.post({"anyone-usually-live-at-answer": "Yes"}) # When @@ -29,7 +33,9 @@ def test_previous_link_when_list_empty_with_return_to_query_string(self): def test_previous_link_when_list_not_empty(self): # Given - self.launchSurvey("test_answer_action_redirect_to_list_add_block_radio") + self.launchSurveyV2( + schema_name="test_answer_action_redirect_to_list_add_block_radio" + ) self.post({"anyone-usually-live-at-answer": "Yes"}) self.add_person("John", "Doe") self.post({"anyone-else-live-at-answer": "Yes"}) @@ -44,7 +50,9 @@ def test_previous_link_return_to_list_collector_when_invalid_return_to_block_id( self, ): # Given - self.launchSurvey("test_answer_action_redirect_to_list_add_block_radio") + self.launchSurveyV2( + schema_name="test_answer_action_redirect_to_list_add_block_radio" + ) self.post({"anyone-usually-live-at-answer": "Yes"}) url_with_invalid_return_to = self.last_url + "-invalid" diff --git a/tests/integration/questionnaire/test_questionnaire_relationships.py b/tests/integration/questionnaire/test_questionnaire_relationships.py index 716110265e..da04ddd25c 100644 --- a/tests/integration/questionnaire/test_questionnaire_relationships.py +++ b/tests/integration/questionnaire/test_questionnaire_relationships.py @@ -1,4 +1,4 @@ -from . import QuestionnaireTestCase +from tests.integration.questionnaire import QuestionnaireTestCase class TestQuestionnaireRelationships(QuestionnaireTestCase): @@ -8,7 +8,7 @@ def remove_list_item(self, position): self.post({"remove-confirmation": "Yes"}) def test_valid_relationship(self): - self.launchSurvey("test_relationships") + self.launchSurveyV2(schema_name="test_relationships") self.add_person("Marie", "Doe") self.add_person("John", "Doe") self.post({"anyone-else": "No"}) @@ -19,7 +19,7 @@ def test_valid_relationship(self): self.assertInUrl("/questionnaire/sections/") def test_resume_should_not_show_last_viewed_guidance(self): - self.launchSurvey("test_relationships") + self.launchSurveyV2(schema_name="test_relationships") self.add_person("Marie", "Doe") self.add_person("John", "Doe") self.post({"anyone-else": "No"}) @@ -28,7 +28,7 @@ def test_resume_should_not_show_last_viewed_guidance(self): self.assertNotInBody("This is the last viewed question in this section") def test_last_relationship(self): - self.launchSurvey("test_relationships") + self.launchSurveyV2(schema_name="test_relationships") first_list_item_id = self.add_person("Marie", "Doe") second_list_item_id = self.add_person("John", "Doe") self.post({"anyone-else": "No"}) @@ -38,17 +38,17 @@ def test_last_relationship(self): ) def test_get_relationships_when_not_on_path_raises_404(self): - self.launchSurvey("test_relationships") + self.launchSurveyV2(schema_name="test_relationships") self.get("/questionnaire/relationships") self.assertStatusNotFound() def test_invalid_relationship_raises_404(self): - self.launchSurvey("test_relationships") + self.launchSurveyV2(schema_name="test_relationships") self.get("/questionnaire/relationships/people/fake-id/to/another-fake-id") self.assertStatusNotFound() def test_go_to_invalid_relationship(self): - self.launchSurvey("test_relationships") + self.launchSurveyV2(schema_name="test_relationships") self.add_person("Marie", "Doe") self.add_person("John", "Doe") self.post({"anyone-else": "No"}) @@ -57,7 +57,7 @@ def test_go_to_invalid_relationship(self): self.assertInUrl("/questionnaire/relationships") def test_failed_validation(self): - self.launchSurvey("test_relationships") + self.launchSurveyV2(schema_name="test_relationships") self.add_person("Marie", "Doe") self.add_person("John", "Doe") self.post({"anyone-else": "No"}) @@ -65,7 +65,7 @@ def test_failed_validation(self): self.assertInBody("There is a problem with your answer") def test_multiple_relationships(self): - self.launchSurvey("test_relationships") + self.launchSurveyV2(schema_name="test_relationships") self.add_person("Marie", "Doe") self.add_person("John", "Doe") self.add_person("Susan", "Doe") @@ -78,7 +78,7 @@ def test_multiple_relationships(self): self.assertInUrl("/questionnaire/sections/section/") def test_relationships_removed_when_list_item_removed(self): - self.launchSurvey("test_relationships", roles=["dumper"]) + self.launchSurveyV2(schema_name="test_relationships", roles=["dumper"]) self.add_person("Marie", "Doe") self.add_person("John", "Doe") self.add_person("Susan", "Doe") @@ -104,7 +104,7 @@ def test_relationships_removed_when_list_item_removed(self): self.assertNotIn(list_item_ids[-1], relationship.values()) def test_relationship_not_altered_when_new_list_item_not_submitted(self): - self.launchSurvey("test_relationships") + self.launchSurveyV2(schema_name="test_relationships") self.add_person("Marie", "Doe") self.add_person("John", "Doe") list_item_ids_original = self.get_list_item_ids() @@ -120,7 +120,7 @@ def test_relationship_not_altered_when_new_list_item_not_submitted(self): self.assertEqual(list_item_ids_original, list_item_ids_new) def test_post_to_relationships_root(self): - self.launchSurvey("test_relationships") + self.launchSurveyV2(schema_name="test_relationships") self.add_person("Marie", "Doe") self.add_person("John", "Doe") self.post({"anyone-else": "No"}) @@ -128,7 +128,7 @@ def test_post_to_relationships_root(self): self.assertStatusOK() def test_head_request_on_relationships_url(self): - self.launchSurvey("test_relationships") + self.launchSurveyV2(schema_name="test_relationships") first_list_item_id = self.add_person("Marie", "Doe") second_list_item_id = self.add_person("John", "Doe") self.post({"anyone-else": "No"}) diff --git a/tests/integration/questionnaire/test_questionnaire_relationships_unrelated.py b/tests/integration/questionnaire/test_questionnaire_relationships_unrelated.py index b073f3d434..77e6eef878 100644 --- a/tests/integration/questionnaire/test_questionnaire_relationships_unrelated.py +++ b/tests/integration/questionnaire/test_questionnaire_relationships_unrelated.py @@ -1,9 +1,11 @@ -from . import QuestionnaireTestCase +from tests.integration.questionnaire import QuestionnaireTestCase class TestQuestionnaireRelationshipsUnrelated(QuestionnaireTestCase): def launch_survey_and_add_people(self): - self.launchSurvey("test_relationships_unrelated", roles=["dumper"]) + self.launchSurveyV2( + schema_name="test_relationships_unrelated", roles=["dumper"] + ) self.add_person("Andrew", "Austin") self.add_person("Betty", "Burns") self.add_person("Carla", "Clark") @@ -36,7 +38,7 @@ def test_is_accessible_when_list_name_and_list_item_valid( self.assertInBody("Are any of these people related to you?") def test_is_not_accessible_when_invalid_list_item(self): - self.launchSurvey("test_relationships_unrelated") + self.launchSurveyV2(schema_name="test_relationships_unrelated") self.get( "/questionnaire/relationships/people/invalid-id/related-to-anyone-else" ) @@ -135,7 +137,9 @@ def test_variants(self): ) self.post({"relationship-answer": "Unrelated"}) self.post({"relationship-answer": "Unrelated"}) - self.assertInBody("Are any of these people related to Betty Burns?") + self.assertInBody( + "Are any of these people related to Betty Burns?" + ) def test_variant_no_answer_routes_to_next_person(self): self.launch_survey_and_add_people() diff --git a/tests/integration/questionnaire/test_questionnaire_resume.py b/tests/integration/questionnaire/test_questionnaire_resume.py index 49f2eb652d..9bea235762 100644 --- a/tests/integration/questionnaire/test_questionnaire_resume.py +++ b/tests/integration/questionnaire/test_questionnaire_resume.py @@ -5,39 +5,43 @@ class TestResume(IntegrationTestCase): def test_navigating_backwards(self): # Given I submit the first page - self.launchSurvey("test_textfield") + self.launchSurveyV2(schema_name="test_textfield") self.post({"name-answer": "Joe Bloggs"}) # When I go back to the first page, sign out and then resume self.get("/questionnaire/name-block") self.signOut() - self.launchSurvey("test_textfield") + self.launchSurveyV2(schema_name="test_textfield") # Then I should resume on the first incomplete location self.assertEqual(SUBMIT_URL_PATH, self.last_url) def test_sign_out_on_section_summary(self): # Given I complete the first section - self.launchSurvey("test_section_summary", display_address="test address") + self.launchSurveyV2( + schema_name="test_section_summary", display_address="test address" + ) self.post({"insurance-type-answer": "Both"}) self.post({"insurance-address-answer": "Address"}) self.post({"listed-answer": "No"}) # When I sign out and then resume self.signOut() - self.launchSurvey("test_section_summary", display_address="test address") + self.launchSurveyV2( + schema_name="test_section_summary", display_address="test address" + ) # Then I should resume on the start of the next section self.assertInUrl("/questionnaire/house-type/") def test_after_submission(self): # Given I complete the questionnaire and submit - self.launchSurvey("test_textfield") + self.launchSurveyV2(schema_name="test_textfield") self.post({"name-answer": "Joe Bloggs"}) self.post() # When I resume - self.launchSurvey("test_textfield") + self.launchSurveyV2(schema_name="test_textfield") # Then I should resume on the thank you page self.assertInUrl(THANK_YOU_URL_PATH) diff --git a/tests/integration/questionnaire/test_questionnaire_routing_to_questionnaire_end.py b/tests/integration/questionnaire/test_questionnaire_routing_to_questionnaire_end.py index 0cbc80a545..0f5257c116 100644 --- a/tests/integration/questionnaire/test_questionnaire_routing_to_questionnaire_end.py +++ b/tests/integration/questionnaire/test_questionnaire_routing_to_questionnaire_end.py @@ -4,7 +4,7 @@ class TestRoutingToQuestionnaireEndBase(IntegrationTestCase): def _launch_and_complete_questionnaire(self, schema): - self.launchSurvey(schema) + self.launchSurveyV2(schema_name=schema) self.post({"test-answer": "No"}) @@ -12,7 +12,7 @@ class TestRoutingToQuestionnaireEndSingleSection(TestRoutingToQuestionnaireEndBa def test_able_to_route_to_questionnaire_end(self): # Given I launch a questionnaire with a single section and answer "No" to the first question self._launch_and_complete_questionnaire( - "test_new_routing_to_questionnaire_end_single_section" + "test_routing_to_questionnaire_end_single_section" ) # Then I should be routed to the end of the questionnaire and be shown the submit page @@ -25,7 +25,7 @@ def test_able_to_route_to_questionnaire_end(self): # Given I launch a questionnaire with multiple sections # When I answer "No" to the first question self._launch_and_complete_questionnaire( - "test_new_routing_to_questionnaire_end_multiple_sections" + "test_routing_to_questionnaire_end_multiple_sections" ) # Then I should be routed to the end of the questionnaire and be shown the submit page with only 1 section @@ -39,7 +39,7 @@ def test_section_is_reenabled_when_changing_answer_after_routing_to_questionnair ): # Given I am able to route to the questionnaire end by completing only section 1 self._launch_and_complete_questionnaire( - "test_new_routing_to_questionnaire_end_multiple_sections" + "test_routing_to_questionnaire_end_multiple_sections" ) self.assertInUrl(SUBMIT_URL_PATH) diff --git a/tests/integration/questionnaire/test_questionnaire_routing_to_section_end.py b/tests/integration/questionnaire/test_questionnaire_routing_to_section_end.py index 2cd5bc6b9d..325b3eb93b 100644 --- a/tests/integration/questionnaire/test_questionnaire_routing_to_section_end.py +++ b/tests/integration/questionnaire/test_questionnaire_routing_to_section_end.py @@ -6,7 +6,7 @@ class TestRoutingToSectionEnd(IntegrationTestCase): def test_section_summary_not_available_if_any_question_in_section_incomplete(self): # Given I launch questionnaire and have not answered questions for a section - self.launchSurvey("test_new_routing_to_section_end") + self.launchSurveyV2(schema_name="test_routing_to_section_end") # When I try access the section summary self.get(SECTION_SUMMARY_URL_PATH.format(section_id="test-section")) @@ -16,14 +16,14 @@ def test_section_summary_not_available_if_any_question_in_section_incomplete(sel def test_section_summary_available_after_completing_section(self): # Given I launch questionnaire and have completed a section - self.launchSurvey("test_new_routing_to_section_end") + self.launchSurveyV2(schema_name="test_routing_to_section_end") self.post({"test-answer": "No"}) self.assertInBody("Were you forced to complete section 1?") self.assertInUrl(SECTION_SUMMARY_URL_PATH.format(section_id="test-section")) def test_section_summary_not_available_after_invalidating_section(self): # Given I launch questionnaire and have completed a section - self.launchSurvey("test_new_routing_to_section_end") + self.launchSurveyV2(schema_name="test_routing_to_section_end") self.post({"test-answer": "No"}) self.assertInBody("Were you forced to complete section 1?") self.assertInUrl(SECTION_SUMMARY_URL_PATH.format(section_id="test-section")) @@ -42,7 +42,7 @@ def test_section_summary_available_after_completing_section_new_routing_engine( self, ): # Given I launch questionnaire and have completed a section - self.launchSurvey("test_new_routing_number_equals") + self.launchSurveyV2(schema_name="test_routing_number_equals") self.post({"answer": "123"}) self.post() self.assertInBody("Check your answers and submit") diff --git a/tests/integration/questionnaire/test_questionnaire_same_name_items.py b/tests/integration/questionnaire/test_questionnaire_same_name_items.py index 94d182321d..78b3beb487 100644 --- a/tests/integration/questionnaire/test_questionnaire_same_name_items.py +++ b/tests/integration/questionnaire/test_questionnaire_same_name_items.py @@ -1,11 +1,12 @@ from app.utilities.json import json_loads - -from . import QuestionnaireTestCase +from tests.integration.questionnaire import QuestionnaireTestCase class TestQuestionnaireSameNameItems(QuestionnaireTestCase): def test_same_name_items(self): - self.launchSurvey("test_list_collector_same_name_items", roles=["dumper"]) + self.launchSurveyV2( + schema_name="test_list_collector_same_name_items", roles=["dumper"] + ) self.post({"you-live-here": "Yes"}) self.post({"first-name": "James", "middle-names": "Brian", "last-name": "May"}) @@ -23,7 +24,9 @@ def test_same_name_items(self): assert item_id_b in actual["LISTS"][0]["same_name_items"] def test_same_name_items_edit_primary(self): - self.launchSurvey("test_list_collector_same_name_items", roles=["dumper"]) + self.launchSurveyV2( + schema_name="test_list_collector_same_name_items", roles=["dumper"] + ) self.post({"you-live-here": "Yes"}) self.post({"first-name": "James", "last-name": "May"}) @@ -43,7 +46,9 @@ def test_same_name_items_edit_primary(self): assert "same_name_items" not in actual["LISTS"][0] def test_same_name_remove_primary(self): - self.launchSurvey("test_list_collector_same_name_items", roles=["dumper"]) + self.launchSurveyV2( + schema_name="test_list_collector_same_name_items", roles=["dumper"] + ) self.post({"you-live-here": "Yes"}) self.post({"first-name": "James", "last-name": "May"}) @@ -61,7 +66,9 @@ def test_same_name_remove_primary(self): assert "same_name_items" not in actual["LISTS"][0] def test_same_name_items_remove_non_primary(self): - self.launchSurvey("test_list_collector_same_name_items", roles=["dumper"]) + self.launchSurveyV2( + schema_name="test_list_collector_same_name_items", roles=["dumper"] + ) self.post({"you-live-here": "Yes"}) self.post({"first-name": "James", "last-name": "May"}) @@ -81,7 +88,9 @@ def test_same_name_items_remove_non_primary(self): assert "same_name_items" not in actual["LISTS"][0] def test_same_name_items_edit_non_primary(self): - self.launchSurvey("test_list_collector_same_name_items", roles=["dumper"]) + self.launchSurveyV2( + schema_name="test_list_collector_same_name_items", roles=["dumper"] + ) self.post({"you-live-here": "Yes"}) self.post({"first-name": "Joe", "last-name": "Smith"}) diff --git a/tests/integration/questionnaire/test_questionnaire_schema_theme_thank_you.py b/tests/integration/questionnaire/test_questionnaire_schema_theme_thank_you.py deleted file mode 100644 index e87138395d..0000000000 --- a/tests/integration/questionnaire/test_questionnaire_schema_theme_thank_you.py +++ /dev/null @@ -1,48 +0,0 @@ -from tests.integration.integration_test_case import IntegrationTestCase - - -class TestSchemaThemeThankYou(IntegrationTestCase): - def test_census_individual(self): - self.launchSurvey( - "test_thank_you_census_individual", - display_address="68 Abingdon Road, Goathill", - ) - self.post({"individual-confirmation": "Yes"}) - self.post() - self.assertInBody("Thank you for completing your census") - self.assertInBody( - "Your individual census has been submitted for 68 Abingdon Road, Goathill" - ) - self.assertInBody( - 'Make sure you leave this page or close your browser if using a shared device' - ) - - def test_census_household(self): - self.launchSurvey( - "test_thank_you_census_household", - display_address="68 Abingdon Road, Goathill", - ) - self.post({"household-confirmation": "Yes"}) - self.post() - self.assertInBody("Thank you for completing the census") - self.assertInBody( - "Your census has been submitted for the household at 68 Abingdon Road, Goathill" - ) - - def test_census_communal_establishment(self): - self.launchSurvey( - "test_thank_you_census_communal_establishment", - display_address="68 Abingdon Road, Goathill", - ) - self.post({"communal-establishment-confirmation": "Yes"}) - self.post() - self.assertInBody("Thank you for completing the census") - self.assertInBody( - "Your census has been submitted for the accommodation at 68 Abingdon Road, Goathill" - ) - - def test_census_theme_schema_name_not_mapped_to_census_type(self): - self.launchSurvey("test_feedback_email_confirmation") - self.post({"schema-confirmation-answer": "Yes"}) - self.post() - self.assertInBody("Thank you for completing the survey") diff --git a/tests/integration/questionnaire/test_questionnaire_submission.py b/tests/integration/questionnaire/test_questionnaire_submission.py index 77ad39d143..8ec22fbeae 100644 --- a/tests/integration/questionnaire/test_questionnaire_submission.py +++ b/tests/integration/questionnaire/test_questionnaire_submission.py @@ -1,10 +1,15 @@ from unittest.mock import Mock +from httmock import HTTMock, urlmatch + +from app.utilities.schema import get_schema_path_map from tests.integration.integration_test_case import IntegrationTestCase from tests.integration.questionnaire import HUB_URL_PATH, THANK_YOU_URL_PATH SUBMIT_URL_PATH = "/questionnaire/submit" +SCHEMA_PATH_MAP = get_schema_path_map(include_test_schemas=True) + class SubmissionTestCase(IntegrationTestCase): @property @@ -21,7 +26,7 @@ def _mock_submission_failure(self): class TestQuestionnaireSubmission(SubmissionTestCase): def _launch_and_submit_questionnaire(self): # Launch questionnaire - self.launchSurvey("test_submit_with_custom_submission_text") + self.launchSurveyV2(schema_name="test_submit_with_custom_submission_text") # Answer questions and submit survey self.post(action="start_questionnaire") @@ -52,10 +57,38 @@ def test_unsuccessful_submission(self): self.assertInUrl(SUBMIT_URL_PATH) +class TestQuestionnaireSubmissionSchemaURL(SubmissionTestCase): + def test_login_token_with_schema_url_should_redirect_to_survey(self): + schema_url = "http://eq-survey-register.url/my-test-schema" + + # Given + token = self.token_generator.create_token_with_schema_url( + schema_name=None, schema_url=schema_url + ) + + # When + with HTTMock(self.schema_url_mock): + self.get(url=f"/session?token={token}") + + self.assertStatusOK() + self.assertInUrl("/questionnaire") + self.post() + self.post() + self.assertInUrl(THANK_YOU_URL_PATH) + + @staticmethod + @urlmatch(netloc=r"eq-survey-register", path=r"\/my-test-schema") + def schema_url_mock(_url, _request): + schema_path = SCHEMA_PATH_MAP["test"]["en"]["test_textarea"] + + with open(schema_path, encoding="utf8") as json_data: + return json_data.read() + + class TestQuestionnaireSubmissionHub(SubmissionTestCase): def _launch_and_submit_questionnaire(self): # Launch questionnaire - self.launchSurvey("test_hub_and_spoke") + self.launchSurveyV2(schema_name="test_hub_and_spoke") # Answer questions and submit questionnaire self.post() @@ -98,7 +131,7 @@ def test_unsuccessful_submission(self): class TestQuestionnaireSubmissionWithSummary(SubmissionTestCase): def _launch_and_submit_questionnaire(self): # Launch questionnaire - self.launchSurvey("test_submit_with_summary") + self.launchSurveyV2(schema_name="test_submit_with_summary") # Answer questions and submit survey self.post() @@ -121,7 +154,7 @@ def test_unsuccessful_submission(self): self._mock_submission_failure() # Given I launch and answer a questionnaire, When I submit but the submissions fails - self.launchSurvey("test_submit_with_summary") + self.launchSurveyV2(schema_name="test_submit_with_summary") self.post() self.post({"dessert-answer": "Cake"}) self.post({"dessert-confirmation-answer": "Yes"}) diff --git a/tests/integration/questionnaire/test_questionnaire_submit.py b/tests/integration/questionnaire/test_questionnaire_submit.py index da2f60860e..bde8cb18f2 100644 --- a/tests/integration/questionnaire/test_questionnaire_submit.py +++ b/tests/integration/questionnaire/test_questionnaire_submit.py @@ -4,12 +4,12 @@ class TestQuestionnaireSubmit(IntegrationTestCase): def _launch_and_complete_questionnaire(self, schema): - self.launchSurvey(schema) + self.launchSurveyV2(schema_name=schema) self.post({"test-answer": "No"}) def test_submit_page_not_accessible_when_hub_enabled(self): # Given I launch a hub questionnaire - self.launchSurvey("test_hub_and_spoke") + self.launchSurveyV2(schema_name="test_hub_and_spoke") # When I try access the submit page for method in [self.get, self.post]: @@ -21,7 +21,7 @@ def test_submit_page_not_accessible_when_hub_enabled(self): def test_invalid_block_once_questionnaire_complete_raises_404(self): # Given I launch questionnaire - self.launchSurvey("test_submit_with_custom_submission_text") + self.launchSurveyV2(schema_name="test_submit_with_custom_submission_text") # When I proceed through the questionnaire self.post(action="start_questionnaire") @@ -37,11 +37,11 @@ def test_invalid_block_once_questionnaire_complete_raises_404(self): def test_submit_page_not_available_after_invalidating_section(self): # Given I launch and complete the questionnaire for schema in [ - "test_new_routing_to_questionnaire_end_single_section", - "test_new_routing_to_questionnaire_end_multiple_sections", + "test_routing_to_questionnaire_end_single_section", + "test_routing_to_questionnaire_end_multiple_sections", ]: with self.subTest(schema=schema): - self.launchSurvey(schema) + self.launchSurveyV2(schema_name=schema) self.post({"test-answer": "No"}) self.assertInUrl(SUBMIT_URL_PATH) @@ -62,7 +62,7 @@ def test_accessing_submit_page_redirects_to_first_incomplete_question_when_quest self, ): # Given a partially completed questionnaire - self.launchSurvey("test_submit_with_custom_submission_text") + self.launchSurveyV2(schema_name="test_submit_with_custom_submission_text") self.post(action="start_questionnaire") self.assertInBody("What is your favourite breakfast food") @@ -76,7 +76,7 @@ def test_accessing_submit_page_redirects_to_first_incomplete_question_when_quest def test_is_displayed(self): # Given I launch a questionnaire - self.launchSurvey("test_submit_with_custom_submission_text") + self.launchSurveyV2(schema_name="test_submit_with_custom_submission_text") # When I complete the questionnaire self.post(action="start_questionnaire") @@ -98,7 +98,9 @@ def test_accessing_submit_page_redirects_to_first_incomplete_question_when_quest self, ): # Given a partially completed questionnaire - self.launchSurvey("test_new_routing_to_questionnaire_end_single_section") + self.launchSurveyV2( + schema_name="test_routing_to_questionnaire_end_single_section" + ) self.post({"test-answer": "Yes"}) # When I make a GET or POST request to the submit page @@ -111,7 +113,9 @@ def test_accessing_submit_page_redirects_to_first_incomplete_question_when_quest def test_is_displayed(self): # Given I launch a questionnaire - self.launchSurvey("test_new_routing_to_questionnaire_end_multiple_sections") + self.launchSurveyV2( + schema_name="test_routing_to_questionnaire_end_multiple_sections" + ) # When I complete the questionnaire self.post({"test-answer": "Yes"}) diff --git a/tests/integration/routes/test_confirmation_email.py b/tests/integration/routes/test_confirmation_email.py index 9939a2b39a..5de8c52154 100644 --- a/tests/integration/routes/test_confirmation_email.py +++ b/tests/integration/routes/test_confirmation_email.py @@ -5,14 +5,14 @@ from tests.integration.integration_test_case import IntegrationTestCase -# pylint: disable=too-many-public-methods,too-few-public-methods +# pylint: disable=too-many-public-methods class TestEmailConfirmation(IntegrationTestCase): def setUp(self): settings.CONFIRMATION_EMAIL_LIMIT = 2 super().setUp() def _launch_and_complete_questionnaire(self): - self.launchSurvey("test_confirmation_email") + self.launchSurveyV2(schema_name="test_confirmation_email") self.post({"answer_id": "Yes"}) self.post() @@ -69,8 +69,8 @@ def test_missing_email_param_confirm_email(self): self.assertBadRequest() def test_confirm_email_with_confirmation_email_not_set(self): - # Given I launch the test_thank_you_census_individual questionnaire, which doesn't have email confirmation set in the schema - self.launchSurvey("test_thank_you_census_individual") + # Given I launch the test_thank_you questionnaire, which doesn't have email confirmation set in the schema + self.launchSurveyV2(schema_name="test_thank_you") self.post() self.post() @@ -82,8 +82,8 @@ def test_confirm_email_with_confirmation_email_not_set(self): self.assertNotInBody("Is this email address correct?") def test_confirmation_email_send_with_confirmation_email_not_set(self): - # Given I launch the test_thank_you_census_individual questionnaire, which doesn't have email confirmation set in the schema - self.launchSurvey("test_thank_you_census_individual") + # Given I launch the test_thank_you questionnaire, which doesn't have email confirmation set in the schema + self.launchSurveyV2(schema_name="test_thank_you") self.post() self.post() @@ -109,7 +109,7 @@ def test_bad_signature_confirmation_email_send(self): def test_thank_you_page_get_not_allowed(self): # Given I launch the test_confirmation_email questionnaire - self.launchSurvey("test_confirmation_email") + self.launchSurveyV2(schema_name="test_confirmation_email") # When I try to view the thank you page without completing the questionnaire self.get("/submitted/thank-you/") @@ -119,7 +119,7 @@ def test_thank_you_page_get_not_allowed(self): def test_thank_you_page_post_not_allowed(self): # Given I launch the test_confirmation_email questionnaire - self.launchSurvey("test_confirmation_email") + self.launchSurveyV2(schema_name="test_confirmation_email") # When I try to POST to the thank you page without completing the questionnaire self.post(url="/submitted/thank-you/") @@ -137,7 +137,7 @@ def test_email_confirmation_page_get_not_allowed(self): # Then I get shown a 404 error self.assertStatusNotFound() - def test_census_themed_schema_with_confirmation_email_true(self): + def test_default_themed_schema_with_confirmation_email_true(self): # Given I launch and complete the test_confirmation_email questionnaire self._launch_and_complete_questionnaire() @@ -145,24 +145,12 @@ def test_census_themed_schema_with_confirmation_email_true(self): self.assertInUrl("/submitted/thank-you/") self.assertInBody("Get confirmation email") self.assertEqualPageTitle( - "Thank you for completing the census - Confirmation email test schema" + "We’ve received your answers - Confirmation email test schema" ) - def test_census_themed_schema_with_confirmation_email_not_set(self): - # Given I launch the test_thank_you_census_individual questionnaire, which doesn't have email confirmation set in the schema - self.launchSurvey("test_thank_you_census_individual") - - # When I complete the questionnaire - self.post() - self.post() - - # Then on the thank you page I don't get a confirmation email form - self.assertInUrl("/submitted/thank-you/") - self.assertNotInBody("Get confirmation email") - def test_default_themed_schema_with_confirmation_email_not_set(self): # Given I launch the test_checkbox questionnaire, which doesn't have email confirmation set in the schema - self.launchSurvey("test_checkbox") + self.launchSurveyV2(schema_name="test_checkbox") # When I complete the questionnaire self.post({"mandatory-checkbox-answer": "Tuna"}) @@ -255,11 +243,7 @@ def test_thank_you_missing_email(self): # Then I get an error message on the thank you page self.assertInUrl("/submitted/thank-you/") - self.assertInBody("There is a problem with this page") - self.assertInBody("Enter an email address") - self.assertEqualPageTitle( - "Error: Thank you for completing the census - Confirmation email test schema" - ) + self.assertInSelectorCSS("Enter an email address", class_="ons-panel__error") def test_thank_you_incorrect_email_format(self): # Given I launch and complete the test_confirmation_email questionnaire @@ -270,13 +254,9 @@ def test_thank_you_incorrect_email_format(self): # Then I get an error message on the thank you page self.assertInUrl("thank-you") - self.assertInBody("There is a problem with this page") - self.assertInBody( - "Enter an email address in a valid format, for example name@example.com" - ) - - self.assertEqualPageTitle( - "Error: Thank you for completing the census - Confirmation email test schema" + self.assertInSelectorCSS( + "Enter an email address in a valid format, for example name@example.com", + class_="ons-panel__error", ) def test_thank_you_email_invalid_tld(self): @@ -288,9 +268,9 @@ def test_thank_you_email_invalid_tld(self): # Then I get an error message on the thank you page self.assertInUrl("thank-you") - self.assertInBody("There is a problem with this page") - self.assertInBody( - "Enter an email address in a valid format, for example name@example.com" + self.assertInSelectorCSS( + "Enter an email address in a valid format, for example name@example.com", + class_="ons-panel__error", ) def test_thank_you_email_invalid_and_invalid_tld(self): @@ -302,9 +282,9 @@ def test_thank_you_email_invalid_and_invalid_tld(self): # Then I get a single error message on the thank you page self.assertInUrl("thank-you") - self.assertInBody("There is a problem with this page") - self.assertInBody( - "Enter an email address in a valid format, for example name@example.com" + self.assertInSelectorCSS( + "Enter an email address in a valid format, for example name@example.com", + class_="ons-panel__error", ) self.assertNotInBody('data-qa="error-link-2"') diff --git a/tests/integration/routes/test_cookie.py b/tests/integration/routes/test_cookie.py index 84604c67e8..9897db045e 100644 --- a/tests/integration/routes/test_cookie.py +++ b/tests/integration/routes/test_cookie.py @@ -3,7 +3,7 @@ class TestCookie(IntegrationTestCase): def test_cookie_contents(self): - self.launchSurvey() + self.launchSurveyV2() cookie = self.getCookie() self.assertIsNotNone(cookie.get("_fresh")) @@ -11,10 +11,12 @@ def test_cookie_contents(self): self.assertIsNotNone(cookie.get("eq-session-id")) self.assertIsNotNone(cookie.get("expires_in")) self.assertIsNotNone(cookie.get("theme")) - self.assertIsNotNone(cookie.get("survey_title")) + self.assertIsNotNone(cookie.get("title")) + self.assertIsNotNone(cookie.get("survey_id")) self.assertIsNotNone(cookie.get("user_ik")) self.assertIsNotNone(cookie.get("account_service_base_url")) - self.assertEqual(len(cookie), 8) + self.assertIsNotNone(cookie.get("language_code")) + self.assertEqual(len(cookie), 10) self.assertIsNone(cookie.get("user_id")) self.assertIsNone(cookie.get("_permanent")) diff --git a/tests/integration/routes/test_dump.py b/tests/integration/routes/test_dump.py index c0668425d7..6e49fac5f9 100644 --- a/tests/integration/routes/test_dump.py +++ b/tests/integration/routes/test_dump.py @@ -14,7 +14,9 @@ def test_dump_debug_not_authenticated(self): def test_dump_debug_authenticated_missing_role(self): # Given I am an authenticated user who has launched a survey # but does not have the 'dumper' role in my metadata - self.launchSurvey("test_radio_mandatory_with_detail_answer_mandatory") + self.launchSurveyV2( + schema_name="test_radio_mandatory_with_detail_answer_mandatory" + ) # When I attempt to dump the questionnaire store self.get("/dump/debug") @@ -25,8 +27,9 @@ def test_dump_debug_authenticated_missing_role(self): def test_dump_debug_authenticated_with_role(self): # Given I am an authenticated user who has launched a survey # and does have the 'dumper' role in my metadata - self.launchSurvey( - "test_radio_mandatory_with_detail_answer_mandatory", roles=["dumper"] + self.launchSurveyV2( + schema_name="test_radio_mandatory_with_detail_answer_mandatory", + roles=["dumper"], ) # And I attempt to dump the questionnaire store @@ -48,7 +51,9 @@ def test_dump_submission_not_authenticated(self): def test_dump_submission_authenticated_missing_role(self): # Given I am an authenticated user who has launched a survey # but does not have the 'dumper' role in my metadata - self.launchSurvey("test_radio_mandatory_with_detail_answer_mandatory") + self.launchSurveyV2( + schema_name="test_radio_mandatory_with_detail_answer_mandatory" + ) # When I attempt to dump the submission payload self.get("/dump/submission") @@ -59,8 +64,9 @@ def test_dump_submission_authenticated_missing_role(self): def test_dump_submission_authenticated_with_role_no_answers(self): # Given I am an authenticated user who has launched a survey # and does have the 'dumper' role in my metadata - self.launchSurvey( - "test_radio_mandatory_with_detail_answer_mandatory", roles=["dumper"] + self.launchSurveyV2( + schema_name="test_radio_mandatory_with_detail_answer_mandatory", + roles=["dumper"], ) # When I haven't submitted any answers @@ -75,23 +81,32 @@ def test_dump_submission_authenticated_with_role_no_answers(self): # tx_id and submitted_at are dynamic; so copy them over expected = { "submission": { - "version": "0.0.3", - "survey_id": "0", - "flushed": False, - "origin": "uk.gov.ons.edc.eq", - "type": "uk.gov.ons.edc.eq:surveyresponse", - "tx_id": actual["submission"]["tx_id"], - "submitted_at": actual["submission"]["submitted_at"], "case_id": actual["submission"]["case_id"], - "collection": { - "period": "201604", - "exercise_sid": "789", - "schema_name": "test_radio_mandatory_with_detail_answer_mandatory", - }, + "collection_exercise_sid": "789", "data": {"answers": [], "lists": []}, - "metadata": {"ru_ref": "123456789012A", "user_id": "integration-test"}, + "data_version": "0.0.3", + "flushed": False, "launch_language_code": "en", + "origin": "uk.gov.ons.edc.eq", + "schema_name": "test_radio_mandatory_with_detail_answer_mandatory", "submission_language_code": "en", + "submitted_at": actual["submission"]["submitted_at"], + "survey_metadata": { + "display_address": "68 Abingdon Road, " "Goathill", + "employment_date": "1983-06-02", + "period_id": "201604", + "period_str": "April 2016", + "ref_p_end_date": "2016-04-30", + "ref_p_start_date": "2016-04-01", + "ru_name": "Integration Testing", + "ru_ref": "12345678901A", + "survey_id": "0", + "trad_as": "Integration Tests", + "user_id": "integration-test", + }, + "tx_id": actual["submission"]["tx_id"], + "type": "uk.gov.ons.edc.eq:surveyresponse", + "version": "v2", } } @@ -100,7 +115,7 @@ def test_dump_submission_authenticated_with_role_no_answers(self): def test_dump_submission_authenticated_with_role_with_answers(self): # Given I am an authenticated user who has launched a survey # and does have the 'dumper' role in my metadata - self.launchSurvey("test_radio_mandatory", roles=["dumper"]) + self.launchSurveyV2(schema_name="test_radio_mandatory", roles=["dumper"]) # When I submit an answer self.post(post_data={"radio-mandatory-answer": "Coffee"}) @@ -117,29 +132,38 @@ def test_dump_submission_authenticated_with_role_with_answers(self): # tx_id and submitted_at are dynamic; so copy them over expected = { "submission": { - "version": "0.0.3", - "survey_id": "0", - "flushed": False, - "origin": "uk.gov.ons.edc.eq", - "type": "uk.gov.ons.edc.eq:surveyresponse", - "tx_id": actual["submission"]["tx_id"], - "started_at": actual["submission"]["started_at"], - "submitted_at": actual["submission"]["submitted_at"], "case_id": actual["submission"]["case_id"], - "collection": { - "period": "201604", - "exercise_sid": "789", - "schema_name": "test_radio_mandatory", - }, + "collection_exercise_sid": "789", "data": { "answers": [ {"answer_id": "radio-mandatory-answer", "value": "Coffee"} ], "lists": [], }, - "metadata": {"ru_ref": "123456789012A", "user_id": "integration-test"}, + "data_version": "0.0.3", + "flushed": False, "launch_language_code": "en", + "origin": "uk.gov.ons.edc.eq", + "schema_name": "test_radio_mandatory", + "started_at": actual["submission"]["started_at"], "submission_language_code": "en", + "submitted_at": actual["submission"]["submitted_at"], + "survey_metadata": { + "display_address": "68 Abingdon Road, " "Goathill", + "employment_date": "1983-06-02", + "period_id": "201604", + "period_str": "April 2016", + "ref_p_end_date": "2016-04-30", + "ref_p_start_date": "2016-04-01", + "ru_name": "Integration Testing", + "ru_ref": "12345678901A", + "survey_id": "0", + "trad_as": "Integration Tests", + "user_id": "integration-test", + }, + "tx_id": actual["submission"]["tx_id"], + "type": "uk.gov.ons.edc.eq:surveyresponse", + "version": "v2", } } assert actual == expected @@ -147,7 +171,7 @@ def test_dump_submission_authenticated_with_role_with_answers(self): def test_dump_submission_authenticated_with_role_with_lists(self): # Given I am an authenticated user who has launched a survey # and does have the 'dumper' role in my metadata - self.launchSurvey("test_relationships", roles=["dumper"]) + self.launchSurveyV2(schema_name="test_relationships", roles=["dumper"]) # When I submit my answers self.post({"anyone-else": "Yes"}) @@ -166,48 +190,61 @@ def test_dump_submission_authenticated_with_role_with_lists(self): # tx_id and submitted_at are dynamic; so copy them over expected = { "submission": { - "version": "0.0.3", - "survey_id": "0", - "flushed": False, - "origin": "uk.gov.ons.edc.eq", - "type": "uk.gov.ons.edc.eq:surveyresponse", - "tx_id": actual["submission"]["tx_id"], - "started_at": actual["submission"]["started_at"], - "submitted_at": actual["submission"]["submitted_at"], "case_id": actual["submission"]["case_id"], - "collection": { - "period": "201604", - "exercise_sid": "789", - "schema_name": "test_relationships", - }, + "collection_exercise_sid": "789", "data": { "answers": [ { "answer_id": "first-name", - "value": "John", "list_item_id": actual["submission"]["data"]["answers"][0][ "list_item_id" ], + "value": "John", }, { "answer_id": "last-name", - "value": "Doe", "list_item_id": actual["submission"]["data"]["answers"][0][ "list_item_id" ], + "value": "Doe", }, {"answer_id": "anyone-else", "value": "No"}, ], "lists": [ { + "items": [ + actual["submission"]["data"]["answers"][0][ + "list_item_id" + ] + ], "name": "people", - "items": actual["submission"]["data"]["lists"][0]["items"], } ], }, - "metadata": {"ru_ref": "123456789012A", "user_id": "integration-test"}, + "data_version": "0.0.3", + "flushed": False, "launch_language_code": "en", + "origin": "uk.gov.ons.edc.eq", + "schema_name": "test_relationships", + "started_at": actual["submission"]["started_at"], "submission_language_code": "en", + "submitted_at": actual["submission"]["submitted_at"], + "survey_metadata": { + "display_address": "68 Abingdon Road, " "Goathill", + "employment_date": "1983-06-02", + "period_id": "201604", + "period_str": "April 2016", + "ref_p_end_date": "2016-04-30", + "ref_p_start_date": "2016-04-01", + "ru_name": "Integration Testing", + "ru_ref": "12345678901A", + "survey_id": "0", + "trad_as": "Integration Tests", + "user_id": "integration-test", + }, + "tx_id": actual["submission"]["tx_id"], + "type": "uk.gov.ons.edc.eq:surveyresponse", + "version": "v2", } } assert actual == expected @@ -225,7 +262,9 @@ def test_dump_route_not_authenticated(self): def test_dump_route_authenticated_missing_role(self): # Given I am an authenticated user who has launched a survey # but does not have the 'dumper' role in my metadata - self.launchSurvey("test_radio_mandatory_with_detail_answer_mandatory") + self.launchSurveyV2( + schema_name="test_radio_mandatory_with_detail_answer_mandatory" + ) # When I attempt to dump the questionnaire store self.get("/dump/routing-path") @@ -236,8 +275,9 @@ def test_dump_route_authenticated_missing_role(self): def test_dump_route_authenticated_with_role(self): # Given I am an authenticated user who has launched a survey # and does have the 'dumper' role in my metadata - self.launchSurvey( - "test_radio_mandatory_with_detail_answer_mandatory", roles=["dumper"] + self.launchSurveyV2( + schema_name="test_radio_mandatory_with_detail_answer_mandatory", + roles=["dumper"], ) # And I attempt to dump the questionnaire store @@ -249,8 +289,9 @@ def test_dump_route_authenticated_with_role(self): def test_dump_route_authenticated_with_role_no_answers(self): # Given I am an authenticated user who has launched a survey # and does have the 'dumper' role in my metadata - self.launchSurvey( - "test_radio_mandatory_with_detail_answer_mandatory", roles=["dumper"] + self.launchSurveyV2( + schema_name="test_radio_mandatory_with_detail_answer_mandatory", + roles=["dumper"], ) # When I haven't submitted any answers @@ -276,7 +317,7 @@ def test_dump_route_authenticated_with_role_no_answers(self): def test_dump_submission_authenticated_with_role_with_answers(self): # Given I am an authenticated user who has launched a survey # and does have the 'dumper' role in my metadata - self.launchSurvey("test_radio_mandatory", roles=["dumper"]) + self.launchSurveyV2(schema_name="test_radio_mandatory", roles=["dumper"]) # When I submit an answer self.post(post_data={"radio-mandatory-answer": "Coffee"}) diff --git a/tests/integration/routes/test_errors.py b/tests/integration/routes/test_errors.py index 49e9ad6255..7871cf4f08 100644 --- a/tests/integration/routes/test_errors.py +++ b/tests/integration/routes/test_errors.py @@ -1,38 +1,87 @@ -from unittest.mock import patch +from mock import Mock, patch +from app.questionnaire.questionnaire_schema import DEFAULT_LANGUAGE_CODE +from app.settings import ( + ACCOUNT_SERVICE_BASE_URL, + ACCOUNT_SERVICE_BASE_URL_SOCIAL, + ONS_URL, +) +from tests.app.parser.conftest import get_response_expires_at +from tests.integration.create_token import ACCOUNT_SERVICE_URL from tests.integration.integration_test_case import IntegrationTestCase +DEFAULT_URL = ACCOUNT_SERVICE_URL +BUSINESS_URL = ACCOUNT_SERVICE_BASE_URL +SOCIAL_URL = ACCOUNT_SERVICE_BASE_URL_SOCIAL -class TestErrors(IntegrationTestCase): + +class TestErrors(IntegrationTestCase): # pylint: disable=too-many-public-methods example_payload = { - "user_id": "integration-test", - "period_str": "April 2016", - "period_id": "201604", + "survey_metadata": { + "data": { + "user_id": "integration-test", + "period_str": "April 2016", + "period_id": "201604", + "ru_ref": "12345678901A", + "ru_name": "Integration Testing", + "ref_p_start_date": "2016-04-01", + "ref_p_end_date": "2016-04-30", + "return_by": "2016-05-06", + "employment_date": "1983-06-02", + "region_code": "GB-ENG", + } + }, "collection_exercise_sid": "789", - "ru_ref": "123456789012A", "response_id": "1234567890123456", - "ru_name": "Integration Testing", - "ref_p_start_date": "2016-04-01", - "ref_p_end_date": "2016-04-30", - "return_by": "2016-05-06", - "employment_date": "1983-06-02", - "region_code": "GB-ENG", "language_code": "en", "account_service_url": "http://correct.place", "roles": [], + "response_expires_at": get_response_expires_at(), + "version": "v2", } + def _assert_generic_500_page_content(self): + self.assertInBody( + "

    Sorry, there is a problem with this service

    \n" + "

    Try again later.

    \n" + "

    If you have started a survey, your answers have been saved.

    " + ) + + def _assert_default_theme_500_page_content( + self, *, url=DEFAULT_URL, has_header=False, contact_us_text="contact us" + ): + header_text = "

    Business surveys

    \n" if has_header else "" + self.assertInBody( + f"{header_text}

    If you have attempted to submit your survey, you should check that this was successful. To do this, " + f'sign in to your business survey account.

    \n' + f'

    If you need more help, {contact_us_text}.

    ' + ) + + def _assert_social_theme_500_page_content( + self, has_header=False, contact_us_text="contact us" + ): + header_text = "

    All other surveys

    \n" if has_header else "" + + self.assertInBody( + f"{header_text}

    If you have attempted to submit your survey, you should check that this was successful. To do this, " + f're-enter your code.

    \n' + f'

    If you need more help, {contact_us_text}.

    ' + ) + def test_errors_404(self): self.get("/hfjdskahfjdkashfsa") self.assertStatusNotFound() # Test that my account link does not show + self.assertNotInBody("Help") self.assertNotInBody("My account") self.assertNotInBody("Sign out") def test_errors_404_with_payload(self): - with patch("tests.integration.create_token.PAYLOAD", self.example_payload): - self.launchSurvey("test_percentage") + with patch( + "tests.integration.create_token.PAYLOAD_V2_BUSINESS", self.example_payload + ): + self.launchSurveyV2(schema_name="test_percentage") self.get("/hfjdskahfjdkashfsa") self.assertStatusNotFound() @@ -46,36 +95,340 @@ def test_errors_405(self): def test_errors_500_with_payload(self): # Given - with patch("tests.integration.create_token.PAYLOAD", self.example_payload): - self.launchSurvey("test_percentage") + with patch( + "tests.integration.create_token.PAYLOAD_V2_BUSINESS", self.example_payload + ): + self.launchSurveyV2(schema_name="test_percentage") # When / Then # Patch out a class in post to raise an exception so that the application error handler # gets called with patch( "app.routes.questionnaire.get_block_handler", - side_effect=Exception("You broked it"), + side_effect=Exception("You broke it"), ): self.post({"answer": "5000000"}) self.assertStatusCode(500) def test_errors_500_exception_during_error_handling(self): # Given - with patch("tests.integration.create_token.PAYLOAD", self.example_payload): - self.launchSurvey("test_percentage") + with patch( + "tests.integration.create_token.PAYLOAD_V2_BUSINESS", self.example_payload + ): + self.launchSurveyV2(schema_name="test_percentage") # When # Patch out a class in post to raise an exception so that the application error handler # gets called with patch( "app.routes.questionnaire.get_block_handler", - side_effect=Exception("You broked it"), + side_effect=Exception("You broke it"), ): # Another exception occurs during exception handling with patch( "app.routes.errors.log_exception", - side_effect=Exception("You broked it again"), + side_effect=Exception("You broke it again"), ): self.post({"answer": "5000000"}) self.assertStatusCode(500) - self.assertInBody("Sorry, there is a problem with this service") + self._assert_generic_500_page_content() + + def test_401_theme_default_cookie_exists(self): + # Given + self.launchSurveyV2(schema_name="test_introduction") + self.assertInUrl("/questionnaire/introduction/") + + # When + current_url = self.last_url + self.exit() + self.get(current_url) + + # Then + self.assertStatusUnauthorised() + cookie = self.getCookie() + self.assertEqual(cookie.get("theme"), "default") + self.assertInBody( + f'

    You will need to sign back in to access your account

    ' + ) + + def test_401_theme_social_cookie_exists(self): + # Given + self.launchSurveyV2( + schema_name="test_theme_social", + theme="social", + account_service_url=SOCIAL_URL, + ) + self.assertInUrl("/questionnaire/radio/") + + # When + current_url = self.last_url + self.saveAndSignOut() + self.get(current_url) + + # Then + self.assertStatusUnauthorised() + cookie = self.getCookie() + self.assertEqual(cookie.get("theme"), "social") + self.assertInBody( + f'

    To access this page you need to re-enter your access code.

    ' + ) + + def test_401_no_cookie(self): + # Given + self.launchSurveyV2(schema_name="test_introduction") + self.assertInUrl("/questionnaire/introduction/") + + # When + current_url = self.last_url + self.exit() + self.deleteCookieAndGetUrl(current_url) + + # Then + self.assertStatusUnauthorised() + self.assertInBody( + [ + ( + f'

    If you are completing a business survey, you need to sign back in to your account.

    ' + ), + f'

    If you started your survey using an access code, you need to re-enter your code.' + "

    ", + ] + ) + + def test_403_theme_default_cookie_exists(self): + # Given + self.launchSurveyV2(schema_name="test_introduction") + + # When + cookie = self.getUrlAndCookie("/dump/debug") + + # Then + self.assertEqual(cookie.get("theme"), "default") + self.assertStatusForbidden() + self.assertInBody( + f'

    For further help, please contact us.

    ' + ) + + def test_403_theme_social_cookie_exists(self): + # Given + self.launchSurveyV2( + schema_name="test_theme_social", + theme="social", + account_service_url=SOCIAL_URL, + ) + + # When + cookie = self.getUrlAndCookie("/dump/debug") + # Then + self.assertEqual(cookie.get("theme"), "social") + self.assertStatusForbidden() + self.assertInBody( + f'

    For further help, please contact us.

    ' + ) + + def test_403_no_cookie(self): + # Given + self.launchSurveyV2(schema_name="test_introduction") + + # When + token = 123 + self.get(url=f"/session?token={token}") + + # Then + self.assertStatusForbidden() + self.assertInBody( + [ + ( + f'

    If you are completing a business survey and you need further help, please contact us.

    ' + ), + ( + f'

    If you started your survey using an access code and you need further help, please contact us.

    ' + ), + ] + ) + + def test_404_theme_default_cookie_exists(self): + # Given + self.launchSurveyV2(schema_name="test_introduction") + + # When + cookie = self.getUrlAndCookie("/abc123") + + # Then + self.assertEqual(cookie.get("theme"), "default") + self.assertStatusNotFound() + self.assertInBody( + f'

    If the web address is correct or you selected a link or button, contact us for more help.

    ' + ) + + def test_404_theme_social_cookie_exists(self): + # Given + self.launchSurveyV2( + schema_name="test_theme_social", + theme="social", + account_service_url=SOCIAL_URL, + ) + + # When + cookie = self.getUrlAndCookie("/abc123") + + # Then + self.assertEqual(cookie.get("theme"), "social") + self.assertStatusNotFound() + self.assertInBody( + f'

    If the web address is correct or you selected a link or button, contact us for more' + " help.

    " + ) + + def test_404_no_cookie(self): + # Given + self.launchSurveyV2(schema_name="test_introduction") + + # When + self.deleteCookieAndGetUrl("/abc123") + + # Then + self.assertStatusNotFound() + self.assertInBody( + [ + "

    If the web address is correct or you selected a link or button, please see the following help links.

    ", + f'

    If you are completing a business survey, please contact us.

    ', + f'

    If you started your survey using an access code, please contact us.

    ', + ] + ) + + def test_404_no_cookie_unauthenticated(self): + # Given + self.launchSurveyV2(schema_name="test_introduction") + + # When + self.exit() + self.deleteCookieAndGetUrl("/abc123") + + # Then + self.assertStatusNotFound() + self.assertInBody( + [ + "

    If the web address is correct or you selected a link or button, please see the following help links.

    ", + f'

    If you are completing a business survey, please contact us.

    ', + f'

    If you started your survey using an access code, please contact us.

    ', + ] + ) + + def test_500_theme_default_cookie_exists(self): + # Given + self.launchSurveyV2(schema_name="test_introduction") + + # When + with patch( + "app.routes.questionnaire.get_block_handler", + side_effect=Exception("You broke it"), + ): + self.post({"answer": "test"}) + cookie = self.getCookie() + + # Then + self.assertEqual(cookie.get("theme"), "default") + self.assertStatusCode(500) + self._assert_generic_500_page_content() + self._assert_default_theme_500_page_content() + + def test_500_theme_social_cookie_exists(self): + # Given + self.launchSurveyV2( + schema_name="test_theme_social", + theme="social", + account_service_url=SOCIAL_URL, + ) + # When + with patch( + "app.routes.questionnaire.get_block_handler", + side_effect=Exception("You broke it"), + ): + self.post({"answer": "test"}) + cookie = self.getCookie() + + # Then + self.assertEqual(cookie.get("theme"), "social") + self.assertStatusCode(500) + self._assert_generic_500_page_content() + self._assert_social_theme_500_page_content() + + def test_500_theme_not_set_in_cookie(self): + # Given I launch a survey, When the 'theme' is not set in the cookie + with patch( + "app.routes.session.set_schema_context_in_cookie", + side_effect=Exception("Theme set failed"), + ): + self.launchSurveyV2(schema_name="test_introduction") + + # Then I see the generic 500 error page + cookie = self.getCookie() + self.assertEqual(cookie.get("theme"), None) + self.assertStatusCode(500) + self._assert_generic_500_page_content() + self._assert_default_theme_500_page_content( + url=BUSINESS_URL, + has_header=True, + contact_us_text="contact us about business surveys", + ) + self._assert_social_theme_500_page_content( + has_header=True, contact_us_text="contact us about all other surveys" + ) + + def test_submission_failed_theme_default_cookie_exists(self): + # Given + submitter = self._application.eq["submitter"] + submitter.send_message = Mock(return_value=False) + + # When + self.launchAndFailSubmission("test_instructions") + self.post() + + # Then + self.assertStatusCode(500) + self.assertInBody( + f'

    If this problem keeps happening, please contact us for help.

    ' + ) + + def test_submission_failed_theme_social_cookie_exists(self): + # Given + submitter = self._application.eq["submitter"] + submitter.send_message = Mock(return_value=False) + + # When + self.launchSurveyV2( + schema_name="test_theme_social", + theme="social", + account_service_url=SOCIAL_URL, + ) + self.post() + self.post() + self.post() + + # Then + self.assertStatusCode(500) + self.assertInBody( + f'

    If this problem keeps happening, please contact us for help.

    ' + ) + + def test_preview_not_enabled_results_in_404(self): + self.launchSurveyV2(schema_name="test_checkbox") + self.post(action="start_questionnaire") + self.get("/questionnaire/preview/") + self.assertStatusCode(404) + + def launchAndFailSubmission(self, schema): + self.launchSurveyV2(schema_name=schema) + self.post() + self.post() + self.post() + + def getUrlAndCookie(self, url): + self.get(url=url) + return self.getCookie() + + def deleteCookieAndGetUrl(self, url): + self.deleteCookie() + self.get(url=url) diff --git a/tests/integration/routes/test_feedback.py b/tests/integration/routes/test_feedback.py index 2c8d845298..e39022f9bf 100644 --- a/tests/integration/routes/test_feedback.py +++ b/tests/integration/routes/test_feedback.py @@ -5,7 +5,7 @@ from tests.integration.integration_test_case import IntegrationTestCase -# pylint: disable=too-many-public-methods +# pylint: disable=too-many-public-methods class TestFeedback(IntegrationTestCase): SEND_FEEDBACK_URL = "/submitted/feedback/send" SENT_FEEDBACK_URL = "/submitted/feedback/sent" @@ -16,7 +16,7 @@ def setUp(self): def test_questionnaire_not_completed(self): # Given I launch the test_feedback questionnaire - self.launchSurvey("test_feedback") + self.launchSurveyV2(schema_name="test_feedback") # When I try to view the feedback page without completing the questionnaire self.get(self.SEND_FEEDBACK_URL) @@ -26,7 +26,7 @@ def test_questionnaire_not_completed(self): def test_questionnaire_not_completed_post(self): # Given I launch the test_feedback questionnaire - self.launchSurvey("test_feedback") + self.launchSurveyV2(schema_name="test_feedback") # When I try to POST to the feedback page without completing the questionnaire self.post(url=self.SEND_FEEDBACK_URL) @@ -46,7 +46,7 @@ def test_feedback_sent_page_without_feedback_submitted(self): def test_feedback_flag_not_set_in_schema(self): # Given I launch the test_textfield questionnaire - self.launchSurvey("test_textfield") + self.launchSurveyV2(schema_name="test_textfield") self.post() self.post() @@ -239,7 +239,7 @@ def test_feedback_call_to_action_shown(self): def test_feedback_submission(self): # Given I submit the email confirmation form - self.launchSurvey("test_feedback_email_confirmation") + self.launchSurveyV2(schema_name="test_feedback_email_confirmation") self.post({"answer_id": "Yes"}) self.post() self.post({"email": "email@example.com"}) @@ -253,9 +253,40 @@ def test_feedback_submission(self): ) self.assertInUrl("/submitted/feedback/sent") + def test_feedback_submission_v2_business(self): + # Given I submit the email confirmation form + self.launchSurveyV2(schema_name="test_feedback_email_confirmation") + self.post({"answer_id": "Yes"}) + self.post() + self.post({"email": "email@example.com"}) + + # When I request the feedback page + self.get("/submitted/feedback/send") + + # Then I am able to submit feedback + self.post( + {"feedback-type": "Page design and structure", "feedback-text": "Feedback"} + ) + self.assertInUrl("/submitted/feedback/sent") + + def test_feedback_submission_v2_social(self): + # Given I submit the email confirmation form + self.launchSurveyV2(schema_name="test_theme_social", theme="social") + self.post({"radio-answer": "Bacon"}) + self.post() + + # When I request the feedback page + self.get("/submitted/feedback/send") + + # Then I am able to submit feedback + self.post( + {"feedback-type": "Page design and structure", "feedback-text": "Feedback"} + ) + self.assertInUrl("/submitted/feedback/sent") + def test_feedback_call_to_action_visible_on_email_confirmation(self): # Given I complete the survey - self.launchSurvey("test_feedback_email_confirmation") + self.launchSurveyV2(schema_name="test_feedback_email_confirmation") self.post({"answer_id": "Yes"}) self.post() @@ -268,7 +299,7 @@ def test_feedback_call_to_action_visible_on_email_confirmation(self): def test_feedback_submission_from_email_confirmation(self): # Given I submit the email confirmation form - self.launchSurvey("test_feedback_email_confirmation") + self.launchSurveyV2(schema_name="test_feedback_email_confirmation") self.post({"answer_id": "Yes"}) self.post() self.post({"email": "email@example.com"}) @@ -284,7 +315,7 @@ def test_feedback_submission_from_email_confirmation(self): def test_feedback_back_breadcrumb_after_email_confirmation(self): # Given I submit the email confirmation form - self.launchSurvey("test_feedback_email_confirmation") + self.launchSurveyV2(schema_name="test_feedback_email_confirmation") self.post({"answer_id": "Yes"}) self.post() self.post({"email": "email@example.com"}) @@ -298,7 +329,7 @@ def test_feedback_back_breadcrumb_after_email_confirmation(self): def test_feedback_submitted_done_button_after_email_confirmation(self): # Given I submit the email confirmation form after submitting feedback - self.launchSurvey("test_feedback_email_confirmation") + self.launchSurveyV2(schema_name="test_feedback_email_confirmation") self.post({"answer_id": "Yes"}) self.post() self.get("/submitted/feedback/send") @@ -356,6 +387,6 @@ def test_head_request_on_feedback_sent(self): self.assertStatusOK() def _launch_and_complete_questionnaire(self): - self.launchSurvey("test_feedback") + self.launchSurveyV2(schema_name="test_feedback") self.post({"answer_id": "Yes"}) self.post() diff --git a/tests/integration/routes/test_jwt_authentication.py b/tests/integration/routes/test_jwt_authentication.py index ff15a28325..eb2fa02cb1 100644 --- a/tests/integration/routes/test_jwt_authentication.py +++ b/tests/integration/routes/test_jwt_authentication.py @@ -13,6 +13,7 @@ TEST_DO_NOT_USE_SR_PUBLIC_KEY, TEST_DO_NOT_USE_UPSTREAM_PRIVATE_KEY, ) +from tests.app.parser.conftest import get_response_expires_at from tests.integration.app_context_test_case import AppContextTestCase from tests.integration.integration_test_case import ( EQ_USER_AUTHENTICATION_RRM_PRIVATE_KEY_KID, @@ -71,20 +72,26 @@ def create_payload(): "tx_id": str(uuid.uuid4()), "jti": str(uuid.uuid4()), "case_id": str(uuid.uuid4()), - "user_id": "jimmy", "iat": int(iat), "exp": int(exp), - "period_str": "2016-01-01", - "period_id": "12", + "survey_metadata": { + "data": { + "period_str": "2016-01-01", + "period_id": "12", + "ref_p_start_date": "2016-01-01", + "ref_p_end_date": "2016-09-01", + "ru_ref": "12345678901A", + "ru_name": "Test", + "return_by": "2016-09-09", + "user_id": "jimmy", + } + }, "schema_name": "test_default", "collection_exercise_sid": "sid", - "ref_p_start_date": "2016-01-01", - "ref_p_end_date": "2016-09-01", - "ru_ref": "1234", "response_id": response_id, - "ru_name": "Test", - "return_by": "2016-09-09", "account_service_url": "http://upstream.url/", + "response_expires_at": get_response_expires_at(), + "version": "v2", } diff --git a/tests/integration/routes/test_questionnaire.py b/tests/integration/routes/test_questionnaire.py index d24eef44b6..5870b0a34c 100644 --- a/tests/integration/routes/test_questionnaire.py +++ b/tests/integration/routes/test_questionnaire.py @@ -3,22 +3,22 @@ class TestQuestionnaire(IntegrationTestCase): def test_head_request_on_root_url(self): - self.launchSurvey("test_hub_and_spoke") + self.launchSurveyV2(schema_name="test_hub_and_spoke") self.head("/questionnaire/") self.assertStatusOK() def test_head_request_on_section_url(self): - self.launchSurvey("test_hub_and_spoke") + self.launchSurveyV2(schema_name="test_hub_and_spoke") self.head("/questionnaire/sections/employment-section") self.assertStatusCode(302) def test_head_request_on_block_url(self): - self.launchSurvey("test_textfield") + self.launchSurveyV2(schema_name="test_textfield") self.head("/questionnaire/name-block") self.assertStatusOK() def test_head_request_on_block_with_optional_date_answer(self): - self.launchSurvey("test_dates") + self.launchSurveyV2(schema_name="test_dates") self.post( { "date-range-from-answer-day": "1", @@ -46,10 +46,33 @@ def test_head_request_on_block_with_optional_date_answer(self): self.assertStatusOK() def test_options_request_before_request(self): - self.launchSurvey("test_hub_and_spoke") + self.launchSurveyV2(schema_name="test_hub_and_spoke") with self.assertLogs() as logs: self.options("/questionnaire/") self.assertStatusOK() for output in logs.output: self.assertNotIn("questionnaire request", output) + + def test_get_request_logs_output(self): + self.launchSurveyV2(schema_name="test_hub_and_spoke") + with self.assertLogs() as logs: + self.get("/questionnaire/") + self.assertStatusOK() + + request_log = logs.output[0] + questionnaire_request_log = logs.output[1] + + self.assertNotIn("tx_id", request_log) + self.assertNotIn("ce_id", request_log) + self.assertNotIn("schema_name", request_log) + self.assertIn("url_path", request_log) + self.assertIn("request_id", request_log) + self.assertIn("method", request_log) + + self.assertIn("tx_id", questionnaire_request_log) + self.assertIn("ce_id", questionnaire_request_log) + self.assertIn("schema_name", questionnaire_request_log) + self.assertIn("url_path", questionnaire_request_log) + self.assertIn("request_id", questionnaire_request_log) + self.assertIn("method", questionnaire_request_log) diff --git a/tests/integration/routes/test_questionnaire_language.py b/tests/integration/routes/test_questionnaire_language.py index 89ba9f1210..c893ca7b78 100644 --- a/tests/integration/routes/test_questionnaire_language.py +++ b/tests/integration/routes/test_questionnaire_language.py @@ -6,21 +6,21 @@ class TestQuestionnaireLanguage(IntegrationTestCase): def test_load_cy_survey(self): # When: load a cy survey - self.launchSurvey("test_language", language_code="cy") + self.launchSurveyV2(schema_name="test_language", language_code="cy") # Then: welsh self.post() self.assertInBody("Rhowch enw") def test_load_non_existent_lang_fallback(self): # When: load a hindi survey - self.launchSurvey("test_language", language_code="hi") + self.launchSurveyV2(schema_name="test_language", language_code="hi") # Then: Falls back to english self.post() self.assertInBody("First Name") def test_language_switch_in_flight(self): # load a english survey - self.launchSurvey("test_language", language_code="en") + self.launchSurveyV2(schema_name="test_language", language_code="en") # The language is english self.post() self.assertInBody("First Name") @@ -30,7 +30,7 @@ def test_language_switch_in_flight(self): def test_switch_to_invalid_language(self): # load a english survey - self.launchSurvey("test_language", language_code="en") + self.launchSurveyV2(schema_name="test_language", language_code="en") # The language is english self.post() self.assertInBody("First Name") @@ -132,7 +132,7 @@ def test_plural_forms_rendered_using_correct_language(self): for data in test_data_sets: with self.subTest(data=data): self.setUp() - self.launchSurvey("test_language") + self.launchSurveyV2(schema_name="test_language") self.post() self.post({"first-name": "Kevin", "last-name": "Bacon"}) @@ -155,7 +155,7 @@ def test_plural_forms_rendered_using_correct_language(self): def test_error_messages(self): # load a welsh survey - self.launchSurvey("test_language", language_code="cy") + self.launchSurveyV2(schema_name="test_language", language_code="cy") # Submit and check the error message is in Welsh self.post() self.post() @@ -164,7 +164,7 @@ def test_error_messages(self): def test_language_switch_hub_submission(self): # load an English survey - self.launchSurvey("test_language", language_code="en") + self.launchSurveyV2(schema_name="test_language", language_code="en") # Complete the survey self.post() @@ -196,7 +196,7 @@ def test_language_switch_hub_submission(self): def test_last_viewed_guidance_is_displayed_after_language_switch(self): # load a welsh survey - self.launchSurvey("test_language", language_code="en") + self.launchSurveyV2(schema_name="test_language", language_code="en") self.post() self.post({"first-name": "John", "last-name": "Smith"}) @@ -207,3 +207,12 @@ def test_last_viewed_guidance_is_displayed_after_language_switch(self): # Switch the language to welsh and check that the last viewed guidance is still being displayed (in welsh) self.get(f"{self.last_url}&language_code=cy") self.assertInBody("Dyma'r cwestiwn a gafodd ei weld ddiwethaf yn yr adran hon") + + def test_sign_out_cy_survey(self): + # When: load a cy survey + self.launchSurveyV2(schema_name="test_language", language_code="cy") + # Then: sign out + self.get(self.getSignOutButton()["href"], follow_redirects=True) + # Check the text and logos are in Welsh + self.assertInBody("Mae eich cynnydd wedi'i gadw") + self.assertInBody("Swyddfa Ystadegau Gwladol") diff --git a/tests/integration/routes/test_session.py b/tests/integration/routes/test_session.py index d36093c778..29f57a634e 100644 --- a/tests/integration/routes/test_session.py +++ b/tests/integration/routes/test_session.py @@ -1,13 +1,45 @@ import time from datetime import datetime, timedelta, timezone +import responses from freezegun import freeze_time +from marshmallow import ValidationError +from mock.mock import patch +from sdc.crypto.key_store import KeyStore +from app.helpers.metadata_helpers import get_ru_ref_without_check_letter +from app.questionnaire.questionnaire_schema import DEFAULT_LANGUAGE_CODE +from app.services.supplementary_data import SupplementaryDataRequestFailed +from app.settings import ACCOUNT_SERVICE_BASE_URL, ACCOUNT_SERVICE_BASE_URL_SOCIAL from app.utilities.json import json_loads -from tests.integration.integration_test_case import IntegrationTestCase +from tests.app.services.test_request_supplementary_data import TEST_SDS_URL +from tests.integration.create_token import PAYLOAD_V2_SUPPLEMENTARY_DATA +from tests.integration.integration_test_case import ( + EQ_SUBMISSION_SDX_PRIVATE_KEY, + EQ_SUBMISSION_SR_PRIVATE_SIGNING_KEY, + EQ_USER_AUTHENTICATION_RRM_PRIVATE_KEY_KID, + KEYS_DICT, + SR_USER_AUTHENTICATION_PUBLIC_KEY_KID, + IntegrationTestCase, +) TIME_TO_FREEZE = datetime(2020, 1, 1, 12, 0, 0, tzinfo=timezone.utc) EQ_SESSION_TIMEOUT_SECONDS = 45 * 60 +BUSINESS_URL = ACCOUNT_SERVICE_BASE_URL +SOCIAL_URL = ACCOUNT_SERVICE_BASE_URL_SOCIAL +TEST_SDS_URL = "http://localhost:5003/v1/unit_data" + +mock_supplementary_data_payload_missing_data = { + "dataset_id": "44f1b432-9421-49e5-bd26-e63e18a30b69", + "survey_id": "123", +} + +mock_supplementary_data_payload_invalid_kid_in_data = { + "dataset_id": "44f1b432-9421-49e5-bd26-e63e18a30b69", + "survey_id": "123", + # pylint: disable-next=line-too-long + "data": "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00iLCJraWQiOiJkZjg4ZmRhZDI2MTJhZTFlODA1NzExMjBlNmM2MzcxZjU1ODk2Njk3In0.lssJXsMUE3dhWtQRUt7DTaZJvx4DpNdLW98cu8g4NijYX9TFpJiOFyzPxUlpFZb-fMa4zW9q6qZofQeQTbl_Ae3QAwGhuWF7v9NMdWM1aH377byyJJyJpdqlU4t-P03evRWZqAG2HtsNE2Zn1ORXn80Dc9IRkzutgrziLI8OBIZeO6-XEgbVCapsQApWkyux7QRdFH95wfda75nVvGqTbBOYvQiMTKd8KzpH2Vl200IOqEpmrcjUCE-yqdTupzcr88hwNI2ZYdv-pTNowJw1FPODZ7V_sE4Ac-JYv3yBTDcXdz3I5-rX8i2HXqz-g3VhveZiAl9q0AgklPkaO_oNWJzjrCb7DZGL4DjiGYuOcw8OSdOpKLXwkExMlado-wigxy1IWoCzFu2E5tWpmLc0WWcjKuBgD7-4tcn059F7GcwhX2uMRESCmc39pblvseM2UnmmQnwr8GvD7gqWdFwtBsECyXQ5UXAxWLJor_MtU8lAFZxiorRcrXZJwAivroPO9iEB-1Mvt2zZFWI_vMgpJCAIpETscotDKMVCG0UMfkKckJqLnmQpvF4oYTr77w1COBX5bi-AV8UrLJ7sVVktSXOBc_KCGRpoImA5cE67hW7mFUdJi1EHA39qt0tTqZD7izpu8sSLxsiuCkfsqrd4uAedcDdQm4QGxXOPD4pxois.wfWsetB3M0x9qfw5.43Wns86lGlbHj63b0ZxE2bxBQVus6FIqelb9LfSbvopLn5oR8FM4vDEnDp_rIyvjmV9YAZJ6HAHaYaWoNyIO0EorgamrB4R3-LqInANoe9c8xLZ9wl_QpE9aWnxsmFGZUWLO3q2fVTPnwBtA_LxK8FD0vjdLL9eHGYEmPVCGVX0BJX04TVW9aoemsx9Yn3ZtfvmQHuROiB-GcA5wOSb-GvhzfplY09GQr7g7221MiYCHYimmEJyxLV5clWPXu6izzVLDyG9l2ewCifiuBLD0O1U_fPlahHTmidwHKJEAEn39biNw5E_dr8WyZ3xBvJa9dP50m0xeyN4COR-xlYcEbuDcKoqN6BnY0bMNDxQYlBO--QcPLQ6h48uTJszwzsmNIwHoi0xy5dQah7c9Nt2lpMuNt1Wix-O8JWYCqaiCKxjwt9G8kabMbzhp1n3LetWweoyV7qJTbiB13Byv6SZwMO9M.8j8wtvwBAHzqRhv5Ii9jjQ", +} class TestSession(IntegrationTestCase): @@ -26,12 +58,23 @@ def setUp(self): } ) + def assert_supplementary_data_500_page(self): + self.launchSupplementaryDataSurvey() + self.assertStatusCode(500) + self.assertInBody("Sorry, there is a problem with this service") + def test_session_expired(self): self.get("/session-expired") self.assertInBody("Sorry, you need to sign in again") + self.assertInBody( + f'

    If you are completing a business survey, you need to sign back in to your account.

    ' + ) + self.assertInBody( + f'

    If you started your survey using an access code, you need to re-enter your code.

    ' + ) def test_session_jti_token_expired(self): - self.launchSurvey(exp=time.time() - float(60)) + self.launchSurveyV2(exp=time.time() - float(60)) self.assertStatusUnauthorised() def test_head_request_on_session_expired(self): @@ -39,12 +82,13 @@ def test_head_request_on_session_expired(self): self.assertStatusOK() def test_head_request_on_session_signed_out(self): - self.head("/signed-out") + self.launchSurveyV2(schema_name="test_introduction") + self.get("/signed-out") self.assertStatusOK() @freeze_time(TIME_TO_FREEZE) def test_get_session_expiry_doesnt_extend_session(self): - self.launchSurvey() + self.launchSurveyV2() # Advance time by 20 mins... with freeze_time(TIME_TO_FREEZE + timedelta(minutes=20)): self.get("/session-expiry") @@ -61,7 +105,7 @@ def test_get_session_expiry_doesnt_extend_session(self): @freeze_time(TIME_TO_FREEZE) def test_patch_session_expiry_extends_session(self): - self.launchSurvey() + self.launchSurveyV2() # Advance time by 20 mins... request_time = TIME_TO_FREEZE + timedelta(minutes=20) with freeze_time(request_time): @@ -77,23 +121,175 @@ def test_patch_session_expiry_extends_session(self): self.assertIn("expires_at", parsed_json) self.assertEqual(parsed_json["expires_at"], expected_expires_at) + @patch("app.routes.session.get_supplementary_data_v1") + @patch("app.routes.session._validate_supplementary_data_lists") + @patch( + "app.questionnaire.questionnaire_store_updater.QuestionnaireStoreUpdaterBase.set_supplementary_data", + ) + def test_supplementary_data_is_loaded_with_correct_identifier_when_new_sds_dataset_id_in_metadata( + self, + mock_set, + mock_validate, + mock_get, + ): + self.launchSupplementaryDataSurvey() + self.assertStatusOK() + mock_get.assert_called_once() + mock_set.assert_called_once() + mock_validate.assert_called_once() -class TestCensusSession(IntegrationTestCase): - def setUp(self): - # Cache for requests - self.last_url = None - self.last_response = None - self.last_csrf_token = None - self.redirect_url = None + used_identifier = mock_get.call_args.kwargs["identifier"] + ru_ref = PAYLOAD_V2_SUPPLEMENTARY_DATA["survey_metadata"]["data"]["ru_ref"] + assert used_identifier == get_ru_ref_without_check_letter(ru_ref) + assert used_identifier != ru_ref - # Perform setup steps - self._set_up_app(setting_overrides={"SURVEY_TYPE": "census"}) + @patch("app.routes.session.get_supplementary_data_v1") + @patch("app.routes.session._validate_supplementary_data_lists") + @patch( + "app.questionnaire.questionnaire_store_updater.QuestionnaireStoreUpdaterBase.set_supplementary_data", + ) + def test_supplementary_data_is_reloaded_when_changed_sds_dataset_id_in_metadata( + self, + mock_set, + mock_validate, + mock_get, + ): + self.launchSupplementaryDataSurvey(response_id="1", sds_dataset_id="first") + self.assertStatusOK() + mock_set.assert_called_once() + mock_get.assert_called_once() + mock_validate.assert_called_once() + self.launchSupplementaryDataSurvey(response_id="1", sds_dataset_id="second") + self.assertStatusOK() + self.assertEqual(mock_get.call_count, 2) + self.assertEqual(mock_set.call_count, 2) + self.assertEqual(mock_validate.call_count, 2) - def test_session_signed_out_no_cookie_session_census_config(self): - self.launchSurvey(schema_name="test_individual_response") - self.assertInBody("Save and sign out") + @patch("app.routes.session.get_supplementary_data_v1") + @patch("app.routes.session._validate_supplementary_data_lists") + @patch( + "app.questionnaire.questionnaire_store_updater.QuestionnaireStoreUpdaterBase.set_supplementary_data", + ) + def test_supplementary_data_is_not_reloaded_when_same_sds_dataset_id_in_metadata( + self, + mock_set, + mock_validate, + mock_get, + ): + self.launchSupplementaryDataSurvey(response_id="1", sds_dataset_id="same") + self.assertStatusOK() + mock_set.assert_called_once() + mock_get.assert_called_once() + mock_validate.assert_called_once() + self.launchSupplementaryDataSurvey(response_id="1", sds_dataset_id="same") + self.assertStatusOK() + mock_get.assert_called_once() + mock_set.assert_called_once() + # validation should happen twice regardless + self.assertEqual(mock_validate.call_count, 2) + + def test_supplementary_data_raises_500_error_when_sds_api_request_fails(self): + with patch( + "app.routes.session.get_supplementary_data_v1", + side_effect=SupplementaryDataRequestFailed, + ): + self.assert_supplementary_data_500_page() + + @responses.activate + def test_supplementary_data_raises_500_error_when_supplementary_data_invalid(self): + responses.add( + responses.GET, + TEST_SDS_URL, + json=mock_supplementary_data_payload_invalid_kid_in_data, + status=200, + ) + self.assert_supplementary_data_500_page() + + @responses.activate + def test_supplementary_data_raises_500_error_when_supplementary_data_missing_data( + self, + ): + responses.add( + responses.GET, + TEST_SDS_URL, + json=mock_supplementary_data_payload_missing_data, + status=200, + ) + self.assert_supplementary_data_500_page() - self.deleteCookie() - self.get("/sign-out", follow_redirects=False) + def test_supplementary_data_raises_500_error_when_missing_supplementary_data_key( + self, + ): + self.key_store = KeyStore( + { + "keys": { + k: KEYS_DICT["keys"][k] + for k in ( + EQ_USER_AUTHENTICATION_RRM_PRIVATE_KEY_KID, + SR_USER_AUTHENTICATION_PUBLIC_KEY_KID, + EQ_SUBMISSION_SDX_PRIVATE_KEY, + EQ_SUBMISSION_SR_PRIVATE_SIGNING_KEY, + ) + } + } + ) + + self.assert_supplementary_data_500_page() + + @patch("app.routes.session.get_supplementary_data_v1") + @patch( + "app.questionnaire.questionnaire_store_updater.QuestionnaireStoreUpdaterBase.set_supplementary_data", + ) + def test_supplementary_data_raises_500_error_when_missing_required_lists( + self, mock_set, mock_get + ): + """Tests that if the supplementary data being loaded does not cover all the dependent lists for the schema + that a validation error is raised""" + mock_get.return_value = {"data": {"items": {"products": []}}} + self.launchSupplementaryDataSurvey(schema_name="test_supplementary_data") + self.assertStatusCode(500) + mock_set.assert_not_called() - self.assertInRedirect("census.gov.uk") + @patch("app.routes.session.get_supplementary_data_v1") + @patch( + "app.questionnaire.questionnaire_store_updater.QuestionnaireStoreUpdaterBase.set_supplementary_data", + ) + def test_supplementary_data_is_loaded_when_all_required_lists_present( + self, mock_set, mock_get + ): + mock_get.return_value = {"data": {"items": {"employees": [], "products": []}}} + self.launchSupplementaryDataSurvey(schema_name="test_supplementary_data") + self.assertStatusOK() + mock_set.assert_called_once() + + @patch("app.routes.session.get_supplementary_data_v1") + @patch( + "app.routes.session._validate_supplementary_data_lists", + side_effect=[ + None, + ValidationError( + "Supplementary data does not include the following lists required for the schema: missing" + ), + ], + ) + @patch( + "app.questionnaire.questionnaire_store_updater.QuestionnaireStoreUpdaterBase.set_supplementary_data", + ) + def test_supplementary_data_raises_500_error_when_survey_becomes_invalid_for_same_dataset( + self, + mock_set, + mock_validate, + mock_get, + ): + """ + This checks the edge case in which a survey changes to have different lists, but the supplementary dataset id + remains the same, so the supplementary data is not fetched again, but is no longer valid for the survey + """ + self.launchSupplementaryDataSurvey(response_id="1", sds_dataset_id="same") + self.assertStatusOK() + mock_set.assert_called_once() + mock_get.assert_called_once() + mock_validate.assert_called_once() + self.launchSupplementaryDataSurvey(response_id="1", sds_dataset_id="same") + self.assertStatusCode(500) + self.assertEqual(mock_validate.call_count, 2) diff --git a/tests/integration/routes/test_thank_you.py b/tests/integration/routes/test_thank_you.py index 82cfd59128..5f4d26a3ba 100644 --- a/tests/integration/routes/test_thank_you.py +++ b/tests/integration/routes/test_thank_you.py @@ -5,20 +5,21 @@ class TestThankYou(IntegrationTestCase): def test_thank_you_page_no_sign_out(self): - self.launchSurvey("test_currency") + self.launchSurveyV2(schema_name="test_currency") # We fill in our answers form_data = { - "answer": "12", + "answer-gbp": "12", "answer-usd": "345", "answer-eur": "67.89", "answer-jpy": "0", } - # We submit the form + # We submit the form and answers for the first page + self.post(form_data) + self.post() + # We submit the form and answers for the second page self.post(form_data) - - # Submit answers self.post() # check we're on the thank you page and there's no sign out button @@ -26,7 +27,7 @@ def test_thank_you_page_no_sign_out(self): self.assertIsNone(self.getSignOutButton()) def test_can_switch_language_on_thank_you_page(self): - self.launchSurvey("test_language") + self.launchSurveyV2(schema_name="test_language") self.post() # We fill in our answers self.post({"first-name": "Kevin", "last-name": "Bacon"}) @@ -60,14 +61,14 @@ def test_can_switch_language_on_thank_you_page(self): self.assertNotInBody("Cymraeg") def test_head_request_on_thank_you(self): - self.launchSurvey("test_confirmation_email") + self.launchSurveyV2(schema_name="test_confirmation_email") self.post() self.post() self.head("/submitted/thank-you") self.assertStatusOK() def test_options_request_post_submission_before_request(self): - self.launchSurvey("test_confirmation_email") + self.launchSurveyV2(schema_name="test_confirmation_email") self.post() self.post() @@ -79,15 +80,17 @@ def test_options_request_post_submission_before_request(self): self.assertNotIn("questionnaire request", output) def test_default_guidance(self): - self.launchSurvey("test_textfield") + self.launchSurveyV2(schema_name="test_textfield") self.post({"name-answer": "Adam"}) self.post() self.assertInUrl("thank-you") - self.assertInBody("Your answers will be processed in the next few weeks.") + self.assertInBody( + "Your response will help inform decision-makers how best to support the UK population and economy." + ) def test_custom_guidance(self): - self.launchSurvey("test_thank_you") + self.launchSurveyV2(schema_name="test_thank_you") self.post({"answer": "Yes"}) self.post() @@ -96,7 +99,7 @@ def test_custom_guidance(self): self.assertInBody('Important link') def test_back_to_surveys_link_on_thank_you(self): - self.launchSurvey("test_thank_you") + self.launchSurveyV2(schema_name="test_thank_you") self.post({"answer": "Yes"}) self.post() @@ -104,8 +107,17 @@ def test_back_to_surveys_link_on_thank_you(self): self.assertInBody("Back to surveys") self.assertInBody(ACCOUNT_SERVICE_TODO_PATH) + def test_back_to_surveys_link_not_on_thank_you_theme_social(self): + self.launchSurveyV2(schema_name="test_theme_social", theme="social") + self.post() + self.post() + + self.assertInUrl("thank-you") + self.assertNotInBody("Back to surveys") + self.assertNotInBody(ACCOUNT_SERVICE_TODO_PATH) + def test_view_answers_after_submission_guidance(self): - self.launchSurvey("test_thank_you") + self.launchSurveyV2(schema_name="test_thank_you") self.post({"answer": "Yes"}) self.post() diff --git a/tests/integration/routes/test_view_preview_questions.py b/tests/integration/routes/test_view_preview_questions.py new file mode 100644 index 0000000000..cfca86e921 --- /dev/null +++ b/tests/integration/routes/test_view_preview_questions.py @@ -0,0 +1,45 @@ +from tests.integration.integration_test_case import IntegrationTestCase + + +class TestPreviewPDF(IntegrationTestCase): + def test_download_pdf(self): + super().setUp() + + # Given I launch a questionnaire and open preview of questions + self.launchSurveyV2(schema_name="test_introduction") + self.get("/questionnaire/preview/") + + # When I try to download preview of questions from the preview page + download_pdf_url = ( + self.getHtmlSoup() + .find("a", {"href": "/questionnaire/preview/download-pdf"}) + .attrs["href"] + ) + + self.get(download_pdf_url) + + # Then I get 200 status code + self.assertStatusOK() + + def test_download_pdf_no_preview(self): + super().setUp() + + # Given I launch a questionnaire without preview enabled + self.launchSurveyV2(schema_name="test_checkbox") + + # When I try to download preview of questions + self.get("/questionnaire/preview/download-pdf") + + # Then I get 404 status code + self.assertStatusNotFound() + + def test_print_button(self): + super().setUp() + + # Given I launch a questionnaire and open preview of questions + self.launchSurveyV2(schema_name="test_introduction") + self.get("/questionnaire/preview/") + + # Then the print button is displayed correctly + print_button = self.getHtmlSoup().find("button", {"data-qa": "btn-print"}) + self.assertIsNotNone(print_button) diff --git a/tests/integration/routes/test_view_submitted_response.py b/tests/integration/routes/test_view_submitted_response.py index 2e4f92e41d..4fa9dd0f47 100644 --- a/tests/integration/routes/test_view_submitted_response.py +++ b/tests/integration/routes/test_view_submitted_response.py @@ -6,7 +6,7 @@ class ViewSubmittedResponseBase(IntegrationTestCase): VIEW_RESPONSE_PAGE_URL = "/submitted/view-response" def _launch_and_complete_questionnaire(self): - self.launchSurvey("test_view_submitted_response") + self.launchSurveyV2(schema_name="test_view_submitted_response") self.post({"name-answer": "John Smith"}) self.post({"address-answer": "NP10 8XG"}) self.post() @@ -61,7 +61,7 @@ def test_enabled(self): def test_not_enabled(self): # Given I launch and complete a questionnaire that does not have view-submitted-response enabled - self.launchSurvey("test_confirmation_email") + self.launchSurveyV2(schema_name="test_confirmation_email") self.post() self.post() diff --git a/tests/integration/routes/test_view_submitted_response_pdf.py b/tests/integration/routes/test_view_submitted_response_pdf.py index 55582fac82..4eb0528dae 100644 --- a/tests/integration/routes/test_view_submitted_response_pdf.py +++ b/tests/integration/routes/test_view_submitted_response_pdf.py @@ -1,5 +1,7 @@ from time import sleep +from freezegun import freeze_time + from app import settings from tests.integration.routes.test_view_submitted_response import ( ViewSubmittedResponseBase, @@ -9,7 +11,7 @@ class TestViewSubmissionResponsePDF(ViewSubmittedResponseBase): def test_download_when_submitted_response_not_enabled(self): # Given I launch and complete a questionnaire that does not have view-submitted-response enabled - self.launchSurvey("test_confirmation_email") + self.launchSurveyV2(schema_name="test_confirmation_email") self.post() self.post() @@ -19,6 +21,7 @@ def test_download_when_submitted_response_not_enabled(self): # Then a 404 page is returned self.assertStatusNotFound() + @freeze_time("2022-06-01T15:34:54+00:00") def test_download_when_submitted_response_enabled_but_not_expired(self): # Given I launch and complete a questionnaire that has view-submitted-response enabled and has not expired self._launch_and_complete_questionnaire() @@ -38,13 +41,13 @@ def test_download_when_submitted_response_enabled_but_not_expired(self): # Check filename is set as expected self.assertIn( - "filename=test_view_submitted_response.pdf", + "filename=test-view-submitted-response-2022-06-01.pdf", self.last_response_headers["Content-Disposition"], ) # Check content length is reasonable. # This is given some leeway as it can change with DS changes. - self.assertGreater(self.last_response.content_length, 16500) + self.assertGreater(self.last_response.content_length, 10000) def test_download_when_submitted_response_enabled_but_expired(self): settings.VIEW_SUBMITTED_RESPONSE_EXPIRATION_IN_SECONDS = 3 diff --git a/tests/integration/routing/test_answer_comparison.py b/tests/integration/routing/test_answer_comparison.py index ff80afce59..ca35ad1a00 100644 --- a/tests/integration/routing/test_answer_comparison.py +++ b/tests/integration/routing/test_answer_comparison.py @@ -8,7 +8,7 @@ class TestAnswerComparisonsSkips(IntegrationTestCase): """ def test_skip_condition_answer_comparison(self): - self.launchSurvey("test_new_skip_condition_answer_comparison") + self.launchSurveyV2(schema_name="test_skip_condition_answer_comparison") self.post(action="start_questionnaire") @@ -42,7 +42,7 @@ class TestAnswerComparisonsRoutes(IntegrationTestCase): """ def test_routes_over_interstitial(self): - self.launchSurvey("test_new_routing_answer_comparison") + self.launchSurveyV2(schema_name="test_routing_answer_comparison") self.post(action="start_questionnaire") diff --git a/tests/integration/routing/test_routing_case_insensitive.py b/tests/integration/routing/test_routing_case_insensitive.py index d227b4addc..a54cc2d178 100644 --- a/tests/integration/routing/test_routing_case_insensitive.py +++ b/tests/integration/routing/test_routing_case_insensitive.py @@ -7,7 +7,7 @@ class TestCaseInsensitiveRouting(IntegrationTestCase): """ def test_routing_equals_any_case_insensitive(self): - self.launchSurvey("test_new_routing_case_insensitive_text_field") + self.launchSurveyV2(schema_name="test_routing_case_insensitive_text_field") # Given I launch the case insensiitive routing rules schema self.post(action="start_questionnaire") @@ -19,7 +19,7 @@ def test_routing_equals_any_case_insensitive(self): self.assertInBody("You submitted India or Azerbaijan.") def test_routing_equals_case_insensitive(self): - self.launchSurvey("test_new_routing_case_insensitive_text_field") + self.launchSurveyV2(schema_name="test_routing_case_insensitive_text_field") # Given I launch the case insensiitive routing rules schema self.post(action="start_questionnaire") diff --git a/tests/integration/session/test_login.py b/tests/integration/session/test_login.py index ed5d576189..72a428c0d9 100644 --- a/tests/integration/session/test_login.py +++ b/tests/integration/session/test_login.py @@ -2,8 +2,11 @@ from httmock import HTTMock, response, urlmatch -from app.utilities.schema import get_schema_path_map -from tests.integration.create_token import PAYLOAD +from app.utilities.schema import ( + CIR_RETRIEVE_COLLECTION_INSTRUMENT_URL, + get_schema_path_map, +) +from tests.integration.create_token import PAYLOAD_V2_BUSINESS from tests.integration.integration_test_case import IntegrationTestCase SCHEMA_PATH_MAP = get_schema_path_map(include_test_schemas=True) @@ -30,9 +33,22 @@ def test_login_with_invalid_token_should_be_forbidden(self): # Then self.assertStatusForbidden() - def test_login_with_valid_token_should_redirect_to_survey(self): + def test_login_with_valid_v2_business_token_should_redirect_to_survey(self): + # Given + token = self.token_generator.create_token_v2(schema_name="test_checkbox") + + # When + self.get(url=f"/session?token={token}") + + # Then + self.assertStatusOK() + self.assertInUrl("/questionnaire") + + def test_login_with_valid_v2_social_token_should_redirect_to_survey(self): # Given - token = self.token_generator.create_token("test_checkbox") + token = self.token_generator.create_token_v2( + schema_name="test_theme_social", theme="social" + ) # When self.get(url=f"/session?token={token}") @@ -43,7 +59,7 @@ def test_login_with_valid_token_should_redirect_to_survey(self): def test_login_with_token_twice_is_unauthorised_when_same_jti_provided(self): # Given - token = self.token_generator.create_token("test_checkbox") + token = self.token_generator.create_token_v2("test_checkbox") self.get(url=f"/session?token={token}") # When @@ -60,9 +76,19 @@ def test_login_without_jti_in_token_is_unauthorised(self): # Then self.assertStatusForbidden() - def test_login_with_valid_token_no_schema_name(self): + def test_login_with_valid_v2_business_token_no_schema_name(self): + # Given + token = self.token_generator.create_token_v2(schema_name="") + + # When + self.get(url=f"/session?token={token}") + + # Then + self.assertStatusForbidden() + + def test_login_with_valid_v2_social_token_no_schema_name(self): # Given - token = self.token_generator.create_token("") + token = self.token_generator.create_token_v2(schema_name="", theme="social") # When self.get(url=f"/session?token={token}") @@ -72,7 +98,7 @@ def test_login_with_valid_token_no_schema_name(self): def test_http_head_request_to_login_returns_successfully_and_get_still_works(self): # Given - token = self.token_generator.create_token("test_checkbox") + token = self.token_generator.create_token_v2("test_checkbox") # When self.head("/session?token=" + token) @@ -84,7 +110,7 @@ def test_http_head_request_to_login_returns_successfully_and_get_still_works(sel def test_login_with_missing_mandatory_claims_should_be_forbidden(self): # Given - payload_vars = PAYLOAD.copy() + payload_vars = PAYLOAD_V2_BUSINESS.copy() payload_vars["iat"] = time.time() payload_vars["exp"] = payload_vars["iat"] + float(3600) # one hour from now @@ -96,47 +122,52 @@ def test_login_with_missing_mandatory_claims_should_be_forbidden(self): # Then self.assertStatusForbidden() - def test_login_with_invalid_questionnaire_claims_should_be_forbidden(self): + def test_login_with_invalid_questionnaire_claims_should_be_forbidden_v2_get(self): # flag_1 should be a boolean - token = self.token_generator.create_token("test_metadata_routing", flag_1=123) + token = self.token_generator.create_token_v2( + "test_metadata_routing", flag_1=123 + ) self.get(url=f"/session?token={token}") self.assertStatusForbidden() - def test_login_token_with_survey_url_should_redirect_to_survey(self): - survey_url = "http://eq-survey-register.url/my-test-schema" + def test_login_with_invalid_version_should_be_forbidden(self): + token = self.token_generator.create_token_invalid_version("test_checkbox") + + self.get(url=f"/session?token={token}") + + self.assertStatusForbidden() + + def test_login_token_with_schema_url_should_redirect_to_survey(self): + schema_url = "http://eq-survey-register.url/my-test-schema" # Given - token = self.token_generator.create_token_with_survey_url( - "test_textarea", survey_url - ) + token = self.token_generator.create_token_with_schema_url(schema_url) # When - with HTTMock(self.survey_url_mock): + with HTTMock(self._schema_url_mock): self.get(url=f"/session?token={token}") self.assertStatusOK() self.assertInUrl("/questionnaire") - def test_login_token_with_incorrect_survey_url_results_in_404(self): - survey_url = "http://eq-survey-register.url/my-test-schema-not-found" + def test_login_token_with_incorrect_schema_url_results_in_500(self): + schema_url = "http://eq-survey-register.url/my-test-schema-not-found" # Given - token = self.token_generator.create_token_with_survey_url( - "test_textarea", survey_url - ) + token = self.token_generator.create_token_with_schema_url(schema_url) # When - with HTTMock(self.survey_url_mock_404): + with HTTMock(self._schema_url_mock_500): self.get(url=f"/session?token={token}") # Then - self.assertStatusNotFound() + self.assertException() @staticmethod @urlmatch(netloc=r"eq-survey-register", path=r"\/my-test-schema") - def survey_url_mock(_url, _request): + def _schema_url_mock(_url, _request): schema_path = SCHEMA_PATH_MAP["test"]["en"]["test_textarea"] with open(schema_path, encoding="utf8") as json_data: @@ -144,8 +175,8 @@ def survey_url_mock(_url, _request): @staticmethod @urlmatch(netloc=r"eq-survey-register", path=r"\/my-test-schema-not-found") - def survey_url_mock_404(_url, _request): - return response(404) + def _schema_url_mock_500(_url, _request): + return response(500) class TestLoginWithPostRequest(IntegrationTestCase): @@ -171,7 +202,7 @@ def test_login_with_invalid_token_should_be_forbidden(self): def test_login_with_valid_token_should_redirect_to_survey(self): # Given - token = self.token_generator.create_token("test_checkbox") + token = self.token_generator.create_token_v2("test_checkbox") # When self.post(url=f"/session?token={token}") @@ -182,7 +213,7 @@ def test_login_with_valid_token_should_redirect_to_survey(self): def test_login_with_token_twice_is_unauthorised_when_same_jti_provided(self): # Given - token = self.token_generator.create_token("test_checkbox") + token = self.token_generator.create_token_v2("test_checkbox") self.post(url=f"/session?token={token}") # When @@ -201,10 +232,10 @@ def test_login_without_jti_in_token_is_unauthorised(self): def test_http_head_request_to_login_returns_successfully_and_post_still_works(self): # Given - token = self.token_generator.create_token("test_checkbox") + token = self.token_generator.create_token_v2("test_checkbox") # When - self.head("/session?token=" + token) + self.head(f"/session?token={token}") self.post(url=f"/session?token={token}") # Then @@ -213,7 +244,7 @@ def test_http_head_request_to_login_returns_successfully_and_post_still_works(se def test_login_with_missing_mandatory_claims_should_be_forbidden(self): # Given - payload_vars = PAYLOAD.copy() + payload_vars = PAYLOAD_V2_BUSINESS.copy() payload_vars["iat"] = time.time() payload_vars["exp"] = payload_vars["iat"] + float(3600) # one hour from now @@ -225,43 +256,75 @@ def test_login_with_missing_mandatory_claims_should_be_forbidden(self): # Then self.assertStatusForbidden() - def test_login_with_invalid_questionnaire_claims_should_be_forbidden(self): + def test_login_with_invalid_questionnaire_claims_should_be_forbidden_v2_post(self): # flag_1 should be a boolean - token = self.token_generator.create_token("test_metadata_routing", flag_1=123) + token = self.token_generator.create_token_v2( + "test_metadata_routing", flag_1=123 + ) + + self.get(url=f"/session?token={token}") + + self.assertStatusForbidden() + + def test_v2_business_login_with_invalid_questionnaire_claims_should_be_forbidden( + self, + ): + # flag_1 should be a boolean + token = self.token_generator.create_token_v2( + "test_metadata_routing", flag_1=123 + ) self.post(url=f"/session?token={token}") self.assertStatusForbidden() - def test_login_token_with_survey_url_should_redirect_to_survey(self): - survey_url = "http://eq-survey-register.url/my-test-schema" + def test_v2_social_login_with_invalid_questionnaire_claims_should_be_forbidden( + self, + ): + token = self.token_generator.create_token_v2( + schema_name="test_address", theme="social" + ) - # Given - token = self.token_generator.create_token_with_survey_url( - "test_textarea", survey_url + self.post(url=f"/session?token={token}") + + self.assertStatusForbidden() + + def test_v2_social_login_with_invalid_receipting_key_should_be_forbidden(self): + token = ( + self.token_generator.create_token_v2_social_token_invalid_receipting_key( + "test_theme_social" + ) ) + self.post(url=f"/session?token={token}") + + self.assertStatusForbidden() + + def test_login_token_with_schema_url_should_redirect_to_survey(self): + schema_url = "http://eq-survey-register.url/my-test-schema" + + # Given + token = self.token_generator.create_token_with_schema_url(schema_url) + # When - with HTTMock(self.survey_url_mock): + with HTTMock(self._schema_url_mock): self.post(url=f"/session?token={token}") self.assertStatusOK() self.assertInUrl("/questionnaire") - def test_login_token_with_incorrect_survey_url_results_in_404(self): - survey_url = "http://eq-survey-register.url/my-test-schema-not-found" + def test_login_token_with_incorrect_schema_url_results_in_404(self): + schema_url = "http://eq-survey-register.url/my-test-schema-not-found" # Given - token = self.token_generator.create_token_with_survey_url( - "test_textarea", survey_url - ) + token = self.token_generator.create_token_with_schema_url(schema_url) # When - with HTTMock(self.survey_url_mock_404): + with HTTMock(self._schema_url_mock_500): self.post(url=f"/session?token={token}") # Then - self.assertStatusNotFound() + self.assertException() def test_login_without_case_id_in_token_is_unauthorised(self): # Given @@ -271,15 +334,65 @@ def test_login_without_case_id_in_token_is_unauthorised(self): # Then self.assertStatusForbidden() + def test_login_token_with_cir_instrument_id_should_redirect_to_survey(self): + cir_instrument_id = "f0519981-426c-8b93-75c0-bfc40c66fe25" + + # Given + token = self.token_generator.create_token_with_cir_instrument_id( + cir_instrument_id=cir_instrument_id + ) + + # When + with HTTMock(self._cir_url_mock): + self.post(url=f"/session?token={token}") + + # Then + self.assertStatusOK() + self.assertInUrl("/questionnaire") + + def test_login_token_with_invalid_cir_instrument_id_results_in_500(self): + cir_instrument_id = "a0df1208-dff5-4a3d-b35d-f9620c4a48ef" + + # Given + token = self.token_generator.create_token_with_cir_instrument_id( + cir_instrument_id=cir_instrument_id + ) + + # When + with HTTMock(self._cir_url_mock_500): + self.post(url=f"/session?token={token}") + + # Then + self.assertException() + @staticmethod @urlmatch(netloc=r"eq-survey-register", path=r"\/my-test-schema") - def survey_url_mock(_url, _request): + def _schema_url_mock(_url, _request): schema_path = SCHEMA_PATH_MAP["test"]["en"]["test_textarea"] with open(schema_path, encoding="utf8") as json_data: return json_data.read() + @staticmethod + @urlmatch( + path=CIR_RETRIEVE_COLLECTION_INSTRUMENT_URL, + query="guid=f0519981-426c-8b93-75c0-bfc40c66fe25", + ) + def _cir_url_mock(_url, _request): + schema_path = SCHEMA_PATH_MAP["test"]["en"]["test_textarea"] + + with open(schema_path, encoding="utf8") as json_data: + return json_data.read() + + @staticmethod + @urlmatch( + path=CIR_RETRIEVE_COLLECTION_INSTRUMENT_URL, + query="guid=a0df1208-dff5-4a3d-b35d-f9620c4a48ef", + ) + def _cir_url_mock_500(_url, _request): + return response(500) + @staticmethod @urlmatch(netloc=r"eq-survey-register", path=r"\/my-test-schema-not-found") - def survey_url_mock_404(_url, _request): - return response(404) + def _schema_url_mock_500(_url, _request): + return response(500) diff --git a/tests/integration/session/test_multiple_login.py b/tests/integration/session/test_multiple_login.py index 6b4a436007..e58504c88b 100644 --- a/tests/integration/session/test_multiple_login.py +++ b/tests/integration/session/test_multiple_login.py @@ -13,8 +13,8 @@ def setUp(self): self.cache = {} - def launchSurvey(self, client, schema_name="test_textfield", **payload_kwargs): - token = self.token_generator.create_token(schema_name, **payload_kwargs) + def launchSurvey(self, client, schema_name, **payload_kwargs): + token = self.token_generator.create_token_v2(schema_name, **payload_kwargs) self.get(client, "/session?token=" + token) # pylint: disable=arguments-renamed @@ -81,11 +81,11 @@ def test_multiple_users_same_survey(self): input_data = "foo bar" # user A inputs an answer - self.launchSurvey(self.client_a, "test_textfield") + self.launchSurvey(self.client_a, schema_name="test_textfield") self.post(self.client_a, {"name-answer": input_data}) # user B gets taken straight to summary as survey is complete - self.launchSurvey(self.client_b, "test_textfield") + self.launchSurvey(self.client_b, schema_name="test_textfield") last_url_b = self.cache[self.client_b]["last_url"] self.assertIn(SUBMIT_URL_PATH, last_url_b) @@ -110,7 +110,9 @@ def test_concurrent_users_same_survey_different_languages(self): """ # user A launches the test language questionnaire in English - self.launchSurvey(self.client_a, "test_language", language_code="en") + self.launchSurvey( + self.client_a, schema_name="test_language", language_code="en" + ) self.post(self.client_a) last_response_a = self.cache[self.client_a]["last_response"] self.assertIn("Please enter a name", last_response_a.get_data(True)) @@ -127,7 +129,9 @@ def test_concurrent_users_same_survey_different_languages(self): self.assertIn("Please enter a name", last_response_a.get_data(True)) # user B launches the same questionnaire but in Welsh - self.launchSurvey(self.client_b, "test_language", language_code="cy") + self.launchSurvey( + self.client_b, schema_name="test_language", language_code="cy" + ) self.post(self.client_b) last_response_b = self.cache[self.client_b]["last_response"] self.assertIn("Rhowch enw", last_response_b.get_data(True)) @@ -169,7 +173,9 @@ def test_multiple_logins_have_same_started_at(self): Ensure that started_at is retained between collections """ # User A starts a survey - self.launchSurvey(self.client_a, "test_introduction", roles=["dumper"]) + self.launchSurvey( + self.client_a, schema_name="test_introduction", roles=["dumper"] + ) # And starts the questionnaire self.post(self.client_a, action="start_questionnaire") @@ -177,7 +183,9 @@ def test_multiple_logins_have_same_started_at(self): a_submission = self.dumpSubmission(self.client_a)["submission"] # User B loads the survey - self.launchSurvey(self.client_b, "test_introduction", roles=["dumper"]) + self.launchSurvey( + self.client_b, schema_name="test_introduction", roles=["dumper"] + ) # And we dump their submission b_submission = self.dumpSubmission(self.client_b)["submission"] diff --git a/tests/integration/session/test_sign_out_and_exit.py b/tests/integration/session/test_sign_out_and_exit.py index 7555690ea9..cdf9e332c1 100644 --- a/tests/integration/session/test_sign_out_and_exit.py +++ b/tests/integration/session/test_sign_out_and_exit.py @@ -3,6 +3,7 @@ SIGN_OUT_URL_PATH = "/sign-out" SIGNED_OUT_URL_PATH = "/signed-out" +SESSION_EXPIRED_PATH = "/session-expired" ACCOUNT_SERVICE_BASE_URL = "http://localhost" ACCOUNT_SERVICE_LOG_OUT_URL_PATH = "/sign-in/logout" ACCOUNT_SERVICE_LOG_OUT_URL = ( @@ -15,20 +16,22 @@ class TestSaveAndSignOut(IntegrationTestCase): def test_sign_out_button_link(self): - self.launchSurvey("test_textfield") - self.assertEqual("/sign-out?todo=True", self.getSignOutButton()["href"]) + self.launchSurveyV2(schema_name="test_textfield") + self.assertEqual( + "/sign-out?internal_redirect=True", self.getSignOutButton()["href"] + ) def test_sign_out_url(self): - self.launchSurvey("test_textfield") + self.launchSurveyV2(schema_name="test_textfield") self.saveAndSignOut() - self.assertInRedirect("/surveys/todo") + self.assertInRedirect(SIGNED_OUT_URL_PATH) def test_sign_out_button_text(self): - self.launchSurvey("test_textfield") + self.launchSurveyV2(schema_name="test_textfield") self.assertEqual("Save and exit survey", self.getSignOutButton().text.strip()) def test_sign_out_button_displayed_pre_submission(self): - self.launchSurvey("test_textfield") + self.launchSurveyV2(schema_name="test_textfield") self.assertIsNotNone(self.getSignOutButton()) def test_no_session_cookie_redirects_to_default_account_service_log_out_url(self): @@ -36,16 +39,23 @@ def test_no_session_cookie_redirects_to_default_account_service_log_out_url(self self.get(SIGN_OUT_URL_PATH, follow_redirects=False) self.assertInRedirect(DEFAULT_ACCOUNT_SERVICE_LOG_OUT_URL) + def test_no_session_cookie_signed_out_redirects_to_session_expiry(self): + self.deleteCookie() + self.get(SIGNED_OUT_URL_PATH, follow_redirects=False) + self.assertInRedirect(SESSION_EXPIRED_PATH) + # Test the behaviour when using Hub/No Hub def test_redirects_to_account_service_log_out_url_using_base_url_from_claims(self): for schema in ["test_textfield", "test_hub_and_spoke"]: with self.subTest(schema=schema): - self.launchSurvey(schema, account_service_url=ACCOUNT_SERVICE_BASE_URL) + self.launchSurveyV2( + schema_name=schema, account_service_url=ACCOUNT_SERVICE_BASE_URL + ) self.signOut() self.assertInRedirect(ACCOUNT_SERVICE_LOG_OUT_URL) def test_head_request_doesnt_sign_out(self): - self.launchSurvey("test_textfield") + self.launchSurveyV2(schema_name="test_textfield") self.head(SIGN_OUT_URL_PATH) self.assertStatusCode(302) self.get("/questionnaire/name-block") @@ -54,7 +64,7 @@ def test_head_request_doesnt_sign_out(self): class TestExitPostSubmissionTestCase(IntegrationTestCase): def _launch_and_submit_questionnaire(self, schema, **kwargs): - self.launchSurvey(schema, **kwargs) + self.launchSurveyV2(schema_name=schema, **kwargs) self.post() self.post() self.assertInUrl("/thank-you") @@ -74,40 +84,15 @@ def test_redirects_to_account_service_log_out_url_using_base_url_from_claims(sel class TestExitPostSubmissionWithHubDefaultTheme(IntegrationTestCase): def _launch_and_submit_questionnaire(self, schema, **kwargs): - self.launchSurvey(schema, **kwargs) + self.launchSurveyV2(schema_name=schema, **kwargs) self.post({"household-relationships-answer": "No"}) self.post() self.assertInUrl("/thank-you") def test_redirects_to_account_service_log_out_url_using_base_url_from_claims(self): self._launch_and_submit_questionnaire( - schema="test_new_hub_section_required_and_enabled", + schema="test_hub_section_required_and_enabled", account_service_url=ACCOUNT_SERVICE_BASE_URL, ) self.signOut() self.assertInRedirect(ACCOUNT_SERVICE_LOG_OUT_URL) - - -class TestCensusSignOut(IntegrationTestCase): - def setUp(self): - self._set_up_app(setting_overrides={"SURVEY_TYPE": "census"}) - - def test_sign_out_url(self): - self.launchSurvey(schema_name="test_individual_response") - self.saveAndSignOut() - self.assertInRedirect("census.gov.uk") - - def test_sign_out_button_text(self): - self.launchSurvey(schema_name="test_individual_response") - self.assertEqual( - "Save and complete later", self.getSignOutButton().text.strip() - ) - - def test_no_session_cookie_redirects_to_default_account_service_log_out_url(self): - self.launchSurvey(schema_name="test_individual_response") - self.assertInBody("Save and sign out") - - self.deleteCookie() - self.signOut() - - self.assertInRedirect("census.gov.uk") diff --git a/tests/integration/session/test_timeout.py b/tests/integration/session/test_timeout.py index 5a90df43be..ec13f3877a 100644 --- a/tests/integration/session/test_timeout.py +++ b/tests/integration/session/test_timeout.py @@ -15,12 +15,12 @@ def tearDown(self): super().tearDown() def test_timeout_continue_valid_session_returns_200(self): - self.launchSurvey("test_timeout") + self.launchSurveyV2(schema_name="test_timeout") self.get(self.last_url) self.assertStatusOK() def test_when_session_times_out_server_side_401_is_returned(self): - self.launchSurvey("test_timeout") + self.launchSurveyV2(schema_name="test_timeout") time.sleep(5) self.get(self.last_url) self.assertStatusUnauthorised() @@ -30,10 +30,10 @@ def test_alternate_401_page_is_displayed_when_no_cookie(self): self.get("/session") self.assertStatusUnauthorised() self.assertInBody("followed a link to a page you are not signed in to") - self.assertEqualPageTitle("Page is not available - ONS Business Surveys") + self.assertEqualPageTitle("Page is not available - ONS Surveys") def test_schema_defined_timeout_cant_be_higher_than_server(self): - self.launchSurvey("test_timeout") + self.launchSurveyV2(schema_name="test_timeout") time.sleep(4) self.get(self.last_url) self.assertStatusUnauthorised() @@ -43,7 +43,7 @@ def test_schema_defined_timeout_cant_be_higher_than_server(self): self.assertEqualPageTitle("Page is not available - Timeout test") def test_submission_complete_timeout(self): - self.launchSurvey("test_timeout") + self.launchSurveyV2(schema_name="test_timeout") self.post() self.post() time.sleep(4) diff --git a/tests/integration/test_app_create.py b/tests/integration/test_app_create.py index a0e78a1540..4fd52d4f52 100644 --- a/tests/integration/test_app_create.py +++ b/tests/integration/test_app_create.py @@ -9,8 +9,10 @@ from mock import patch from app.cloud_tasks import CloudTaskPublisher +from app.oidc.gcp_oidc import OIDCCredentialsServiceGCP +from app.oidc.local_oidc import OIDCCredentialsServiceLocal from app.publisher import LogPublisher, PubSubPublisher -from app.setup import create_app +from app.setup import MissingEnvironmentVariable, create_app from app.storage.datastore import Datastore from app.storage.dynamodb import Dynamodb from app.submitter.submitter import ( @@ -70,17 +72,17 @@ def test_adds_i18n_to_application(self): self.assertIsInstance(babel, Babel) def test_adds_logging_of_request_ids(self): - with patch("app.setup.logger") as logger: + with patch("structlog.contextvars.bind_contextvars") as bind_contextvars: self._setting_overrides.update({"EQ_APPLICATION_VERSION": False}) application = create_app(self._setting_overrides) application.test_client().get("/") - self.assertEqual(1, logger.new.call_count) - _, kwargs = logger.new.call_args + self.assertEqual(1, bind_contextvars.call_count) + _, kwargs = bind_contextvars.call_args self.assertTrue(UUID(kwargs["request_id"], version=4)) def test_adds_logging_of_span_and_trace(self): - with patch("app.setup.logger") as logger: + with patch("structlog.contextvars.bind_contextvars") as bind_contextvars: self._setting_overrides.update({"EQ_APPLICATION_VERSION": False}) application = create_app(self._setting_overrides) @@ -89,8 +91,8 @@ def test_adds_logging_of_span_and_trace(self): } application.test_client().get("/", headers=x_cloud_headers) - self.assertEqual(1, logger.bind.call_count) - _, kwargs = logger.bind.call_args + self.assertEqual(2, bind_contextvars.call_count) + _, kwargs = bind_contextvars.call_args self.assertTrue(kwargs["span"] == "0123456789012345678901") self.assertTrue(kwargs["trace"] == "0123456789") @@ -114,8 +116,8 @@ def test_enforces_secure_headers(self): headers["Strict-Transport-Security"], ) self.assertEqual("DENY", headers["X-Frame-Options"]) - self.assertEqual("1; mode=block", headers["X-Xss-Protection"]) self.assertEqual("nosniff", headers["X-Content-Type-Options"]) + self.assertNotIn("X-XSS-Protection", headers) def test_csp_policy_headers(self): cdn_url = "https://cdn.test.domain" @@ -137,16 +139,17 @@ def test_csp_policy_headers(self): csp_policy_parts = headers["Content-Security-Policy"].split("; ") self.assertIn(f"default-src 'self' {cdn_url}", csp_policy_parts) self.assertIn( - "script-src 'self' https://www.googletagmanager.com https://www.google-analytics.com " - f"https://ssl.google-analytics.com 'unsafe-inline' {cdn_url} 'nonce-{request.csp_nonce}'", + "script-src 'self' https://*.googletagmanager.com " + f"{cdn_url} 'nonce-{request.csp_nonce}'", csp_policy_parts, ) self.assertIn( - f"style-src 'self' https://tagmanager.google.com https://fonts.googleapis.com 'unsafe-inline' {cdn_url}", + f"style-src 'self' https://fonts.googleapis.com {cdn_url}", csp_policy_parts, ) self.assertIn( - f"img-src 'self' data: https://www.google-analytics.com https://ssl.gstatic.com https://www.gstatic.com {cdn_url}", + "img-src 'self' data: https://ssl.gstatic.com https://www.gstatic.com https://*.google-analytics.com" + f" https://*.googletagmanager.com {cdn_url}", csp_policy_parts, ) self.assertIn( @@ -154,10 +157,8 @@ def test_csp_policy_headers(self): csp_policy_parts, ) self.assertIn( - "frame-src https://www.googletagmanager.com", csp_policy_parts - ) - self.assertIn( - f"connect-src 'self' https://www.google-analytics.com {cdn_url} {address_lookup_api_url}", + "connect-src 'self' https://*.google-analytics.com https://*.analytics.google.com" + f" https://*.googletagmanager.com {cdn_url} {address_lookup_api_url}", csp_policy_parts, ) self.assertIn( @@ -288,9 +289,9 @@ def test_defaults_to_adding_the_log_publisher_to_the_application(self): def test_adds_cloud_task_publisher_to_the_application(self): self._setting_overrides["EQ_SUBMISSION_CONFIRMATION_BACKEND"] = "cloud-tasks" - self._setting_overrides[ - "EQ_SUBMISSION_CONFIRMATION_CLOUD_FUNCTION_NAME" - ] = "test" + self._setting_overrides["EQ_SUBMISSION_CONFIRMATION_CLOUD_FUNCTION_NAME"] = ( + "test" + ) # When with patch( @@ -305,9 +306,9 @@ def test_adds_cloud_task_publisher_to_the_application(self): def test_submission_backend_not_set_raises_exception(self): # Given self._setting_overrides["EQ_SUBMISSION_CONFIRMATION_BACKEND"] = "" - self._setting_overrides[ - "EQ_SUBMISSION_CONFIRMATION_CLOUD_FUNCTION_NAME" - ] = "test" + self._setting_overrides["EQ_SUBMISSION_CONFIRMATION_CLOUD_FUNCTION_NAME"] = ( + "test" + ) # When with patch( @@ -389,3 +390,76 @@ def test_conditional_expected_secret(self, mock_safe_load): assert "Missing Secret [ADDRESS_LOOKUP_API_AUTH_TOKEN_SECRET]" in str( ex.exception ) + + def test_setup_oidc_service_gcp(self): + # Given + self._setting_overrides["OIDC_TOKEN_BACKEND"] = "gcp" + self._setting_overrides["SDS_OAUTH2_CLIENT_ID"] = "1234567890" + self._setting_overrides["CIR_OAUTH2_CLIENT_ID"] = "1234567890" + + # When + application = create_app(self._setting_overrides) + + # Then + assert isinstance( + application.eq["oidc_credentials_service"], OIDCCredentialsServiceGCP + ) + + def test_setup_oidc_service_local(self): + # Given + self._setting_overrides["OIDC_TOKEN_BACKEND"] = "local" + + # When + application = create_app(self._setting_overrides) + + # Then + assert isinstance( + application.eq["oidc_credentials_service"], OIDCCredentialsServiceLocal + ) + + def test_oidc_backend_invalid_raises_exception(self): + # Given + self._setting_overrides["OIDC_TOKEN_BACKEND"] = "invalid" + + # When + with self.assertRaises(NotImplementedError) as ex: + create_app(self._setting_overrides) + + # Then + assert "Unknown OIDC_TOKEN_BACKEND" in str(ex.exception) + + def test_oidc_backend_missing_raises_exception(self): + # Given + self._setting_overrides["OIDC_TOKEN_BACKEND"] = "" + + # When + with self.assertRaises(MissingEnvironmentVariable) as ex: + create_app(self._setting_overrides) + + # Then + assert "Setting OIDC_TOKEN_BACKEND Missing" in str(ex.exception) + + def test_sds_oauth_2_client_id_missing_raises_exception(self): + # Given + self._setting_overrides["OIDC_TOKEN_BACKEND"] = "gcp" + self._setting_overrides["SDS_OAUTH2_CLIENT_ID"] = "" + + # When + with self.assertRaises(MissingEnvironmentVariable) as ex: + create_app(self._setting_overrides) + + # Then + assert "Setting SDS_OAUTH2_CLIENT_ID Missing" in str(ex.exception) + + def test_cir_oauth_2_client_id_missing_raises_exception(self): + # Given + self._setting_overrides["OIDC_TOKEN_BACKEND"] = "gcp" + self._setting_overrides["SDS_OAUTH2_CLIENT_ID"] = "123456789" + self._setting_overrides["CIR_OAUTH2_CLIENT_ID"] = "" + + # When + with self.assertRaises(MissingEnvironmentVariable) as ex: + create_app(self._setting_overrides) + + # Then + assert "Setting CIR_OAUTH2_CLIENT_ID Missing" in str(ex.exception) diff --git a/tests/integration/test_application_variables.py b/tests/integration/test_application_variables.py index 429366f03c..62a5398f33 100644 --- a/tests/integration/test_application_variables.py +++ b/tests/integration/test_application_variables.py @@ -1,44 +1,49 @@ from app import settings +from app.utilities.json import json_loads from tests.integration.integration_test_case import IntegrationTestCase class TestApplicationVariables(IntegrationTestCase): def setUp(self): settings.EQ_ENABLE_LIVE_RELOAD = True - settings.EQ_GOOGLE_TAG_MANAGER_ID = "TestId" - settings.EQ_GOOGLE_TAG_MANAGER_AUTH = "TestAuth" + settings.EQ_GOOGLE_TAG_ID = "TestId" super().setUp() def tearDown(self): super().tearDown() settings.EQ_ENABLE_LIVE_RELOAD = False - settings.EQ_GOOGLE_TAG_MANAGER_ID = None - settings.EQ_GOOGLE_TAG_MANAGER_AUTH = None + settings.EQ_GOOGLE_TAG_ID = None def test_google_analytics_code_and_credentials_are_present(self): - self.launchSurvey("test_textfield") + self.launchSurveyV2(schema_name="test_feedback", roles=["dumper"]) + self.get("/dump/debug") + actual = json_loads(self.getResponseData()) self._client.set_cookie( - "localhost", key="ons_cookie_policy", value="'usage':true" + domain="localhost", key="ons_cookie_policy", value="'usage':true" ) - self.get("/questionnaire/name-block/") + self.get("/questionnaire/feedback/") self.assertStatusOK() - self.assertInHead("gtm.start") - self.assertInHead("dataLayer = []") - self.assertInBody("https://www.googletagmanager.com") - self.assertInHead(settings.EQ_GOOGLE_TAG_MANAGER_AUTH) - self.assertInHead(settings.EQ_GOOGLE_TAG_MANAGER_ID) + self.assertInHead( + f'"form_type": "H", "survey_id": "0", "title": "Feedback test schema", "tx_id": "{actual["METADATA"]["tx_id"]}"' + ) + self.assertInHead("https://www.googletagmanager.com") + self.assertInHead(settings.EQ_GOOGLE_TAG_ID) - def test_google_analytics_data_layer_is_set_to_nisra_false(self): - self.launchSurvey("test_thank_you_census_individual") + def test_google_analytics_data_layer_has_no_null_fields(self): + self.launchSurveyV2(schema_name="test_textfield", roles=["dumper"]) + self.get("/dump/debug") + actual = json_loads(self.getResponseData()) self._client.set_cookie( - "localhost", key="ons_cookie_policy", value="'usage':true" + domain="localhost", key="ons_cookie_policy", value="'usage':true" ) - self.get("/questionnaire/individual-confirmation/") + self.get("/questionnaire/name-block/") self.assertStatusOK() - self.assertInHead("gtm.start") - self.assertInHead('dataLayer = [{"nisra": false}]') + # form_type is empty so should not be present + self.assertInHead( + f'"survey_id": "001", "title": "Other input fields", "tx_id": "{actual["METADATA"]["tx_id"]}"' + ) def test_livereload_script_rendered(self): - self.launchSurvey("test_textfield") + self.launchSurveyV2(schema_name="test_textfield") self.assertStatusOK() self.assertTrue("__bs_script__" in self.getResponseData()) diff --git a/tests/integration/test_application_variables_negative.py b/tests/integration/test_application_variables_negative.py index a87fa99d9b..8c83a6cebc 100644 --- a/tests/integration/test_application_variables_negative.py +++ b/tests/integration/test_application_variables_negative.py @@ -8,6 +8,6 @@ def setUp(self): super().setUp() def test_livereload_script_not_rendered(self): - self.launchSurvey("test_textfield") + self.launchSurveyV2(schema_name="test_textfield") self.assertStatusOK() self.assertFalse("__bs_script__" in self.getResponseData()) diff --git a/tests/integration/test_broken_submission.py b/tests/integration/test_broken_submission.py index f6f557364d..eb50e5d0ff 100644 --- a/tests/integration/test_broken_submission.py +++ b/tests/integration/test_broken_submission.py @@ -12,17 +12,16 @@ def setUp(self): self.instance.send_message.return_value = False super().setUp() - self.launchSurvey("test_percentage") + self.launchSurveyV2(schema_name="test_percentage") def tearDown(self): self.patcher.stop() def test_broken_submitter_results_in_500(self): self.post({"answer": "50"}) + self.post({"answer-decimal": "5.5"}) self.assertStatusOK() self.post() - self.assertEqual( - self.instance.send_message.called, True - ) # pylint: disable=no-member + self.assertEqual(self.instance.send_message.called, True) self.assertStatusCode(500) diff --git a/tests/integration/test_create_token.py b/tests/integration/test_create_token.py new file mode 100644 index 0000000000..a172e53ea1 --- /dev/null +++ b/tests/integration/test_create_token.py @@ -0,0 +1,203 @@ +from unittest import mock + +from app.authentication.authenticator import decrypt_token +from tests.integration.app_context_test_case import AppContextTestCase +from tests.integration.create_token import ( + PAYLOAD_V2_BUSINESS, + PAYLOAD_V2_SOCIAL, + PAYLOAD_V2_SUPPLEMENTARY_DATA, +) +from tests.integration.integration_test_case import IntegrationTestCase + +EXPECTED_TOKEN_BUSINESS = { + "account_service_url": "http://upstream.url", + "case_id": "1001", + "collection_exercise_sid": "789", + "exp": 1709058195.091798, + "iat": 1709054595.091798, + "jti": "1001", + "language_code": "en", + "response_expires_at": "2024-02-28T09:59:43.109276+00:00", + "response_id": "1234567890123456", + "roles": [], + "schema_name": "test_metadata_routing.json", + "survey_metadata": { + "data": { + "display_address": "68 Abingdon Road, Goathill", + "employment_date": "1983-06-02", + "flag_1": 123, + "link": "https://example.com", + "period_id": "202402", + "period_str": "April 2016", + "ref_p_end_date": "2016-04-30", + "ref_p_start_date": "2016-04-01", + "ru_name": "Integration Testing", + "ru_ref": "12345678901A", + "trad_as": "Integration Tests", + "user_id": "integration-test", + } + }, + "tx_id": "1001", + "version": "v2", +} +EXPECTED_TOKEN_SOCIAL = { + "account_service_url": "http://upstream.url", + "case_id": "1001", + "collection_exercise_sid": "789", + "exp": 1709058195.091798, + "iat": 1709054595.091798, + "jti": "1001", + "language_code": "en", + "response_expires_at": "2024-02-28T09:59:43.109276+00:00", + "response_id": "1234567890123456", + "roles": [], + "schema_name": "social_demo.json", + "survey_metadata": { + "data": { + "case_ref": "1000000000000001", + "date": "2016-05-12", + "flag_1": True, + "qid": PAYLOAD_V2_SOCIAL["survey_metadata"]["data"]["qid"], + "user_id": "64389274239", + }, + "receipting_keys": ["qid"], + }, + "tx_id": "1001", + "version": "v2", +} + + +class TestCreateToken(IntegrationTestCase, AppContextTestCase): + """ + The purpose of this test class is to test the creation of a token (from create_token.py) to ensure + metadata in a decrypted token is nested/found in the correct level. + """ + + # Patches are used since the values "uuid4", "time" and "get_response_expires_at" return are dynamic + @mock.patch("tests.integration.create_token.uuid4") + @mock.patch( + "tests.integration.create_token.time", + ) + @mock.patch( + "tests.integration.create_token.get_response_expires_at", + ) + def test_payload_content_and_structure_from_token( + self, mock_response_expiry_time, mock_time, mock_uuid + ): + mock_uuid.return_value = 1001 + mock_time.return_value = 1709054595.091798 + mock_response_expiry_time.return_value = "2024-02-28T09:59:43.109276+00:00" + test_parameters = [ + { + "schema": "test_metadata_routing.json", + "theme": "default", + "additional_payload": { + "flag_1": 123, + "period_id": "202402", + "link": "https://example.com", + }, + "payload": PAYLOAD_V2_BUSINESS, + "expected_token": EXPECTED_TOKEN_BUSINESS, + }, + { + "schema": "social_demo.json", + "theme": "social", + "additional_payload": { + "flag_1": True, + "user_id": "64389274239", + "date": "2016-05-12", + }, + "payload": PAYLOAD_V2_SOCIAL, + "expected_token": EXPECTED_TOKEN_SOCIAL, + }, + ] + for value in test_parameters: + with self.subTest(): + additional_payload = value["additional_payload"] + token = self.token_generator.create_token_v2( + schema_name=value["schema"], + theme=value["theme"], + **additional_payload, + ) + + with self.test_app.app_context(): + decrypted_token = decrypt_token(token) + self.assertEqual(value["expected_token"], decrypted_token) + + def test_uuid_consistent_after_decryption(self): + token = self.token_generator.create_token_v2( + "test_checkbox.json", theme="social", value="Dummy Text" + ) + with self.test_app.app_context(): + decrypted_token = decrypt_token(token) + assert decrypted_token["survey_metadata"] == { + "data": { + "case_ref": "1000000000000001", + "qid": PAYLOAD_V2_SOCIAL["survey_metadata"]["data"]["qid"], + "value": "Dummy Text", + }, + "receipting_keys": ["qid"], + } + + def test_sds_metadata_included_in_token(self): + token = self.token_generator.create_supplementary_data_token( + "test_checkbox.json" + ) + with self.test_app.app_context(): + decrypted_token = decrypt_token(token) + self.assertEqual( + decrypted_token, PAYLOAD_V2_SUPPLEMENTARY_DATA | decrypted_token + ) + + def test_additional_payload_added_in_token(self): + token = self.token_generator.create_supplementary_data_token( + "test_address.json", + flag_1=True, + sds_dataset_id="54f1b432-9421-49e5-bd26-e63e18a30b69", + ) + with self.test_app.app_context(): + decrypted_token = decrypt_token(token) + assert decrypted_token["survey_metadata"] == { + "data": { + "display_address": "68 Abingdon Road, Goathill", + "employment_date": "1983-06-02", + "flag_1": True, + "period_id": "201604", + "period_str": "April 2016", + "ref_p_end_date": "2016-04-30", + "ref_p_start_date": "2016-04-01", + "ru_name": "Integration Testing", + "ru_ref": "12345678901A", + "sds_dataset_id": "54f1b432-9421-49e5-bd26-e63e18a30b69", + "survey_id": "123", + "trad_as": "Integration Tests", + "user_id": "integration-test", + } + } + + def test_metadata_is_removed_from_token(self): + metadata_tokens = [ + { + "token": self.token_generator.create_token_without_jti( + "test_number.json" + ), + "removed_metadata": "jti", + }, + { + "token": self.token_generator.create_token_without_case_id( + "test_numbers.json" + ), + "removed_metadata": "case_id", + }, + { + "token": self.token_generator.create_token_without_trad_as( + "test_numbers.json" + ), + "removed_metadata": "trad_as", + }, + ] + for values in metadata_tokens: + with self.subTest(): + with self.test_app.app_context(): + decrypted_token = decrypt_token(values["token"]) + self.assertNotIn(values["removed_metadata"], decrypted_token) diff --git a/tests/integration/test_flush_data.py b/tests/integration/test_flush_data.py index 10e9468a5e..7bbd9a16af 100644 --- a/tests/integration/test_flush_data.py +++ b/tests/integration/test_flush_data.py @@ -1,10 +1,18 @@ import time import uuid +from httmock import HTTMock, urlmatch from mock import patch +from app.utilities.schema import ( + CIR_RETRIEVE_COLLECTION_INSTRUMENT_URL, + get_schema_path_map, +) +from tests.app.parser.conftest import get_response_expires_at from tests.integration.integration_test_case import IntegrationTestCase +SCHEMA_PATH_MAP = get_schema_path_map(include_test_schemas=True) + class TestFlushData(IntegrationTestCase): def setUp(self): @@ -18,7 +26,7 @@ def setUp(self): self.encrypt_instance = mock_encrypter_class super().setUp() - self.launchSurvey("test_textfield") + self.launchSurveyV2(schema_name="test_textfield") form_data = {"name-answer": "Joe Bloggs"} self.post(form_data) @@ -29,6 +37,25 @@ def tearDown(self): super().tearDown() + @staticmethod + @urlmatch(netloc=r"eq-survey-register", path=r"\/my-test-schema") + def schema_url_mock(_url, _request): + schema_path = SCHEMA_PATH_MAP["test"]["en"]["test_textfield"] + + with open(schema_path, encoding="utf8") as json_data: + return json_data.read() + + @staticmethod + @urlmatch( + path=CIR_RETRIEVE_COLLECTION_INSTRUMENT_URL, + query="guid=f0519981-426c-8b93-75c0-bfc40c66fe25", + ) + def cir_url_mock(_url, _request): + schema_path = SCHEMA_PATH_MAP["test"]["en"]["test_textarea"] + + with open(schema_path, encoding="utf8") as json_data: + return json_data.read() + def test_flush_data_successful(self): self.post( url="/flush?token=" @@ -83,9 +110,7 @@ def test_invalid_token_passed_to_flush(self): self.assertStatusForbidden() def test_flush_errors_when_submission_fails(self): - self.submitter_instance.send_message.return_value = ( - False # pylint: disable=no-member - ) + self.submitter_instance.send_message.return_value = False self.post( url="/flush?token=" @@ -99,8 +124,8 @@ def test_flush_sets_flushed_flag_to_true(self): + self.token_generator.generate_token(self.get_payload()) ) - self.encrypt_instance.assert_called_once() # pylint: disable=no-member - args = self.encrypt_instance.call_args[0] # pylint: disable=no-member + self.encrypt_instance.assert_called_once() + args = self.encrypt_instance.call_args[0] self.assertTrue('"flushed": true' in args[0]) @@ -112,4 +137,100 @@ def get_payload(): "exp": time.time() + 1000, "response_id": "1234567890123456", "roles": ["flusher"], + "version": "v2", + "survey_metadata": {"data": {}}, + } + + @patch("app.routes.flush.convert_answers_v2") + def test_flush_data_successful_v2(self, mock_convert_answers_v2): + mock_convert_answer_payload = { + "case_id": "19300487-87e7-42df-9330-718efb08e660", + "tx_id": "5d8b97f7-c8bd-42e1-88c9-e7721388463b", + "type": "uk.gov.ons.edc.eq:surveyresponse", + "version": "v2", + "data_version": "0.0.3", + "origin": "uk.gov.ons.edc.eq", + "collection_exercise_sid": "1eeb58ec-bae7-414d-a02e-5c3c23052dc7", + "schema_name": "test_textfield", + "flushed": True, + "submitted_at": "2023-02-07T11:42:59.575214+00:00", + "launch_language_code": "en", + "survey_metadata": { + "survey_id": "001", + "period_id": "201605", + "ru_name": "ESSENTIAL ENTERPRISE LTD.", + "user_id": "UNKNOWN", + "ru_ref": "12345678901A", + }, + "data": { + "answers": [{"answer_id": "name-answer", "value": "sdfsdf"}], + "lists": [], + }, + "started_at": "2023-02-07T11:42:32.380784+00:00", + "response_expires_at": get_response_expires_at(), } + self.launchSurveyV2("test_textfield") + form_data = {"name-answer": "Joe Bloggs"} + self.post(form_data) + mock_convert_answers_v2.return_value = mock_convert_answer_payload + self.post( + url="/flush?token=" + + self.token_generator.generate_token(self.get_payload()) + ) + self.assertStatusOK() + mock_convert_answers_v2.assert_called_once() + + def test_flush_logs_output(self): + with self.assertLogs() as logs: + self.post( + url=f"/flush?token={self.token_generator.create_token_v2(schema_name='test_textfield', **self.get_payload())}" + ) + + flush_log = logs.output[6] + + self.assertIn("successfully flushed answers", flush_log) + self.assertIn("tx_id", flush_log) + self.assertIn("ce_id", flush_log) + self.assertIn("schema_name", flush_log) + self.assertNotIn("schema_url", flush_log) + + def test_flush_logs_output_schema_url(self): + schema_url = "http://eq-survey-register.url/my-test-schema" + token = self.token_generator.create_token_with_schema_url(schema_url=schema_url) + with HTTMock(self.schema_url_mock): + self.get(url=f"/session?token={token}") + self.assertStatusOK() + with self.assertLogs() as logs: + self.post( + url=f"/flush?token={self.token_generator.create_token_with_schema_url(schema_url=schema_url, **self.get_payload())}" + ) + + flush_log = logs.output[6] + + self.assertIn("successfully flushed answers", flush_log) + self.assertIn("tx_id", flush_log) + self.assertIn("ce_id", flush_log) + self.assertIn("schema_url", flush_log) + + def test_flush_logs_output_cir_instrument_id(self): + token = self.token_generator.create_token_with_cir_instrument_id( + cir_instrument_id="f0519981-426c-8b93-75c0-bfc40c66fe25" + ) + with HTTMock(self.cir_url_mock): + self.get(url=f"/session?token={token}") + self.assertStatusOK() + with self.assertLogs() as logs: + token_with_flush_role = ( + self.token_generator.create_token_with_cir_instrument_id( + cir_instrument_id="f0519981-426c-8b93-75c0-bfc40c66fe25", + payload=self.get_payload(), + ) + ) + self.post(url=f"/flush?token={token_with_flush_role}") + + flush_log = logs.output[6] + + self.assertIn("successfully flushed answers", flush_log) + self.assertIn("tx_id", flush_log) + self.assertIn("ce_id", flush_log) + self.assertIn("cir_instrument_id", flush_log) diff --git a/tests/integration/test_header_links.py b/tests/integration/test_header_links.py index c3a23c0cf3..744b7540f4 100644 --- a/tests/integration/test_header_links.py +++ b/tests/integration/test_header_links.py @@ -1,3 +1,4 @@ +from app.settings import ACCOUNT_SERVICE_BASE_URL from tests.integration.create_token import ACCOUNT_SERVICE_URL from tests.integration.integration_test_case import IntegrationTestCase @@ -25,11 +26,47 @@ def assert_sign_out_link_does_not_exist(self): self.assertIsNone(sign_out_link) self.assertNotInBody("Sign out") + def assert_help_link_exist(self): + help_link = self.getLinkById("header-link-help") + self.assertIsNotNone(help_link) + self.assertEqual(help_link.text, "Help") + self.assertEqual( + help_link["href"], + f"{ACCOUNT_SERVICE_URL}/surveys/surveys-help?survey_ref=001&ru_ref=12345678901", + ) + + def assert_help_link_exist_not_authenticated(self): + help_link = self.getLinkById("header-link-help") + self.assertIsNotNone(help_link) + self.assertEqual(help_link.text, "Help") + self.assertEqual( + help_link["href"], + f"{ACCOUNT_SERVICE_BASE_URL}/help", + ) + + def assert_help_link_exist_not_authenticated_after_sign_out(self): + help_link = self.getLinkById("header-link-help") + self.assertIsNotNone(help_link) + self.assertEqual(help_link.text, "Help") + self.assertEqual( + help_link["href"], + f"{ACCOUNT_SERVICE_URL}/help", + ) + + def assert_help_link_does_not_exist_not_authenticated_after_sign_out(self): + help_link = self.getLinkById("header-link-help") + self.assertIsNone(help_link) + + def assert_help_link_does_not_exist(self): + help_link = self.getLinkById("header-link-help") + self.assertIsNone(help_link) + self.assertNotInBody("Help") + class TestHeaderLinksPreSubmission(TestHeaderLinks): def test_links_in_header_when_valid_session(self): # Given - self.launchSurvey("test_thank_you") + self.launchSurveyV2(schema_name="test_thank_you") # When self.assertStatusOK() @@ -37,6 +74,43 @@ def test_links_in_header_when_valid_session(self): # Then self.assert_my_account_link_exist() self.assert_sign_out_link_exist() + self.assert_help_link_exist() + + def test_links_in_header_when_no_session_but_cookie_exists(self): + # Given + self.launchSurveyV2(schema_name="test_thank_you") + self.assertInUrl("questionnaire/did-you-know/") + self.saveAndSignOut() + + # When + self.assertStatusCode(302) + self.get("questionnaire/") + + # Then + self.assertInUrl("questionnaire/") + cookie = self.getCookie() + self.assertEqual(cookie.get("theme"), "default") + self.assert_my_account_link_does_not_exist() + self.assert_sign_out_link_does_not_exist() + self.assert_help_link_exist_not_authenticated_after_sign_out() + + def test_links_in_header_when_no_session_but_cookie_exists_theme_social(self): + # Given + self.launchSurveyV2(schema_name="test_theme_social", theme="social") + self.assertInUrl("/questionnaire/radio/") + self.saveAndSignOut() + + # When + self.assertStatusCode(302) + self.get("questionnaire/") + + # Then + self.assertInUrl("questionnaire/") + cookie = self.getCookie() + self.assertEqual(cookie.get("theme"), "social") + self.assert_my_account_link_does_not_exist() + self.assert_sign_out_link_does_not_exist() + self.assert_help_link_does_not_exist() def test_links_not_in_header_when_no_session(self): # Given @@ -48,12 +122,25 @@ def test_links_not_in_header_when_no_session(self): # Then self.assert_my_account_link_does_not_exist() self.assert_sign_out_link_does_not_exist() + self.assert_help_link_does_not_exist() + + def test_links_not_in_header_when_valid_session_theme_social(self): + # Given + self.launchSurveyV2(schema_name="test_theme_social", theme="social") + + # When + self.assertStatusOK() + + # Then + self.assert_my_account_link_does_not_exist() + self.assert_sign_out_link_does_not_exist() + self.assert_help_link_does_not_exist() class TestHeaderLinksPostSubmission(TestHeaderLinks): def test_links_in_header_when_valid_session(self): # Given - self.launchSurvey("test_thank_you") + self.launchSurveyV2(schema_name="test_thank_you") self.post() self.post() @@ -64,6 +151,7 @@ def test_links_in_header_when_valid_session(self): # Then self.assert_my_account_link_exist() self.assert_sign_out_link_exist() + self.assert_help_link_exist() def test_links_not_in_header_when_no_session(self): # Given @@ -75,14 +163,49 @@ def test_links_not_in_header_when_no_session(self): # Then self.assert_my_account_link_does_not_exist() self.assert_sign_out_link_does_not_exist() + self.assert_help_link_does_not_exist() + + def test_links_not_in_header_when_valid_session_theme_social_thank_you_page(self): + # Given + self.launchSurveyV2(schema_name="test_theme_social", theme="social") + self.post() + self.post() + + # When + self.assertInUrl("/thank-you") + self.assertStatusOK() + + # Then + self.assert_my_account_link_does_not_exist() + self.assert_sign_out_link_does_not_exist() + self.assert_help_link_does_not_exist() class TestHeaderLinksPostSignOut(TestHeaderLinks): def test_links_not_in_header_after_sign_out(self): # Given - self.launchSurvey("test_thank_you") + self.launchSurveyV2(schema_name="test_thank_you") self.assert_my_account_link_exist() self.assert_sign_out_link_exist() + self.assert_help_link_exist() + + # When I sign out and go back to previous url since we will be redirected + current_url = self.last_url + self.signOut() + self.get(current_url) + + # Then + self.assertInBody("Sorry, you need to sign in again") + self.assert_my_account_link_does_not_exist() + self.assert_sign_out_link_does_not_exist() + self.assert_help_link_exist_not_authenticated_after_sign_out() + + def test_links_not_in_header_after_sign_out_theme_social(self): + # Given + self.launchSurveyV2(schema_name="test_theme_social") + self.assert_my_account_link_does_not_exist() + self.assert_sign_out_link_does_not_exist() + self.assert_help_link_does_not_exist() # When I sign out and go back to previous url since we will be redirected current_url = self.last_url @@ -93,3 +216,4 @@ def test_links_not_in_header_after_sign_out(self): self.assertInBody("Sorry, you need to sign in again") self.assert_my_account_link_does_not_exist() self.assert_sign_out_link_does_not_exist() + self.assert_help_link_does_not_exist_not_authenticated_after_sign_out() diff --git a/tests/integration/test_no_questionnaire_state.py b/tests/integration/test_no_questionnaire_state.py index e5f6362a7f..919a939733 100644 --- a/tests/integration/test_no_questionnaire_state.py +++ b/tests/integration/test_no_questionnaire_state.py @@ -6,7 +6,7 @@ class TestNoQuestionnaireState(IntegrationTestCase): def test_questionnaire_route_before_questionnaire_submitted(self): # Given - self.launchSurvey("test_view_submitted_response") + self.launchSurveyV2(schema_name="test_view_submitted_response") # When with patch("app.routes.questionnaire.get_metadata", return_value=None): @@ -17,7 +17,7 @@ def test_questionnaire_route_before_questionnaire_submitted(self): def test_post_submission_route_before_questionnaire_submitted(self): # Given - self.launchSurvey("test_view_submitted_response") + self.launchSurveyV2(schema_name="test_view_submitted_response") # When with patch("app.routes.questionnaire.get_metadata", return_value=None): @@ -28,7 +28,7 @@ def test_post_submission_route_before_questionnaire_submitted(self): def test_post_submission_route_after_questionnaire_submitted(self): # Given - self.launchSurvey("test_view_submitted_response") + self.launchSurveyV2(schema_name="test_view_submitted_response") self.post() self.post() self.post() diff --git a/tests/integration/test_previously_submitted.py b/tests/integration/test_previously_submitted.py index b3c9080aaf..cc5f50bbba 100644 --- a/tests/integration/test_previously_submitted.py +++ b/tests/integration/test_previously_submitted.py @@ -5,7 +5,7 @@ class TestPreviouslySubmitted(IntegrationTestCase): def test_previously_submitted(self): # Given I complete the questionnaire and submit - self.launchSurvey("test_textfield") + self.launchSurveyV2(schema_name="test_textfield") self.post() self.post() self.assertInUrl(THANK_YOU_URL_PATH) diff --git a/tests/integration/test_textarea.py b/tests/integration/test_textarea.py index 8f580c14c7..ddff8af27e 100644 --- a/tests/integration/test_textarea.py +++ b/tests/integration/test_textarea.py @@ -8,7 +8,7 @@ class TestTextArea(IntegrationTestCase): def test_empty_submission(self): - self.launchSurvey("test_textarea") + self.launchSurveyV2(schema_name="test_textarea") self.post() self.assertInBody("No answer provided") @@ -17,7 +17,7 @@ def test_empty_submission(self): self.assertInUrl(THANK_YOU_URL_PATH) def test_too_many_characters(self): - self.launchSurvey("test_textarea") + self.launchSurveyV2(schema_name="test_textarea") self.post({"answer": "This is longer than twenty characters"}) self.assertInBody( @@ -25,7 +25,7 @@ def test_too_many_characters(self): ) def test_acceptable_submission(self): - self.launchSurvey("test_textarea") + self.launchSurveyV2(schema_name="test_textarea") self.post({"answer": "Less than 20 chars"}) self.assertInBody("Less than 20 chars") @@ -34,7 +34,7 @@ def test_acceptable_submission(self): self.assertInUrl(THANK_YOU_URL_PATH) def test_big_list_of_naughty_strings(self): - self.launchSurvey("test_big_list_naughty_strings") + self.launchSurveyV2(schema_name="test_big_list_naughty_strings") answers = {} for counter, value in enumerate(NAUGHTY_STRINGS): diff --git a/tests/jwt-test-keys/sdc-sds-supplementary_data-encryption-private-v1.pem b/tests/jwt-test-keys/sdc-sds-supplementary_data-encryption-private-v1.pem new file mode 100644 index 0000000000..7105e02018 --- /dev/null +++ b/tests/jwt-test-keys/sdc-sds-supplementary_data-encryption-private-v1.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKAIBAAKCAgEAvQCr8fkBjjThO2+bQ/Y2dIsO9fRcodxiC0Gyz36NKjVc0Gqh +Kthrcyvhv+birZRGRJDi6f7i0sQ1LO+ZvJGlHD5cWZMdHvzNZTa+jVIw5DswZxzc +SZuI1/EJmCVvNOEy+Tth79IPNV3KtsKIEN044Ih/BhIYYu/mjrKfM2vddFwT1BXJ +FH5grEusaeODYhPvCSwvy5QRFiQIbm/wCP+Y8Dm3EhSPulHBBFEcy4xf+gM9fYn5 +Fww+vMnUibAGU9rZhNBDI1dV2czW7/7et7hmTtrtZsPyaqHY05Td0B2a/N7ZhyQt +zTUqbxOVKw5OgnFn6UE1vmUWYSULSPKrLig76MSITyOH1h1mTK6T5UbYyzcFBUvx +nOi7M9ddqIR57DaDJqSA6vP6ngzN8UYqr36ZOBJqpfmj+mIGQN2HhhPBrI41AVvV +YU/5bC0IHZYWjd12+bHQhf41tLUsrK53p6xEG+n0Txgp8HMhFmWoKwWXNEkNvdAE +tgH7QnObjjTV10nS09EQFgA6kYA0YwTeIiVlx9Y0J7oUCmwosMpcbaAlImlgafTh +AutTqT83Iw8gs2bCRd6h8ysHi3QiqNn6dV/QlHRHqfNBwHapHRUbBTVenwOVrzyV +VnAd5a3P574zsm/gRz/9+rBIxebQdFrre+M4DLAnzLpdqv5+y/LqH3WiNMkCAwEA +AQKCAgBRVPqpNAhRU7wgwZRFGKyyVizn9nHuTVH7mhgCZmkE4tW/8kLMlzkV5KpO +1GJzY70hQGAFZePh4wEnByxXEy3EC6nd+gqsDQmuJnK1icr0S+w2UxsQqdenZVhF +msZSMR6oVb99Xh2hT20uXGQFLc2OAe73g83utWG3wnHzxNUVf5Ig0AcpxICBZEcb +ggZFrGJOxi8DIgKATp06OP1IQgVkStHW+/YlrYyr+OO1TAD5K2/ImBkSq/hLcWb+ +oTr31tOH7b8WdDzDbvyHZlwdH0MXZ+qFMIkfDeqqkgMpzbOmYZemKhFznw9VoU2t +q4hpZbfbjm48MnAA+dnzWEoFoNa2RJMHhZsTNSuDCXXunPvWGTymonco1eqiH+zx +X5yVtMRXtpCn1vbsJ/f39DB56X0Yj+S2edAvhOHORt9RfXZ/nBM9LngOpqjKKz1v +w9Mh7g+vGv/l4BPIznbKsQZEWj5ndtoUYFPtnMA1dKLr03jhMfAzsvamF2U8IjVZ +idzgJwIkAl00ywVQswXIAa7bW/3/moXGlHFz93qYf64mROY4ASffKMkRiaeFpTW3 +nTrIy0h+F3vZxSSc2Qw55auNDRnefNr6wsa1hrYRnzIknloK/wcaIulWGIyzEW4i +mH/RWTl+pdu7zmqb0j1BMr48wbxIuusceiC4n42AJQbFIg5BSQKCAQEA4D8kHgmA +Ct9vkq+hoyl99JnfZlfDr+N0p3+7Nc4mbVsgtl604grWjg7RIEXYFzXwfVd7pG5M +P70zqUo8gIDhZGBUksr6PZ8GcAyG8RiBE+gzkhBycw5JxRvJC/3Qsu6am1hr+M7v +peQTUolgiNekIPncJbP/j/5CQiuMlCyFk9GHWvFhaqq2pRrkkUYZ6zplzPePlnB+ +CkN0+q6HWOAXh4wCfkaUwb+xFFWiRxqYxoSL6OnPcSrXVG3BEIztMbuC9AzdPhPM +M1zaiYQf6mxeT4XOQcBMdcumxxltCUH5tiuGdYOHtB6Biq+if/p7VzjxswrejAnb +7jqlSLJZENajDwKCAQEA18Pym+HHz0HIdUuCcky/f1tyAnNijYoZP2En4ztKwcj6 +jp5CRgQC+ufdkrpJKWqhv3kHPCZvsKk066Rr03wTxvPMhb93xH4VBZ+bf58s7bUR +4KdJhJEf8DJbJiTBDno3ddhwtSXBfW6Eoyy6x6X6Hkw4DZm0DGaFN8vfJAZ9OprJ +q4nO0NYP1A0aYzE7BDgq5Tkr03ecNHLzIqP8zIVkkn6ewbUUG7wGbjRV7XSOpvum +GUjy04eKMIIZkWztIfJS7IuzXa2wK3I8lDu0wrmrm17lVjpZ9/ja8KaTVkgabL2P +Lg25NMk1qj0AQj88RrVkmGRWpnbuc1qJ4DOnA8zKpwKCAQA3bYP45LJAfb/vSvgy +A0R93DbK7jCRXjBsYnccsorvBtJMIZamNLWZwXHRf1INUqjR4njOSPER5CtL0eyo +erK7g9ADxKYb6x3FPmNwXnUxPXjZxrTzWXnEfbyw+RjH0ZBni3CMvGGh6IEaKpiw +2lRYTkorC5XEur0X6/nAeky+H9FMGlPQ8Mdagg4zFle7u+CDzEEylzWgRdI5UEBm +KGXIfEP1gG6ugTo843nMB3fxwbtvY7OBrmwxEzvgYmUSoN2agz+AY5Zar73Ytc7J +u+WH1HQJ7oU3rJHZrqAz5JnbfGCs1UkKrWupowYQihJImeusLKibhqhU9yv5jxPS +xKrjAoIBAFJSvAVH3wGv+rjuJ4ZOzB3emSBgP/D7COkKu7pSTBKmCRtTPLwUGcL7 +pqmuE+4Odkpk9iK4E5NW7A8ge9eEFtOo/5bkV+ELrh+oJx9Jb03+8SRDD6TZ7lKq +E+b4zQQmE3UOMOqczjd6bHcJwPYd2NGoiRZ/V5gHobqJOck4BJ3QozOk79j0Y7On +kDLafMb+Wzd8WcFkeJ/2X9gOs4yhNJ9EWnRUD6kJU3bG1yYze54wk84/7A5TP6GE +chbvdYanO4ZvQu9yLq5U9tIj+bL2PoiYa24780nOlFKPa9XWyuZEaRXMPKbsQmKC +xc+A6xGbchdG6Vy4MgCnQcXeT1H+2C8CggEBAMvwlPifgjCk2v7ejVPvA0K63aii +l8Acv6wtvELeQGHLg/ZmzbHNZG9mExhuHASeIEn/1p64dAUQ6NTvHq78bgZ4sa2M +U6surGKebDwcbgMZe5lN6jZhem9DHWyXCLco3FHWFj6bNMuBfOdUemVi4bEukcrM +ItfML9fhBIKva5aAWej+lEQydTImhLOfgXbEse7ic+ZWVddpuqNCq137Mcbc1q0t +QChlDkY6+uOdYa3xmSCHtDc64ymzBR6XAQHQnX9fqfLWdz3Ytk9s+H34Hrn6mJZ/ +/MRZOZimld2tdshUdEQ+Kaf4wVLcEwJuW5/NeMbba+iIA/sPqHRkYmVs1v8= +-----END RSA PRIVATE KEY----- diff --git a/tox.ini b/tox.ini deleted file mode 100644 index a60575d8f7..0000000000 --- a/tox.ini +++ /dev/null @@ -1,5 +0,0 @@ -[flake8] -# Ignore node_modules and cloned repos when not in a virtual environment -exclude = node_modules/*,tests/*,src/* -max-line-length = 160 -ignore = C815,C816,W503,E203 diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index c151f67a99..0000000000 --- a/yarn.lock +++ /dev/null @@ -1,5305 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@ampproject/remapping@^2.1.0": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.1.2.tgz#4edca94973ded9630d20101cd8559cedb8d8bd34" - integrity sha512-hoyByceqwKirw7w3Z7gnIIZC3Wx3J484Y3L/cMpXFbr7d9ZQj2mODrirNzcJa+SM3UlpWXYvKV4RlRpFXlWgXg== - dependencies: - "@jridgewell/trace-mapping" "^0.3.0" - -"@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789" - integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg== - dependencies: - "@babel/highlight" "^7.16.7" - -"@babel/compat-data@^7.13.11", "@babel/compat-data@^7.16.4", "@babel/compat-data@^7.16.8", "@babel/compat-data@^7.17.0": - version "7.17.0" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.17.0.tgz#86850b8597ea6962089770952075dcaabb8dba34" - integrity sha512-392byTlpGWXMv4FbyWw3sAZ/FrW/DrwqLGXpy0mbyNe9Taqv1mg9yON5/o0cnr8XYCkFTZbC1eV+c+LAROgrng== - -"@babel/core@^7.17.5": - version "7.17.5" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.17.5.tgz#6cd2e836058c28f06a4ca8ee7ed955bbf37c8225" - integrity sha512-/BBMw4EvjmyquN5O+t5eh0+YqB3XXJkYD2cjKpYtWOfFy4lQ4UozNSmxAcWT8r2XtZs0ewG+zrfsqeR15i1ajA== - dependencies: - "@ampproject/remapping" "^2.1.0" - "@babel/code-frame" "^7.16.7" - "@babel/generator" "^7.17.3" - "@babel/helper-compilation-targets" "^7.16.7" - "@babel/helper-module-transforms" "^7.16.7" - "@babel/helpers" "^7.17.2" - "@babel/parser" "^7.17.3" - "@babel/template" "^7.16.7" - "@babel/traverse" "^7.17.3" - "@babel/types" "^7.17.0" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.1.2" - semver "^6.3.0" - -"@babel/generator@^7.17.3": - version "7.17.3" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.17.3.tgz#a2c30b0c4f89858cb87050c3ffdfd36bdf443200" - integrity sha512-+R6Dctil/MgUsZsZAkYgK+ADNSZzJRRy0TvY65T71z/CR854xHQ1EweBYXdfT+HNeN7w0cSJJEzgxZMv40pxsg== - dependencies: - "@babel/types" "^7.17.0" - jsesc "^2.5.1" - source-map "^0.5.0" - -"@babel/helper-annotate-as-pure@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz#bb2339a7534a9c128e3102024c60760a3a7f3862" - integrity sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw== - dependencies: - "@babel/types" "^7.16.7" - -"@babel/helper-builder-binary-assignment-operator-visitor@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.16.7.tgz#38d138561ea207f0f69eb1626a418e4f7e6a580b" - integrity sha512-C6FdbRaxYjwVu/geKW4ZeQ0Q31AftgRcdSnZ5/jsH6BzCJbtvXvhpfkbkThYSuutZA7nCXpPR6AD9zd1dprMkA== - dependencies: - "@babel/helper-explode-assignable-expression" "^7.16.7" - "@babel/types" "^7.16.7" - -"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.7.tgz#06e66c5f299601e6c7da350049315e83209d551b" - integrity sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA== - dependencies: - "@babel/compat-data" "^7.16.4" - "@babel/helper-validator-option" "^7.16.7" - browserslist "^4.17.5" - semver "^6.3.0" - -"@babel/helper-create-class-features-plugin@^7.16.10", "@babel/helper-create-class-features-plugin@^7.16.7", "@babel/helper-create-class-features-plugin@^7.17.6": - version "7.17.6" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.6.tgz#3778c1ed09a7f3e65e6d6e0f6fbfcc53809d92c9" - integrity sha512-SogLLSxXm2OkBbSsHZMM4tUi8fUzjs63AT/d0YQIzr6GSd8Hxsbk2KYDX0k0DweAzGMj/YWeiCsorIdtdcW8Eg== - dependencies: - "@babel/helper-annotate-as-pure" "^7.16.7" - "@babel/helper-environment-visitor" "^7.16.7" - "@babel/helper-function-name" "^7.16.7" - "@babel/helper-member-expression-to-functions" "^7.16.7" - "@babel/helper-optimise-call-expression" "^7.16.7" - "@babel/helper-replace-supers" "^7.16.7" - "@babel/helper-split-export-declaration" "^7.16.7" - -"@babel/helper-create-regexp-features-plugin@^7.16.7": - version "7.17.0" - resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.17.0.tgz#1dcc7d40ba0c6b6b25618997c5dbfd310f186fe1" - integrity sha512-awO2So99wG6KnlE+TPs6rn83gCz5WlEePJDTnLEqbchMVrBeAujURVphRdigsk094VhvZehFoNOihSlcBjwsXA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.16.7" - regexpu-core "^5.0.1" - -"@babel/helper-define-polyfill-provider@^0.3.1": - version "0.3.1" - resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.1.tgz#52411b445bdb2e676869e5a74960d2d3826d2665" - integrity sha512-J9hGMpJQmtWmj46B3kBHmL38UhJGhYX7eqkcq+2gsstyYt341HmPeWspihX43yVRA0mS+8GGk2Gckc7bY/HCmA== - dependencies: - "@babel/helper-compilation-targets" "^7.13.0" - "@babel/helper-module-imports" "^7.12.13" - "@babel/helper-plugin-utils" "^7.13.0" - "@babel/traverse" "^7.13.0" - debug "^4.1.1" - lodash.debounce "^4.0.8" - resolve "^1.14.2" - semver "^6.1.2" - -"@babel/helper-environment-visitor@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz#ff484094a839bde9d89cd63cba017d7aae80ecd7" - integrity sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag== - dependencies: - "@babel/types" "^7.16.7" - -"@babel/helper-explode-assignable-expression@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.16.7.tgz#12a6d8522fdd834f194e868af6354e8650242b7a" - integrity sha512-KyUenhWMC8VrxzkGP0Jizjo4/Zx+1nNZhgocs+gLzyZyB8SHidhoq9KK/8Ato4anhwsivfkBLftky7gvzbZMtQ== - dependencies: - "@babel/types" "^7.16.7" - -"@babel/helper-function-name@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz#f1ec51551fb1c8956bc8dd95f38523b6cf375f8f" - integrity sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA== - dependencies: - "@babel/helper-get-function-arity" "^7.16.7" - "@babel/template" "^7.16.7" - "@babel/types" "^7.16.7" - -"@babel/helper-get-function-arity@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz#ea08ac753117a669f1508ba06ebcc49156387419" - integrity sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw== - dependencies: - "@babel/types" "^7.16.7" - -"@babel/helper-hoist-variables@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz#86bcb19a77a509c7b77d0e22323ef588fa58c246" - integrity sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg== - dependencies: - "@babel/types" "^7.16.7" - -"@babel/helper-member-expression-to-functions@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.16.7.tgz#42b9ca4b2b200123c3b7e726b0ae5153924905b0" - integrity sha512-VtJ/65tYiU/6AbMTDwyoXGPKHgTsfRarivm+YbB5uAzKUyuPjgZSgAFeG87FCigc7KNHu2Pegh1XIT3lXjvz3Q== - dependencies: - "@babel/types" "^7.16.7" - -"@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437" - integrity sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg== - dependencies: - "@babel/types" "^7.16.7" - -"@babel/helper-module-transforms@^7.16.7": - version "7.17.6" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.17.6.tgz#3c3b03cc6617e33d68ef5a27a67419ac5199ccd0" - integrity sha512-2ULmRdqoOMpdvkbT8jONrZML/XALfzxlb052bldftkicAUy8AxSCkD5trDPQcwHNmolcl7wP6ehNqMlyUw6AaA== - dependencies: - "@babel/helper-environment-visitor" "^7.16.7" - "@babel/helper-module-imports" "^7.16.7" - "@babel/helper-simple-access" "^7.16.7" - "@babel/helper-split-export-declaration" "^7.16.7" - "@babel/helper-validator-identifier" "^7.16.7" - "@babel/template" "^7.16.7" - "@babel/traverse" "^7.17.3" - "@babel/types" "^7.17.0" - -"@babel/helper-optimise-call-expression@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.7.tgz#a34e3560605abbd31a18546bd2aad3e6d9a174f2" - integrity sha512-EtgBhg7rd/JcnpZFXpBy0ze1YRfdm7BnBX4uKMBd3ixa3RGAE002JZB66FJyNH7g0F38U05pXmA5P8cBh7z+1w== - dependencies: - "@babel/types" "^7.16.7" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz#aa3a8ab4c3cceff8e65eb9e73d87dc4ff320b2f5" - integrity sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA== - -"@babel/helper-remap-async-to-generator@^7.16.8": - version "7.16.8" - resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.8.tgz#29ffaade68a367e2ed09c90901986918d25e57e3" - integrity sha512-fm0gH7Flb8H51LqJHy3HJ3wnE1+qtYR2A99K06ahwrawLdOFsCEWjZOrYricXJHoPSudNKxrMBUPEIPxiIIvBw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.16.7" - "@babel/helper-wrap-function" "^7.16.8" - "@babel/types" "^7.16.8" - -"@babel/helper-replace-supers@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.16.7.tgz#e9f5f5f32ac90429c1a4bdec0f231ef0c2838ab1" - integrity sha512-y9vsWilTNaVnVh6xiJfABzsNpgDPKev9HnAgz6Gb1p6UUwf9NepdlsV7VXGCftJM+jqD5f7JIEubcpLjZj5dBw== - dependencies: - "@babel/helper-environment-visitor" "^7.16.7" - "@babel/helper-member-expression-to-functions" "^7.16.7" - "@babel/helper-optimise-call-expression" "^7.16.7" - "@babel/traverse" "^7.16.7" - "@babel/types" "^7.16.7" - -"@babel/helper-simple-access@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz#d656654b9ea08dbb9659b69d61063ccd343ff0f7" - integrity sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g== - dependencies: - "@babel/types" "^7.16.7" - -"@babel/helper-skip-transparent-expression-wrappers@^7.16.0": - version "7.16.0" - resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.16.0.tgz#0ee3388070147c3ae051e487eca3ebb0e2e8bb09" - integrity sha512-+il1gTy0oHwUsBQZyJvukbB4vPMdcYBrFHa0Uc4AizLxbq6BOYC51Rv4tWocX9BLBDLZ4kc6qUFpQ6HRgL+3zw== - dependencies: - "@babel/types" "^7.16.0" - -"@babel/helper-split-export-declaration@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz#0b648c0c42da9d3920d85ad585f2778620b8726b" - integrity sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw== - dependencies: - "@babel/types" "^7.16.7" - -"@babel/helper-validator-identifier@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" - integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== - -"@babel/helper-validator-option@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz#b203ce62ce5fe153899b617c08957de860de4d23" - integrity sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ== - -"@babel/helper-wrap-function@^7.16.8": - version "7.16.8" - resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.16.8.tgz#58afda087c4cd235de92f7ceedebca2c41274200" - integrity sha512-8RpyRVIAW1RcDDGTA+GpPAwV22wXCfKOoM9bet6TLkGIFTkRQSkH1nMQ5Yet4MpoXe1ZwHPVtNasc2w0uZMqnw== - dependencies: - "@babel/helper-function-name" "^7.16.7" - "@babel/template" "^7.16.7" - "@babel/traverse" "^7.16.8" - "@babel/types" "^7.16.8" - -"@babel/helpers@^7.17.2": - version "7.17.2" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.17.2.tgz#23f0a0746c8e287773ccd27c14be428891f63417" - integrity sha512-0Qu7RLR1dILozr/6M0xgj+DFPmi6Bnulgm9M8BVa9ZCWxDqlSnqt3cf8IDPB5m45sVXUZ0kuQAgUrdSFFH79fQ== - dependencies: - "@babel/template" "^7.16.7" - "@babel/traverse" "^7.17.0" - "@babel/types" "^7.17.0" - -"@babel/highlight@^7.16.7": - version "7.16.10" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.10.tgz#744f2eb81579d6eea753c227b0f570ad785aba88" - integrity sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw== - dependencies: - "@babel/helper-validator-identifier" "^7.16.7" - chalk "^2.0.0" - js-tokens "^4.0.0" - -"@babel/parser@^7.16.7", "@babel/parser@^7.17.3": - version "7.17.3" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.3.tgz#b07702b982990bf6fdc1da5049a23fece4c5c3d0" - integrity sha512-7yJPvPV+ESz2IUTPbOL+YkIGyCqOyNIzdguKQuJGnH7bg1WTIifuM21YqokFt/THWh1AkCRn9IgoykTRCBVpzA== - -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.16.7.tgz#4eda6d6c2a0aa79c70fa7b6da67763dfe2141050" - integrity sha512-anv/DObl7waiGEnC24O9zqL0pSuI9hljihqiDuFHC8d7/bjr/4RLGPWuc8rYOff/QPzbEPSkzG8wGG9aDuhHRg== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.16.7.tgz#cc001234dfc139ac45f6bcf801866198c8c72ff9" - integrity sha512-di8vUHRdf+4aJ7ltXhaDbPoszdkh59AQtJM5soLsuHpQJdFQZOA4uGj0V2u/CZ8bJ/u8ULDL5yq6FO/bCXnKHw== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.16.0" - "@babel/plugin-proposal-optional-chaining" "^7.16.7" - -"@babel/plugin-proposal-async-generator-functions@^7.16.8": - version "7.16.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.16.8.tgz#3bdd1ebbe620804ea9416706cd67d60787504bc8" - integrity sha512-71YHIvMuiuqWJQkebWJtdhQTfd4Q4mF76q2IX37uZPkG9+olBxsX+rH1vkhFto4UeJZ9dPY2s+mDvhDm1u2BGQ== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/helper-remap-async-to-generator" "^7.16.8" - "@babel/plugin-syntax-async-generators" "^7.8.4" - -"@babel/plugin-proposal-class-properties@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.16.7.tgz#925cad7b3b1a2fcea7e59ecc8eb5954f961f91b0" - integrity sha512-IobU0Xme31ewjYOShSIqd/ZGM/r/cuOz2z0MDbNrhF5FW+ZVgi0f2lyeoj9KFPDOAqsYxmLWZte1WOwlvY9aww== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" - -"@babel/plugin-proposal-class-static-block@^7.16.7": - version "7.17.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.17.6.tgz#164e8fd25f0d80fa48c5a4d1438a6629325ad83c" - integrity sha512-X/tididvL2zbs7jZCeeRJ8167U/+Ac135AM6jCAx6gYXDUviZV5Ku9UDvWS2NCuWlFjIRXklYhwo6HhAC7ETnA== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.17.6" - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - -"@babel/plugin-proposal-dynamic-import@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.16.7.tgz#c19c897eaa46b27634a00fee9fb7d829158704b2" - integrity sha512-I8SW9Ho3/8DRSdmDdH3gORdyUuYnk1m4cMxUAdu5oy4n3OfN8flDEH+d60iG7dUfi0KkYwSvoalHzzdRzpWHTg== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - -"@babel/plugin-proposal-export-namespace-from@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.16.7.tgz#09de09df18445a5786a305681423ae63507a6163" - integrity sha512-ZxdtqDXLRGBL64ocZcs7ovt71L3jhC1RGSyR996svrCi3PYqHNkb3SwPJCs8RIzD86s+WPpt2S73+EHCGO+NUA== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - -"@babel/plugin-proposal-json-strings@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.16.7.tgz#9732cb1d17d9a2626a08c5be25186c195b6fa6e8" - integrity sha512-lNZ3EEggsGY78JavgbHsK9u5P3pQaW7k4axlgFLYkMd7UBsiNahCITShLjNQschPyjtO6dADrL24757IdhBrsQ== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/plugin-syntax-json-strings" "^7.8.3" - -"@babel/plugin-proposal-logical-assignment-operators@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.16.7.tgz#be23c0ba74deec1922e639832904be0bea73cdea" - integrity sha512-K3XzyZJGQCr00+EtYtrDjmwX7o7PLK6U9bi1nCwkQioRFVUv6dJoxbQjtWVtP+bCPy82bONBKG8NPyQ4+i6yjg== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - -"@babel/plugin-proposal-nullish-coalescing-operator@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.16.7.tgz#141fc20b6857e59459d430c850a0011e36561d99" - integrity sha512-aUOrYU3EVtjf62jQrCj63pYZ7k6vns2h/DQvHPWGmsJRYzWXZ6/AsfgpiRy6XiuIDADhJzP2Q9MwSMKauBQ+UQ== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - -"@babel/plugin-proposal-numeric-separator@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.16.7.tgz#d6b69f4af63fb38b6ca2558442a7fb191236eba9" - integrity sha512-vQgPMknOIgiuVqbokToyXbkY/OmmjAzr/0lhSIbG/KmnzXPGwW/AdhdKpi+O4X/VkWiWjnkKOBiqJrTaC98VKw== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - -"@babel/plugin-proposal-object-rest-spread@^7.16.7": - version "7.17.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.17.3.tgz#d9eb649a54628a51701aef7e0ea3d17e2b9dd390" - integrity sha512-yuL5iQA/TbZn+RGAfxQXfi7CNLmKi1f8zInn4IgobuCWcAb7i+zj4TYzQ9l8cEzVyJ89PDGuqxK1xZpUDISesw== - dependencies: - "@babel/compat-data" "^7.17.0" - "@babel/helper-compilation-targets" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.16.7" - -"@babel/plugin-proposal-optional-catch-binding@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.16.7.tgz#c623a430674ffc4ab732fd0a0ae7722b67cb74cf" - integrity sha512-eMOH/L4OvWSZAE1VkHbr1vckLG1WUcHGJSLqqQwl2GaUqG6QjddvrOaTUMNYiv77H5IKPMZ9U9P7EaHwvAShfA== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - -"@babel/plugin-proposal-optional-chaining@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.16.7.tgz#7cd629564724816c0e8a969535551f943c64c39a" - integrity sha512-eC3xy+ZrUcBtP7x+sq62Q/HYd674pPTb/77XZMb5wbDPGWIdUbSr4Agr052+zaUPSb+gGRnjxXfKFvx5iMJ+DA== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.16.0" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - -"@babel/plugin-proposal-private-methods@^7.16.11": - version "7.16.11" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.16.11.tgz#e8df108288555ff259f4527dbe84813aac3a1c50" - integrity sha512-F/2uAkPlXDr8+BHpZvo19w3hLFKge+k75XUprE6jaqKxjGkSYcK+4c+bup5PdW/7W/Rpjwql7FTVEDW+fRAQsw== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.16.10" - "@babel/helper-plugin-utils" "^7.16.7" - -"@babel/plugin-proposal-private-property-in-object@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.16.7.tgz#b0b8cef543c2c3d57e59e2c611994861d46a3fce" - integrity sha512-rMQkjcOFbm+ufe3bTZLyOfsOUOxyvLXZJCTARhJr+8UMSoZmqTe1K1BgkFcrW37rAchWg57yI69ORxiWvUINuQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.16.7" - "@babel/helper-create-class-features-plugin" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - -"@babel/plugin-proposal-unicode-property-regex@^7.16.7", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.16.7.tgz#635d18eb10c6214210ffc5ff4932552de08188a2" - integrity sha512-QRK0YI/40VLhNVGIjRNAAQkEHws0cswSdFFjpFyt943YmJIU1da9uW63Iu6NFV6CxTZW5eTDCrwZUstBWgp/Rg== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" - -"@babel/plugin-syntax-async-generators@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" - integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-class-properties@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" - integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-syntax-class-static-block@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" - integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-dynamic-import@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" - integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-export-namespace-from@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" - integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - -"@babel/plugin-syntax-json-strings@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" - integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" - integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" - integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-numeric-separator@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" - integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-object-rest-spread@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" - integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-catch-binding@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" - integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-chaining@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" - integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-private-property-in-object@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" - integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-top-level-await@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" - integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-arrow-functions@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.16.7.tgz#44125e653d94b98db76369de9c396dc14bef4154" - integrity sha512-9ffkFFMbvzTvv+7dTp/66xvZAWASuPD5Tl9LK3Z9vhOmANo6j94rik+5YMBt4CwHVMWLWpMsriIc2zsa3WW3xQ== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - -"@babel/plugin-transform-async-to-generator@^7.16.8": - version "7.16.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.16.8.tgz#b83dff4b970cf41f1b819f8b49cc0cfbaa53a808" - integrity sha512-MtmUmTJQHCnyJVrScNzNlofQJ3dLFuobYn3mwOTKHnSCMtbNsqvF71GQmJfFjdrXSsAA7iysFmYWw4bXZ20hOg== - dependencies: - "@babel/helper-module-imports" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/helper-remap-async-to-generator" "^7.16.8" - -"@babel/plugin-transform-block-scoped-functions@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.16.7.tgz#4d0d57d9632ef6062cdf354bb717102ee042a620" - integrity sha512-JUuzlzmF40Z9cXyytcbZEZKckgrQzChbQJw/5PuEHYeqzCsvebDx0K0jWnIIVcmmDOAVctCgnYs0pMcrYj2zJg== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - -"@babel/plugin-transform-block-scoping@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.16.7.tgz#f50664ab99ddeaee5bc681b8f3a6ea9d72ab4f87" - integrity sha512-ObZev2nxVAYA4bhyusELdo9hb3H+A56bxH3FZMbEImZFiEDYVHXQSJ1hQKFlDnlt8G9bBrCZ5ZpURZUrV4G5qQ== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - -"@babel/plugin-transform-classes@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.16.7.tgz#8f4b9562850cd973de3b498f1218796eb181ce00" - integrity sha512-WY7og38SFAGYRe64BrjKf8OrE6ulEHtr5jEYaZMwox9KebgqPi67Zqz8K53EKk1fFEJgm96r32rkKZ3qA2nCWQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.16.7" - "@babel/helper-environment-visitor" "^7.16.7" - "@babel/helper-function-name" "^7.16.7" - "@babel/helper-optimise-call-expression" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/helper-replace-supers" "^7.16.7" - "@babel/helper-split-export-declaration" "^7.16.7" - globals "^11.1.0" - -"@babel/plugin-transform-computed-properties@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.16.7.tgz#66dee12e46f61d2aae7a73710f591eb3df616470" - integrity sha512-gN72G9bcmenVILj//sv1zLNaPyYcOzUho2lIJBMh/iakJ9ygCo/hEF9cpGb61SCMEDxbbyBoVQxrt+bWKu5KGw== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - -"@babel/plugin-transform-destructuring@^7.16.7": - version "7.17.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.17.3.tgz#c445f75819641788a27a0a3a759d9df911df6abc" - integrity sha512-dDFzegDYKlPqa72xIlbmSkly5MluLoaC1JswABGktyt6NTXSBcUuse/kWE/wvKFWJHPETpi158qJZFS3JmykJg== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - -"@babel/plugin-transform-dotall-regex@^7.16.7", "@babel/plugin-transform-dotall-regex@^7.4.4": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.16.7.tgz#6b2d67686fab15fb6a7fd4bd895d5982cfc81241" - integrity sha512-Lyttaao2SjZF6Pf4vk1dVKv8YypMpomAbygW+mU5cYP3S5cWTfCJjG8xV6CFdzGFlfWK81IjL9viiTvpb6G7gQ== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" - -"@babel/plugin-transform-duplicate-keys@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.16.7.tgz#2207e9ca8f82a0d36a5a67b6536e7ef8b08823c9" - integrity sha512-03DvpbRfvWIXyK0/6QiR1KMTWeT6OcQ7tbhjrXyFS02kjuX/mu5Bvnh5SDSWHxyawit2g5aWhKwI86EE7GUnTw== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - -"@babel/plugin-transform-exponentiation-operator@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.16.7.tgz#efa9862ef97e9e9e5f653f6ddc7b665e8536fe9b" - integrity sha512-8UYLSlyLgRixQvlYH3J2ekXFHDFLQutdy7FfFAMm3CPZ6q9wHCwnUyiXpQCe3gVVnQlHc5nsuiEVziteRNTXEA== - dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" - -"@babel/plugin-transform-for-of@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.16.7.tgz#649d639d4617dff502a9a158c479b3b556728d8c" - integrity sha512-/QZm9W92Ptpw7sjI9Nx1mbcsWz33+l8kuMIQnDwgQBG5s3fAfQvkRjQ7NqXhtNcKOnPkdICmUHyCaWW06HCsqg== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - -"@babel/plugin-transform-function-name@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.16.7.tgz#5ab34375c64d61d083d7d2f05c38d90b97ec65cf" - integrity sha512-SU/C68YVwTRxqWj5kgsbKINakGag0KTgq9f2iZEXdStoAbOzLHEBRYzImmA6yFo8YZhJVflvXmIHUO7GWHmxxA== - dependencies: - "@babel/helper-compilation-targets" "^7.16.7" - "@babel/helper-function-name" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" - -"@babel/plugin-transform-literals@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.16.7.tgz#254c9618c5ff749e87cb0c0cef1a0a050c0bdab1" - integrity sha512-6tH8RTpTWI0s2sV6uq3e/C9wPo4PTqqZps4uF0kzQ9/xPLFQtipynvmT1g/dOfEJ+0EQsHhkQ/zyRId8J2b8zQ== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - -"@babel/plugin-transform-member-expression-literals@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.16.7.tgz#6e5dcf906ef8a098e630149d14c867dd28f92384" - integrity sha512-mBruRMbktKQwbxaJof32LT9KLy2f3gH+27a5XSuXo6h7R3vqltl0PgZ80C8ZMKw98Bf8bqt6BEVi3svOh2PzMw== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - -"@babel/plugin-transform-modules-amd@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.16.7.tgz#b28d323016a7daaae8609781d1f8c9da42b13186" - integrity sha512-KaaEtgBL7FKYwjJ/teH63oAmE3lP34N3kshz8mm4VMAw7U3PxjVwwUmxEFksbgsNUaO3wId9R2AVQYSEGRa2+g== - dependencies: - "@babel/helper-module-transforms" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" - babel-plugin-dynamic-import-node "^2.3.3" - -"@babel/plugin-transform-modules-commonjs@^7.16.8": - version "7.16.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.16.8.tgz#cdee19aae887b16b9d331009aa9a219af7c86afe" - integrity sha512-oflKPvsLT2+uKQopesJt3ApiaIS2HW+hzHFcwRNtyDGieAeC/dIHZX8buJQ2J2X1rxGPy4eRcUijm3qcSPjYcA== - dependencies: - "@babel/helper-module-transforms" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/helper-simple-access" "^7.16.7" - babel-plugin-dynamic-import-node "^2.3.3" - -"@babel/plugin-transform-modules-systemjs@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.16.7.tgz#887cefaef88e684d29558c2b13ee0563e287c2d7" - integrity sha512-DuK5E3k+QQmnOqBR9UkusByy5WZWGRxfzV529s9nPra1GE7olmxfqO2FHobEOYSPIjPBTr4p66YDcjQnt8cBmw== - dependencies: - "@babel/helper-hoist-variables" "^7.16.7" - "@babel/helper-module-transforms" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/helper-validator-identifier" "^7.16.7" - babel-plugin-dynamic-import-node "^2.3.3" - -"@babel/plugin-transform-modules-umd@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.16.7.tgz#23dad479fa585283dbd22215bff12719171e7618" - integrity sha512-EMh7uolsC8O4xhudF2F6wedbSHm1HHZ0C6aJ7K67zcDNidMzVcxWdGr+htW9n21klm+bOn+Rx4CBsAntZd3rEQ== - dependencies: - "@babel/helper-module-transforms" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" - -"@babel/plugin-transform-named-capturing-groups-regex@^7.16.8": - version "7.16.8" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.16.8.tgz#7f860e0e40d844a02c9dcf9d84965e7dfd666252" - integrity sha512-j3Jw+n5PvpmhRR+mrgIh04puSANCk/T/UA3m3P1MjJkhlK906+ApHhDIqBQDdOgL/r1UYpz4GNclTXxyZrYGSw== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.16.7" - -"@babel/plugin-transform-new-target@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.16.7.tgz#9967d89a5c243818e0800fdad89db22c5f514244" - integrity sha512-xiLDzWNMfKoGOpc6t3U+etCE2yRnn3SM09BXqWPIZOBpL2gvVrBWUKnsJx0K/ADi5F5YC5f8APFfWrz25TdlGg== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - -"@babel/plugin-transform-object-super@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.16.7.tgz#ac359cf8d32cf4354d27a46867999490b6c32a94" - integrity sha512-14J1feiQVWaGvRxj2WjyMuXS2jsBkgB3MdSN5HuC2G5nRspa5RK9COcs82Pwy5BuGcjb+fYaUj94mYcOj7rCvw== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/helper-replace-supers" "^7.16.7" - -"@babel/plugin-transform-parameters@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.16.7.tgz#a1721f55b99b736511cb7e0152f61f17688f331f" - integrity sha512-AT3MufQ7zZEhU2hwOA11axBnExW0Lszu4RL/tAlUJBuNoRak+wehQW8h6KcXOcgjY42fHtDxswuMhMjFEuv/aw== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - -"@babel/plugin-transform-property-literals@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.16.7.tgz#2dadac85155436f22c696c4827730e0fe1057a55" - integrity sha512-z4FGr9NMGdoIl1RqavCqGG+ZuYjfZ/hkCIeuH6Do7tXmSm0ls11nYVSJqFEUOSJbDab5wC6lRE/w6YjVcr6Hqw== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - -"@babel/plugin-transform-regenerator@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.16.7.tgz#9e7576dc476cb89ccc5096fff7af659243b4adeb" - integrity sha512-mF7jOgGYCkSJagJ6XCujSQg+6xC1M77/03K2oBmVJWoFGNUtnVJO4WHKJk3dnPC8HCcj4xBQP1Egm8DWh3Pb3Q== - dependencies: - regenerator-transform "^0.14.2" - -"@babel/plugin-transform-reserved-words@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.16.7.tgz#1d798e078f7c5958eec952059c460b220a63f586" - integrity sha512-KQzzDnZ9hWQBjwi5lpY5v9shmm6IVG0U9pB18zvMu2i4H90xpT4gmqwPYsn8rObiadYe2M0gmgsiOIF5A/2rtg== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - -"@babel/plugin-transform-runtime@^7.17.0": - version "7.17.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.17.0.tgz#0a2e08b5e2b2d95c4b1d3b3371a2180617455b70" - integrity sha512-fr7zPWnKXNc1xoHfrIU9mN/4XKX4VLZ45Q+oMhfsYIaHvg7mHgmhfOy/ckRWqDK7XF3QDigRpkh5DKq6+clE8A== - dependencies: - "@babel/helper-module-imports" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" - babel-plugin-polyfill-corejs2 "^0.3.0" - babel-plugin-polyfill-corejs3 "^0.5.0" - babel-plugin-polyfill-regenerator "^0.3.0" - semver "^6.3.0" - -"@babel/plugin-transform-shorthand-properties@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.16.7.tgz#e8549ae4afcf8382f711794c0c7b6b934c5fbd2a" - integrity sha512-hah2+FEnoRoATdIb05IOXf+4GzXYTq75TVhIn1PewihbpyrNWUt2JbudKQOETWw6QpLe+AIUpJ5MVLYTQbeeUg== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - -"@babel/plugin-transform-spread@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.16.7.tgz#a303e2122f9f12e0105daeedd0f30fb197d8ff44" - integrity sha512-+pjJpgAngb53L0iaA5gU/1MLXJIfXcYepLgXB3esVRf4fqmj8f2cxM3/FKaHsZms08hFQJkFccEWuIpm429TXg== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.16.0" - -"@babel/plugin-transform-sticky-regex@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.16.7.tgz#c84741d4f4a38072b9a1e2e3fd56d359552e8660" - integrity sha512-NJa0Bd/87QV5NZZzTuZG5BPJjLYadeSZ9fO6oOUoL4iQx+9EEuw/eEM92SrsT19Yc2jgB1u1hsjqDtH02c3Drw== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - -"@babel/plugin-transform-template-literals@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.16.7.tgz#f3d1c45d28967c8e80f53666fc9c3e50618217ab" - integrity sha512-VwbkDDUeenlIjmfNeDX/V0aWrQH2QiVyJtwymVQSzItFDTpxfyJh3EVaQiS0rIN/CqbLGr0VcGmuwyTdZtdIsA== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - -"@babel/plugin-transform-typeof-symbol@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.16.7.tgz#9cdbe622582c21368bd482b660ba87d5545d4f7e" - integrity sha512-p2rOixCKRJzpg9JB4gjnG4gjWkWa89ZoYUnl9snJ1cWIcTH/hvxZqfO+WjG6T8DRBpctEol5jw1O5rA8gkCokQ== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - -"@babel/plugin-transform-unicode-escapes@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.16.7.tgz#da8717de7b3287a2c6d659750c964f302b31ece3" - integrity sha512-TAV5IGahIz3yZ9/Hfv35TV2xEm+kaBDaZQCn2S/hG9/CZ0DktxJv9eKfPc7yYCvOYR4JGx1h8C+jcSOvgaaI/Q== - dependencies: - "@babel/helper-plugin-utils" "^7.16.7" - -"@babel/plugin-transform-unicode-regex@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.16.7.tgz#0f7aa4a501198976e25e82702574c34cfebe9ef2" - integrity sha512-oC5tYYKw56HO75KZVLQ+R/Nl3Hro9kf8iG0hXoaHP7tjAyCpvqBiSNe6vGrZni1Z6MggmUOC6A7VP7AVmw225Q== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" - -"@babel/preset-env@^7.16.11": - version "7.16.11" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.16.11.tgz#5dd88fd885fae36f88fd7c8342475c9f0abe2982" - integrity sha512-qcmWG8R7ZW6WBRPZK//y+E3Cli151B20W1Rv7ln27vuPaXU/8TKms6jFdiJtF7UDTxcrb7mZd88tAeK9LjdT8g== - dependencies: - "@babel/compat-data" "^7.16.8" - "@babel/helper-compilation-targets" "^7.16.7" - "@babel/helper-plugin-utils" "^7.16.7" - "@babel/helper-validator-option" "^7.16.7" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.16.7" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.16.7" - "@babel/plugin-proposal-async-generator-functions" "^7.16.8" - "@babel/plugin-proposal-class-properties" "^7.16.7" - "@babel/plugin-proposal-class-static-block" "^7.16.7" - "@babel/plugin-proposal-dynamic-import" "^7.16.7" - "@babel/plugin-proposal-export-namespace-from" "^7.16.7" - "@babel/plugin-proposal-json-strings" "^7.16.7" - "@babel/plugin-proposal-logical-assignment-operators" "^7.16.7" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.16.7" - "@babel/plugin-proposal-numeric-separator" "^7.16.7" - "@babel/plugin-proposal-object-rest-spread" "^7.16.7" - "@babel/plugin-proposal-optional-catch-binding" "^7.16.7" - "@babel/plugin-proposal-optional-chaining" "^7.16.7" - "@babel/plugin-proposal-private-methods" "^7.16.11" - "@babel/plugin-proposal-private-property-in-object" "^7.16.7" - "@babel/plugin-proposal-unicode-property-regex" "^7.16.7" - "@babel/plugin-syntax-async-generators" "^7.8.4" - "@babel/plugin-syntax-class-properties" "^7.12.13" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - "@babel/plugin-syntax-json-strings" "^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - "@babel/plugin-syntax-top-level-await" "^7.14.5" - "@babel/plugin-transform-arrow-functions" "^7.16.7" - "@babel/plugin-transform-async-to-generator" "^7.16.8" - "@babel/plugin-transform-block-scoped-functions" "^7.16.7" - "@babel/plugin-transform-block-scoping" "^7.16.7" - "@babel/plugin-transform-classes" "^7.16.7" - "@babel/plugin-transform-computed-properties" "^7.16.7" - "@babel/plugin-transform-destructuring" "^7.16.7" - "@babel/plugin-transform-dotall-regex" "^7.16.7" - "@babel/plugin-transform-duplicate-keys" "^7.16.7" - "@babel/plugin-transform-exponentiation-operator" "^7.16.7" - "@babel/plugin-transform-for-of" "^7.16.7" - "@babel/plugin-transform-function-name" "^7.16.7" - "@babel/plugin-transform-literals" "^7.16.7" - "@babel/plugin-transform-member-expression-literals" "^7.16.7" - "@babel/plugin-transform-modules-amd" "^7.16.7" - "@babel/plugin-transform-modules-commonjs" "^7.16.8" - "@babel/plugin-transform-modules-systemjs" "^7.16.7" - "@babel/plugin-transform-modules-umd" "^7.16.7" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.16.8" - "@babel/plugin-transform-new-target" "^7.16.7" - "@babel/plugin-transform-object-super" "^7.16.7" - "@babel/plugin-transform-parameters" "^7.16.7" - "@babel/plugin-transform-property-literals" "^7.16.7" - "@babel/plugin-transform-regenerator" "^7.16.7" - "@babel/plugin-transform-reserved-words" "^7.16.7" - "@babel/plugin-transform-shorthand-properties" "^7.16.7" - "@babel/plugin-transform-spread" "^7.16.7" - "@babel/plugin-transform-sticky-regex" "^7.16.7" - "@babel/plugin-transform-template-literals" "^7.16.7" - "@babel/plugin-transform-typeof-symbol" "^7.16.7" - "@babel/plugin-transform-unicode-escapes" "^7.16.7" - "@babel/plugin-transform-unicode-regex" "^7.16.7" - "@babel/preset-modules" "^0.1.5" - "@babel/types" "^7.16.8" - babel-plugin-polyfill-corejs2 "^0.3.0" - babel-plugin-polyfill-corejs3 "^0.5.0" - babel-plugin-polyfill-regenerator "^0.3.0" - core-js-compat "^3.20.2" - semver "^6.3.0" - -"@babel/preset-modules@^0.1.5": - version "0.1.5" - resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.5.tgz#ef939d6e7f268827e1841638dc6ff95515e115d9" - integrity sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" - "@babel/plugin-transform-dotall-regex" "^7.4.4" - "@babel/types" "^7.4.4" - esutils "^2.0.2" - -"@babel/register@^7.17.0": - version "7.17.0" - resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.17.0.tgz#8051e0b7cb71385be4909324f072599723a1f084" - integrity sha512-UNZsMAZ7uKoGHo1HlEXfteEOYssf64n/PNLHGqOKq/bgYcu/4LrQWAHJwSCb3BRZK8Hi5gkJdRcwrGTO2wtRCg== - dependencies: - clone-deep "^4.0.1" - find-cache-dir "^2.0.0" - make-dir "^2.1.0" - pirates "^4.0.5" - source-map-support "^0.5.16" - -"@babel/runtime@^7.17.2", "@babel/runtime@^7.8.4": - version "7.17.2" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941" - integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw== - dependencies: - regenerator-runtime "^0.13.4" - -"@babel/template@^7.16.7": - version "7.16.7" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" - integrity sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w== - dependencies: - "@babel/code-frame" "^7.16.7" - "@babel/parser" "^7.16.7" - "@babel/types" "^7.16.7" - -"@babel/traverse@^7.13.0", "@babel/traverse@^7.16.7", "@babel/traverse@^7.16.8", "@babel/traverse@^7.17.0", "@babel/traverse@^7.17.3": - version "7.17.3" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.17.3.tgz#0ae0f15b27d9a92ba1f2263358ea7c4e7db47b57" - integrity sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw== - dependencies: - "@babel/code-frame" "^7.16.7" - "@babel/generator" "^7.17.3" - "@babel/helper-environment-visitor" "^7.16.7" - "@babel/helper-function-name" "^7.16.7" - "@babel/helper-hoist-variables" "^7.16.7" - "@babel/helper-split-export-declaration" "^7.16.7" - "@babel/parser" "^7.17.3" - "@babel/types" "^7.17.0" - debug "^4.1.0" - globals "^11.1.0" - -"@babel/types@^7.16.0", "@babel/types@^7.16.7", "@babel/types@^7.16.8", "@babel/types@^7.17.0", "@babel/types@^7.4.4": - version "7.17.0" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.17.0.tgz#a826e368bccb6b3d84acd76acad5c0d87342390b" - integrity sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw== - dependencies: - "@babel/helper-validator-identifier" "^7.16.7" - to-fast-properties "^2.0.0" - -"@eslint/eslintrc@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.0.tgz#7ce1547a5c46dfe56e1e45c3c9ed18038c721c6a" - integrity sha512-igm9SjJHNEJRiUnecP/1R5T3wKLEJ7pL6e2P+GUSfCd0dGjPYYZve08uzw8L2J8foVHFz+NGu12JxRcU2gGo6w== - dependencies: - ajv "^6.12.4" - debug "^4.3.2" - espree "^9.3.1" - globals "^13.9.0" - ignore "^4.0.6" - import-fresh "^3.2.1" - js-yaml "^4.1.0" - minimatch "^3.0.4" - strip-json-comments "^3.1.1" - -"@humanwhocodes/config-array@^0.9.2": - version "0.9.5" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.5.tgz#2cbaf9a89460da24b5ca6531b8bbfc23e1df50c7" - integrity sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw== - dependencies: - "@humanwhocodes/object-schema" "^1.2.1" - debug "^4.1.1" - minimatch "^3.0.4" - -"@humanwhocodes/object-schema@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" - integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== - -"@jest/types@^27.5.1": - version "27.5.1" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.5.1.tgz#3c79ec4a8ba61c170bf937bcf9e98a9df175ec80" - integrity sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw== - dependencies: - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^3.0.0" - "@types/node" "*" - "@types/yargs" "^16.0.0" - chalk "^4.0.0" - -"@jridgewell/resolve-uri@^3.0.3": - version "3.0.5" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz#68eb521368db76d040a6315cdb24bf2483037b9c" - integrity sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew== - -"@jridgewell/sourcemap-codec@^1.4.10": - version "1.4.11" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz#771a1d8d744eeb71b6adb35808e1a6c7b9b8c8ec" - integrity sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg== - -"@jridgewell/trace-mapping@^0.3.0": - version "0.3.4" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz#f6a0832dffd5b8a6aaa633b7d9f8e8e94c83a0c3" - integrity sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ== - dependencies: - "@jridgewell/resolve-uri" "^3.0.3" - "@jridgewell/sourcemap-codec" "^1.4.10" - -"@nodelib/fs.scandir@2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" - integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== - dependencies: - "@nodelib/fs.stat" "2.0.5" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" - integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== - -"@nodelib/fs.walk@^1.2.3": - version "1.2.8" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" - integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== - dependencies: - "@nodelib/fs.scandir" "2.1.5" - fastq "^1.6.0" - -"@sindresorhus/is@^4.0.0": - version "4.6.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f" - integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw== - -"@szmarczak/http-timer@^4.0.5": - version "4.0.6" - resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz#b4a914bb62e7c272d4e5989fe4440f812ab1d807" - integrity sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w== - dependencies: - defer-to-connect "^2.0.0" - -"@testim/chrome-version@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@testim/chrome-version/-/chrome-version-1.1.2.tgz#092005c5b77bd3bb6576a4677110a11485e11864" - integrity sha512-1c4ZOETSRpI0iBfIFUqU4KqwBAB2lHUAlBjZz/YqOHqwM9dTTzjV6Km0ZkiEiSCx/tLr1BtESIKyWWMww+RUqw== - -"@types/aria-query@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.0.tgz#df2d64b5cc73cca0d75e2a7793d6b5c199c2f7b2" - integrity sha512-P+dkdFu0n08PDIvw+9nT9ByQnd+Udc8DaWPb9HKfaPwCvWvQpC5XaMRx2xLWECm9x1VKNps6vEAlirjA6+uNrQ== - -"@types/cacheable-request@^6.0.1": - version "6.0.2" - resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.2.tgz#c324da0197de0a98a2312156536ae262429ff6b9" - integrity sha512-B3xVo+dlKM6nnKTcmm5ZtY/OL8bOAOd2Olee9M1zft65ox50OzjEHW91sDiU9j6cvW8Ejg1/Qkf4xd2kugApUA== - dependencies: - "@types/http-cache-semantics" "*" - "@types/keyv" "*" - "@types/node" "*" - "@types/responselike" "*" - -"@types/diff@^5.0.0": - version "5.0.2" - resolved "https://registry.yarnpkg.com/@types/diff/-/diff-5.0.2.tgz#dd565e0086ccf8bc6522c6ebafd8a3125c91c12b" - integrity sha512-uw8eYMIReOwstQ0QKF0sICefSy8cNO/v7gOTiIy9SbwuHyEecJUm7qlgueOO5S1udZ5I/irVydHVwMchgzbKTg== - -"@types/easy-table@^0.0.33": - version "0.0.33" - resolved "https://registry.yarnpkg.com/@types/easy-table/-/easy-table-0.0.33.tgz#b1f7ec29014ec24906b4f28d8368e2e99b399313" - integrity sha512-/vvqcJPmZUfQwCgemL0/34G7bIQnCuvgls379ygRlcC1FqNqk3n+VZ15dAO51yl6JNDoWd8vsk+kT8zfZ1VZSw== - -"@types/ejs@^3.0.5": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@types/ejs/-/ejs-3.1.0.tgz#ab8109208106b5e764e5a6c92b2ba1c625b73020" - integrity sha512-DCg+Ka+uDQ31lJ/UtEXVlaeV3d6t81gifaVWKJy4MYVVgvJttyX/viREy+If7fz+tK/gVxTGMtyrFPnm4gjrVA== - -"@types/fibers@^3.1.0": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@types/fibers/-/fibers-3.1.1.tgz#b714d357eebf6aec0bc5d70512e573b89bc84f20" - integrity sha512-yHoUi46uika0snoTpNcVqUSvgbRndaIps4TUCotrXjtc0DHDoPQckmyXEZ2bX3e4mpJmyEW3hRhCwQa/ISCPaA== - -"@types/fs-extra@^9.0.4": - version "9.0.13" - resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.13.tgz#7594fbae04fe7f1918ce8b3d213f74ff44ac1f45" - integrity sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA== - dependencies: - "@types/node" "*" - -"@types/http-cache-semantics@*": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz#0ea7b61496902b95890dc4c3a116b60cb8dae812" - integrity sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ== - -"@types/inquirer@^8.1.2": - version "8.2.0" - resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-8.2.0.tgz#b9566d048f5ff65159f2ed97aff45fe0f00b35ec" - integrity sha512-BNoMetRf3gmkpAlV5we+kxyZTle7YibdOntIZbU5pyIfMdcwy784KfeZDAcuyMznkh5OLa17RVXZOGA5LTlkgQ== - dependencies: - "@types/through" "*" - rxjs "^7.2.0" - -"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" - integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== - -"@types/istanbul-lib-report@*": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" - integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== - dependencies: - "@types/istanbul-lib-coverage" "*" - -"@types/istanbul-reports@^3.0.0": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff" - integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw== - dependencies: - "@types/istanbul-lib-report" "*" - -"@types/json5@^0.0.29": - version "0.0.29" - resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" - integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= - -"@types/keyv@*": - version "3.1.3" - resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.3.tgz#1c9aae32872ec1f20dcdaee89a9f3ba88f465e41" - integrity sha512-FXCJgyyN3ivVgRoml4h94G/p3kY+u/B86La+QptcqJaWtBWtmc6TtkNfS40n9bIvyLteHh7zXOtgbobORKPbDg== - dependencies: - "@types/node" "*" - -"@types/lodash.flattendeep@^4.4.6": - version "4.4.6" - resolved "https://registry.yarnpkg.com/@types/lodash.flattendeep/-/lodash.flattendeep-4.4.6.tgz#2686d9161ae6c3d56d6745fa118308d88562ae53" - integrity sha512-uLm2MaRVlqJSGsMK0RZpP5T3KqReq+9WbYDHCUhBhp98v56hMG/Yht52bsoTSui9xz2mUvQ9NfG3LrNGDL92Ng== - dependencies: - "@types/lodash" "*" - -"@types/lodash.pickby@^4.6.6": - version "4.6.6" - resolved "https://registry.yarnpkg.com/@types/lodash.pickby/-/lodash.pickby-4.6.6.tgz#3dc39c2b38432f7a0c5e5627b0d5c0e3878b4f35" - integrity sha512-NFa13XxlMd9eFi0UFZFWIztpMpXhozbijrx3Yb1viYZphT7jyopIFVoIRF4eYMjruWNEG1rnyrRmg/8ej9T8Iw== - dependencies: - "@types/lodash" "*" - -"@types/lodash.union@^4.6.6": - version "4.6.6" - resolved "https://registry.yarnpkg.com/@types/lodash.union/-/lodash.union-4.6.6.tgz#2f77f2088326ed147819e9e384182b99aae8d4b0" - integrity sha512-Wu0ZEVNcyCz8eAn6TlUbYWZoGbH9E+iOHxAZbwUoCEXdUiy6qpcz5o44mMXViM4vlPLLCPlkAubEP1gokoSZaw== - dependencies: - "@types/lodash" "*" - -"@types/lodash@*": - version "4.14.179" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.179.tgz#490ec3288088c91295780237d2497a3aa9dfb5c5" - integrity sha512-uwc1x90yCKqGcIOAT6DwOSuxnrAbpkdPsUOZtwrXb4D/6wZs+6qG7QnIawDuZWg0sWpxl+ltIKCaLoMlna678w== - -"@types/mocha@^9.0.0": - version "9.1.0" - resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.0.tgz#baf17ab2cca3fcce2d322ebc30454bff487efad5" - integrity sha512-QCWHkbMv4Y5U9oW10Uxbr45qMMSzl4OzijsozynUAgx3kEHUdXB00udx2dWDQ7f2TU2a2uuiFaRZjCe3unPpeg== - -"@types/node@*", "@types/node@^17.0.4": - version "17.0.21" - resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.21.tgz#864b987c0c68d07b4345845c3e63b75edd143644" - integrity sha512-DBZCJbhII3r90XbQxI8Y9IjjiiOGlZ0Hr32omXIZvwwZ7p4DMMXGrKXVyPfuoBOri9XNtL0UK69jYIBIsRX3QQ== - -"@types/object-inspect@^1.8.0": - version "1.8.1" - resolved "https://registry.yarnpkg.com/@types/object-inspect/-/object-inspect-1.8.1.tgz#7c08197ad05cc0e513f529b1f3919cc99f720e1f" - integrity sha512-0JTdf3CGV0oWzE6Wa40Ayv2e2GhpP3pEJMcrlM74vBSJPuuNkVwfDnl0SZxyFCXETcB4oKA/MpTVfuYSMOelBg== - -"@types/puppeteer@^5.4.0": - version "5.4.5" - resolved "https://registry.yarnpkg.com/@types/puppeteer/-/puppeteer-5.4.5.tgz#154e3850a77bfd3967f036680de8ddc88eb3a12b" - integrity sha512-lxCjpDEY+DZ66+W3x5Af4oHnEmUXt0HuaRzkBGE2UZiZEp/V1d3StpLPlmNVu/ea091bdNmVPl44lu8Wy/0ZCA== - dependencies: - "@types/node" "*" - -"@types/recursive-readdir@^2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@types/recursive-readdir/-/recursive-readdir-2.2.0.tgz#b39cd5474fd58ea727fe434d5c68b7a20ba9121c" - integrity sha512-HGk753KRu2N4mWduovY4BLjYq4jTOL29gV2OfGdGxHcPSWGFkC5RRIdk+VTs5XmYd7MVAD+JwKrcb5+5Y7FOCg== - dependencies: - "@types/node" "*" - -"@types/responselike@*", "@types/responselike@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" - integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA== - dependencies: - "@types/node" "*" - -"@types/stack-utils@^2.0.0": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" - integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== - -"@types/stream-buffers@^3.0.3": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@types/stream-buffers/-/stream-buffers-3.0.4.tgz#bf128182da7bc62722ca0ddf5458a9c65f76e648" - integrity sha512-qU/K1tb2yUdhXkLIATzsIPwbtX6BpZk0l3dPW6xqWyhfzzM1ECaQ/8faEnu3CNraLiQ9LHyQQPBGp7N9Fbs25w== - dependencies: - "@types/node" "*" - -"@types/supports-color@^8.1.0": - version "8.1.1" - resolved "https://registry.yarnpkg.com/@types/supports-color/-/supports-color-8.1.1.tgz#1b44b1b096479273adf7f93c75fc4ecc40a61ee4" - integrity sha512-dPWnWsf+kzIG140B8z2w3fr5D03TLWbOAFQl45xUpI3vcizeXriNR5VYkWZ+WTMsUHqZ9Xlt3hrxGNANFyNQfw== - -"@types/through@*": - version "0.0.30" - resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.30.tgz#e0e42ce77e897bd6aead6f6ea62aeb135b8a3895" - integrity sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg== - dependencies: - "@types/node" "*" - -"@types/tmp@^0.2.0": - version "0.2.3" - resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.2.3.tgz#908bfb113419fd6a42273674c00994d40902c165" - integrity sha512-dDZH/tXzwjutnuk4UacGgFRwV+JSLaXL1ikvidfJprkb7L9Nx1njcRHHmi3Dsvt7pgqqTEeucQuOrWHPFgzVHA== - -"@types/ua-parser-js@^0.7.33": - version "0.7.36" - resolved "https://registry.yarnpkg.com/@types/ua-parser-js/-/ua-parser-js-0.7.36.tgz#9bd0b47f26b5a3151be21ba4ce9f5fa457c5f190" - integrity sha512-N1rW+njavs70y2cApeIw1vLMYXRwfBy+7trgavGuuTfOd7j1Yh7QTRc/yqsPl6ncokt72ZXuxEU0PiCp9bSwNQ== - -"@types/which@^1.3.2": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@types/which/-/which-1.3.2.tgz#9c246fc0c93ded311c8512df2891fb41f6227fdf" - integrity sha512-8oDqyLC7eD4HM307boe2QWKyuzdzWBj56xI/imSl2cpL+U3tCMaTAkMJ4ee5JBZ/FsOJlvRGeIShiZDAl1qERA== - -"@types/yargs-parser@*": - version "21.0.0" - resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" - integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== - -"@types/yargs@^16.0.0": - version "16.0.4" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-16.0.4.tgz#26aad98dd2c2a38e421086ea9ad42b9e51642977" - integrity sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw== - dependencies: - "@types/yargs-parser" "*" - -"@types/yauzl@^2.9.1": - version "2.9.2" - resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.2.tgz#c48e5d56aff1444409e39fa164b0b4d4552a7b7a" - integrity sha512-8uALY5LTvSuHgloDVUvWP3pIauILm+8/0pDMokuDYIoNsOkSwd5AiHBTSEJjKTDcZr5z8UpgOWZkxBF4iJftoA== - dependencies: - "@types/node" "*" - -"@ungap/promise-all-settled@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" - integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== - -"@wdio/cli@^7.16.16": - version "7.17.4" - resolved "https://registry.yarnpkg.com/@wdio/cli/-/cli-7.17.4.tgz#a978caab4029541bc9d6493f27046e262dc73e49" - integrity sha512-nG+ZCKQ8Bu+rXYuHdsJ5Al/aNlD6YrU75DWaOU4JCJJmt/ulhVZ2awIyTiIYzRsZVwohPmm8lR01wYlMwHRX9g== - dependencies: - "@types/ejs" "^3.0.5" - "@types/fs-extra" "^9.0.4" - "@types/inquirer" "^8.1.2" - "@types/lodash.flattendeep" "^4.4.6" - "@types/lodash.pickby" "^4.6.6" - "@types/lodash.union" "^4.6.6" - "@types/node" "^17.0.4" - "@types/recursive-readdir" "^2.2.0" - "@wdio/config" "7.17.3" - "@wdio/logger" "7.17.3" - "@wdio/types" "7.17.3" - "@wdio/utils" "7.17.3" - async-exit-hook "^2.0.1" - chalk "^4.0.0" - chokidar "^3.0.0" - cli-spinners "^2.1.0" - ejs "^3.0.1" - fs-extra "^10.0.0" - inquirer "8.1.5" - lodash.flattendeep "^4.4.0" - lodash.pickby "^4.6.0" - lodash.union "^4.6.0" - mkdirp "^1.0.4" - recursive-readdir "^2.2.2" - webdriverio "7.17.4" - yargs "^17.0.0" - yarn-install "^1.0.0" - -"@wdio/config@7.17.3": - version "7.17.3" - resolved "https://registry.yarnpkg.com/@wdio/config/-/config-7.17.3.tgz#23a004e7685db98394a6e1c99f0a53968a44bd2c" - integrity sha512-MSWCsx0w1EbxbwOD8ykTxHqgx208CWoz9n4oWHx7Q1APfetqWFLM4O7K8cdZS1gV4IvH4EAV9807L91K8r0JNw== - dependencies: - "@wdio/logger" "7.17.3" - "@wdio/types" "7.17.3" - deepmerge "^4.0.0" - glob "^7.1.2" - -"@wdio/local-runner@^7.16.16": - version "7.17.4" - resolved "https://registry.yarnpkg.com/@wdio/local-runner/-/local-runner-7.17.4.tgz#2e25e94631a4565e763498a0ccc8e9d5964a9fef" - integrity sha512-Gw3zAirGhHMyhedrzF4sumLjXwHo3AuUGTKwrIsZCrxloVT2TogO7882czmVRbaeo8SuvQbwpTDrdogf5hVsJw== - dependencies: - "@types/stream-buffers" "^3.0.3" - "@wdio/logger" "7.17.3" - "@wdio/repl" "7.17.3" - "@wdio/runner" "7.17.4" - "@wdio/types" "7.17.3" - async-exit-hook "^2.0.1" - split2 "^4.0.0" - stream-buffers "^3.0.2" - -"@wdio/logger@7.17.3", "@wdio/logger@^7.5.3": - version "7.17.3" - resolved "https://registry.yarnpkg.com/@wdio/logger/-/logger-7.17.3.tgz#fcdfcbd9892173f9df0de67f448ae6d6f2aa6c3b" - integrity sha512-hpvJDsJMX8G/8gXHOEipxkQPjojjA+BRCZqCvZRLCVpWm2JB7tBoMzu9sUJXcpSkY03b94KAd4EwNA2uNAf9aQ== - dependencies: - chalk "^4.0.0" - loglevel "^1.6.0" - loglevel-plugin-prefix "^0.8.4" - strip-ansi "^6.0.0" - -"@wdio/mocha-framework@^7.16.15": - version "7.17.3" - resolved "https://registry.yarnpkg.com/@wdio/mocha-framework/-/mocha-framework-7.17.3.tgz#ffbf118d674ca997cf523618780ddb46e84ad3fb" - integrity sha512-6vT4Pf/u+sjQTGrhUd+IrkuhAVT6ojp6jcqywCD+4v2HwdRDek8PShZq3HsvEvqRDCZhiMeyISHkqE8D2ozpgg== - dependencies: - "@types/mocha" "^9.0.0" - "@wdio/logger" "7.17.3" - "@wdio/types" "7.17.3" - "@wdio/utils" "7.17.3" - expect-webdriverio "^3.0.0" - mocha "^9.0.0" - -"@wdio/protocols@7.17.3": - version "7.17.3" - resolved "https://registry.yarnpkg.com/@wdio/protocols/-/protocols-7.17.3.tgz#a140bba3bdcfd108bee94a99c02e34752dfa8187" - integrity sha512-DxVRil2uMDOshk0gMOrmemC9uEZuB5Dv4bJX/ozZwXPV9AHd6oJqUrsF/fs8bT9+4AWkE58yqsRBFc/pt7sFMw== - -"@wdio/repl@7.17.3": - version "7.17.3" - resolved "https://registry.yarnpkg.com/@wdio/repl/-/repl-7.17.3.tgz#49c82f7c2842c348c8266b864b86e36e3f018773" - integrity sha512-ZX4dYnoOb9NC3IQFhva4B7FCoVx9v7CIG7g5W4bX/un5Xfyz3Fne1vGP9Aku15nyIaXRSCzuV6vpT/5KR6q6Hg== - dependencies: - "@wdio/utils" "7.17.3" - -"@wdio/reporter@7.17.3": - version "7.17.3" - resolved "https://registry.yarnpkg.com/@wdio/reporter/-/reporter-7.17.3.tgz#115f5bdbafb423ce7de52a9940d057973664b446" - integrity sha512-jf+Qapw6gXLgi2IQJ2Jd7EqwUt2UFKxWYUwPeKPKpyC3LNnnO4muv/wATYnIhCllgae8VUttvomg2jqFYhqxNQ== - dependencies: - "@types/diff" "^5.0.0" - "@types/node" "^17.0.4" - "@types/object-inspect" "^1.8.0" - "@types/supports-color" "^8.1.0" - "@types/tmp" "^0.2.0" - "@wdio/types" "7.17.3" - diff "^5.0.0" - fs-extra "^10.0.0" - object-inspect "^1.10.3" - supports-color "8.1.1" - -"@wdio/runner@7.17.4": - version "7.17.4" - resolved "https://registry.yarnpkg.com/@wdio/runner/-/runner-7.17.4.tgz#dd3a5e0e07c24db03b23152bfd2dafd742f379bf" - integrity sha512-TV4zbRAIxcV+5Y6R8FN+1wVPqPDaiX8W7p0D6aayZo0+HpU0hFTCW73dmKbCqJ4a49JsNcJkITA83dLmRWJ8mg== - dependencies: - "@wdio/config" "7.17.3" - "@wdio/logger" "7.17.3" - "@wdio/types" "7.17.3" - "@wdio/utils" "7.17.3" - deepmerge "^4.0.0" - gaze "^1.1.2" - webdriver "7.17.3" - webdriverio "7.17.4" - -"@wdio/spec-reporter@^7.16.14": - version "7.17.3" - resolved "https://registry.yarnpkg.com/@wdio/spec-reporter/-/spec-reporter-7.17.3.tgz#ccaabecf61a989fd729dcba2eaddcd9ed5824999" - integrity sha512-p5kzIuA8jljGq/8EFZV3N2SdpMPFfXKULPj8YKhvd/XZFJk3YTNT67GVcOC+Qz4SUobYf4o/L3+w60KS2NI5RQ== - dependencies: - "@types/easy-table" "^0.0.33" - "@wdio/reporter" "7.17.3" - "@wdio/types" "7.17.3" - chalk "^4.0.0" - easy-table "^1.1.1" - pretty-ms "^7.0.0" - -"@wdio/sync@^7.16.16": - version "7.17.4" - resolved "https://registry.yarnpkg.com/@wdio/sync/-/sync-7.17.4.tgz#4401f7e6831f68c64eef32f9c19342a1741ba46f" - integrity sha512-XJp+awbHlxiE/WP29CBM3qQ5Y4Je9e3DtRiTGL99f4UpyyqkDaX8oOkpMfzs1GKBBlmx4rEgnc3T+0AzBGAoXQ== - dependencies: - "@types/fibers" "^3.1.0" - "@types/puppeteer" "^5.4.0" - "@wdio/logger" "7.17.3" - "@wdio/types" "7.17.3" - fibers "^5.0.0" - webdriverio "7.17.4" - -"@wdio/types@7.17.3": - version "7.17.3" - resolved "https://registry.yarnpkg.com/@wdio/types/-/types-7.17.3.tgz#79b3d3ac0d8ac6c88f51c1af1deb12630d127246" - integrity sha512-j8kYdaMl4NFRS8M1bFDuEa3GMbUZbLQY7i6XEnJSetyW0GyMDLlzwcfXI4DdX85+3JbO5624UGKxVsQcuA7T3A== - dependencies: - "@types/node" "^17.0.4" - got "^11.8.1" - -"@wdio/utils@7.17.3": - version "7.17.3" - resolved "https://registry.yarnpkg.com/@wdio/utils/-/utils-7.17.3.tgz#5e7063543d8517fdc9eb05b6b4a9ccd5dbb2c38d" - integrity sha512-20bGTCmgBNVKa2BJs3B5kxbsryjhfEOoKDnFjZ/rAVZYT1t1sg0e/W+vRfamd++NqTaIHOY/IKGEFiEnCw5nXw== - dependencies: - "@wdio/logger" "7.17.3" - "@wdio/types" "7.17.3" - p-iteration "^1.1.8" - -acorn-jsx@^5.3.1: - version "5.3.2" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" - integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== - -acorn@^8.7.0: - version "8.7.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" - integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== - -agent-base@6: - version "6.0.2" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" - integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== - dependencies: - debug "4" - -aggregate-error@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" - integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== - dependencies: - clean-stack "^2.0.0" - indent-string "^4.0.0" - -ajv@^6.10.0, ajv@^6.12.4: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ansi-colors@4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" - integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== - -ansi-escapes@^4.2.1, ansi-escapes@^4.3.0: - version "4.3.2" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" - integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== - dependencies: - type-fest "^0.21.3" - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-regex@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" - integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== - -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= - -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -ansi-styles@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" - integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== - -ansi-styles@^6.0.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.1.0.tgz#87313c102b8118abd57371afab34618bf7350ed3" - integrity sha512-VbqNsoz55SYGczauuup0MFUyXNQviSpFTj1RQtFzmQLk18qbVSpTFFGMT293rmDaQuKCT6InmbuEyUne4mTuxQ== - -anymatch@~3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" - integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -archiver-utils@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-2.1.0.tgz#e8a460e94b693c3e3da182a098ca6285ba9249e2" - integrity sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw== - dependencies: - glob "^7.1.4" - graceful-fs "^4.2.0" - lazystream "^1.0.0" - lodash.defaults "^4.2.0" - lodash.difference "^4.5.0" - lodash.flatten "^4.4.0" - lodash.isplainobject "^4.0.6" - lodash.union "^4.6.0" - normalize-path "^3.0.0" - readable-stream "^2.0.0" - -archiver@^5.0.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/archiver/-/archiver-5.3.0.tgz#dd3e097624481741df626267564f7dd8640a45ba" - integrity sha512-iUw+oDwK0fgNpvveEsdQ0Ase6IIKztBJU2U0E9MzszMfmVVUyv1QJhS2ITW9ZCqx8dktAxVAjWWkKehuZE8OPg== - dependencies: - archiver-utils "^2.1.0" - async "^3.2.0" - buffer-crc32 "^0.2.1" - readable-stream "^3.6.0" - readdir-glob "^1.0.0" - tar-stream "^2.2.0" - zip-stream "^4.1.0" - -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - -aria-query@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.0.0.tgz#210c21aaf469613ee8c9a62c7f86525e058db52c" - integrity sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg== - -array-includes@^3.1.4: - version "3.1.4" - resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.4.tgz#f5b493162c760f3539631f005ba2bb46acb45ba9" - integrity sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" - get-intrinsic "^1.1.1" - is-string "^1.0.7" - -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - -array.prototype.flat@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz#07e0975d84bbc7c48cd1879d609e682598d33e13" - integrity sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.0" - -asn1.js@^5.0.1: - version "5.4.1" - resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" - integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== - dependencies: - bn.js "^4.0.0" - inherits "^2.0.1" - minimalistic-assert "^1.0.0" - safer-buffer "^2.1.0" - -assertion-error@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" - integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== - -astral-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" - integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== - -async-exit-hook@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/async-exit-hook/-/async-exit-hook-2.0.1.tgz#8bd8b024b0ec9b1c01cccb9af9db29bd717dfaf3" - integrity sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw== - -async@0.9.x: - version "0.9.2" - resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" - integrity sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0= - -async@^3.2.0: - version "3.2.3" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9" - integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g== - -at-least-node@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" - integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== - -axios@^0.24.0: - version "0.24.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.24.0.tgz#804e6fa1e4b9c5288501dd9dff56a7a0940d20d6" - integrity sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA== - dependencies: - follow-redirects "^1.14.4" - -babel-plugin-dynamic-import-node@^2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" - integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ== - dependencies: - object.assign "^4.1.0" - -babel-plugin-polyfill-corejs2@^0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.1.tgz#440f1b70ccfaabc6b676d196239b138f8a2cfba5" - integrity sha512-v7/T6EQcNfVLfcN2X8Lulb7DjprieyLWJK/zOWH5DUYcAgex9sP3h25Q+DLsX9TloXe3y1O8l2q2Jv9q8UVB9w== - dependencies: - "@babel/compat-data" "^7.13.11" - "@babel/helper-define-polyfill-provider" "^0.3.1" - semver "^6.1.1" - -babel-plugin-polyfill-corejs3@^0.5.0: - version "0.5.2" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.2.tgz#aabe4b2fa04a6e038b688c5e55d44e78cd3a5f72" - integrity sha512-G3uJih0XWiID451fpeFaYGVuxHEjzKTHtc9uGFEjR6hHrvNzeS/PX+LLLcetJcytsB5m4j+K3o/EpXJNb/5IEQ== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.3.1" - core-js-compat "^3.21.0" - -babel-plugin-polyfill-regenerator@^0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.1.tgz#2c0678ea47c75c8cc2fbb1852278d8fb68233990" - integrity sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.3.1" - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -base64-js@^1.3.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -base64url@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" - integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== - -binary-extensions@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== - -bl@^4.0.3, bl@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" - integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== - dependencies: - buffer "^5.5.0" - inherits "^2.0.4" - readable-stream "^3.4.0" - -bn.js@^4.0.0: - version "4.12.0" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" - integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -brace-expansion@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" - integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== - dependencies: - balanced-match "^1.0.0" - -braces@^3.0.1, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - -browser-stdout@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" - integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== - -browserslist@^4.17.5, browserslist@^4.19.1: - version "4.20.0" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.0.tgz#35951e3541078c125d36df76056e94738a52ebe9" - integrity sha512-bnpOoa+DownbciXj0jVGENf8VYQnE2LNWomhYuCsMmmx9Jd9lwq0WXODuwpSsp8AVdKM2/HorrzxAfbKvWTByQ== - dependencies: - caniuse-lite "^1.0.30001313" - electron-to-chromium "^1.4.76" - escalade "^3.1.1" - node-releases "^2.0.2" - picocolors "^1.0.0" - -buffer-crc32@^0.2.1, buffer-crc32@^0.2.13, buffer-crc32@~0.2.3: - version "0.2.13" - resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" - integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= - -buffer-from@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" - integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== - -buffer@^5.2.1, buffer@^5.5.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" - integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.1.13" - -buffer@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" - integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.2.1" - -cac@^3.0.3: - version "3.0.4" - resolved "https://registry.yarnpkg.com/cac/-/cac-3.0.4.tgz#6d24ceec372efe5c9b798808bc7f49b47242a4ef" - integrity sha1-bSTO7Dcu/lybeYgIvH9JtHJCpO8= - dependencies: - camelcase-keys "^3.0.0" - chalk "^1.1.3" - indent-string "^3.0.0" - minimist "^1.2.0" - read-pkg-up "^1.0.1" - suffix "^0.1.0" - text-table "^0.2.0" - -cacheable-lookup@^5.0.3: - version "5.0.4" - resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz#5a6b865b2c44357be3d5ebc2a467b032719a7005" - integrity sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA== - -cacheable-request@^7.0.2: - version "7.0.2" - resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.2.tgz#ea0d0b889364a25854757301ca12b2da77f91d27" - integrity sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew== - dependencies: - clone-response "^1.0.2" - get-stream "^5.1.0" - http-cache-semantics "^4.0.0" - keyv "^4.0.0" - lowercase-keys "^2.0.0" - normalize-url "^6.0.1" - responselike "^2.0.0" - -call-bind@^1.0.0, call-bind@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== - dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -camelcase-keys@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-3.0.0.tgz#fc0c6c360363f7377e3793b9a16bccf1070c1ca4" - integrity sha1-/AxsNgNj9zd+N5O5oWvM8QcMHKQ= - dependencies: - camelcase "^3.0.0" - map-obj "^1.0.0" - -camelcase@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" - integrity sha1-MvxLn82vhF/N9+c7uXysImHwqwo= - -camelcase@^6.0.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" - integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== - -caniuse-lite@^1.0.30001313: - version "1.0.30001314" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001314.tgz#65c7f9fb7e4594fca0a333bec1d8939662377596" - integrity sha512-0zaSO+TnCHtHJIbpLroX7nsD+vYuOVjl3uzFbJO1wMVbuveJA0RK2WcQA9ZUIOiO0/ArMiMgHJLxfEZhQiC0kw== - -chai@^4.3.6: - version "4.3.6" - resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.6.tgz#ffe4ba2d9fa9d6680cc0b370adae709ec9011e9c" - integrity sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q== - dependencies: - assertion-error "^1.1.0" - check-error "^1.0.2" - deep-eql "^3.0.1" - get-func-name "^2.0.0" - loupe "^2.3.1" - pathval "^1.1.1" - type-detect "^4.0.5" - -chalk@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - -chalk@^2.0.0, chalk@^2.0.1, chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1: - version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chardet@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" - integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== - -check-error@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" - integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= - -chokidar@3.5.3, chokidar@^3.0.0, chokidar@^3.5.0: - version "3.5.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" - integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - -chownr@^1.1.1: - version "1.1.4" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" - integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== - -chrome-launcher@^0.15.0: - version "0.15.0" - resolved "https://registry.yarnpkg.com/chrome-launcher/-/chrome-launcher-0.15.0.tgz#5144a57aba0cf2f4cbe61dccefdde024fb3ca7fc" - integrity sha512-ZQqX5kb9H0+jy1OqLnWampfocrtSZaGl7Ny3F9GRha85o4odbL8x55paUzh51UC7cEmZ5obp3H2Mm70uC2PpRA== - dependencies: - "@types/node" "*" - escape-string-regexp "^4.0.0" - is-wsl "^2.2.0" - lighthouse-logger "^1.0.0" - -chromedriver@^99.0.0: - version "99.0.0" - resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-99.0.0.tgz#fbfcc7e74991dd50962e7dd456d78eaf49f56774" - integrity sha512-pyB+5LuyZdb7EBPL3i5D5yucZUD+SlkdiUtmpjaEnLd9zAXp+SvD/hP5xF4l/ZmWvUo/1ZLxAI1YBdhazGTpgA== - dependencies: - "@testim/chrome-version" "^1.1.2" - axios "^0.24.0" - del "^6.0.0" - extract-zip "^2.0.1" - https-proxy-agent "^5.0.0" - proxy-from-env "^1.1.0" - tcp-port-used "^1.0.1" - -clean-stack@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" - integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== - -cli-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" - integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== - dependencies: - restore-cursor "^3.1.0" - -cli-spinners@^2.1.0, cli-spinners@^2.5.0: - version "2.6.1" - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.1.tgz#adc954ebe281c37a6319bfa401e6dd2488ffb70d" - integrity sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g== - -cli-truncate@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" - integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== - dependencies: - slice-ansi "^3.0.0" - string-width "^4.2.0" - -cli-truncate@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-3.1.0.tgz#3f23ab12535e3d73e839bb43e73c9de487db1389" - integrity sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA== - dependencies: - slice-ansi "^5.0.0" - string-width "^5.0.0" - -cli-width@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" - integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== - -cliui@^7.0.2: - version "7.0.4" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" - integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^7.0.0" - -clone-deep@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" - integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== - dependencies: - is-plain-object "^2.0.4" - kind-of "^6.0.2" - shallow-clone "^3.0.0" - -clone-response@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" - integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= - dependencies: - mimic-response "^1.0.0" - -clone@^1.0.2: - version "1.0.4" - resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" - integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= - -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -colorette@^2.0.16: - version "2.0.16" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da" - integrity sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g== - -commander@^8.3.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" - integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== - -commondir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" - integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= - -compress-commons@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-4.1.1.tgz#df2a09a7ed17447642bad10a85cc9a19e5c42a7d" - integrity sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ== - dependencies: - buffer-crc32 "^0.2.13" - crc32-stream "^4.0.2" - normalize-path "^3.0.0" - readable-stream "^3.6.0" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -convert-source-map@^1.7.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" - integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== - dependencies: - safe-buffer "~5.1.1" - -core-js-compat@^3.20.2, core-js-compat@^3.21.0: - version "3.21.1" - resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.21.1.tgz#cac369f67c8d134ff8f9bd1623e3bc2c42068c82" - integrity sha512-gbgX5AUvMb8gwxC7FLVWYT7Kkgu/y7+h/h1X43yJkNqhlK2fuYyQimqvKGNZFAY6CKii/GFKJ2cp/1/42TN36g== - dependencies: - browserslist "^4.19.1" - semver "7.0.0" - -core-util-is@~1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" - integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== - -crc-32@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.1.tgz#436d2bcaad27bcb6bd073a2587139d3024a16460" - integrity sha512-Dn/xm/1vFFgs3nfrpEVScHoIslO9NZRITWGz/1E/St6u4xw99vfZzVkW0OSnzx2h9egej9xwMCEut6sqwokM/w== - dependencies: - exit-on-epipe "~1.0.1" - printj "~1.3.1" - -crc32-stream@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-4.0.2.tgz#c922ad22b38395abe9d3870f02fa8134ed709007" - integrity sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w== - dependencies: - crc-32 "^1.2.0" - readable-stream "^3.4.0" - -cross-fetch@3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" - integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== - dependencies: - node-fetch "2.6.7" - -cross-spawn@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41" - integrity sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE= - dependencies: - lru-cache "^4.0.1" - which "^1.2.9" - -cross-spawn@^7.0.2, cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -css-shorthand-properties@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/css-shorthand-properties/-/css-shorthand-properties-1.1.1.tgz#1c808e63553c283f289f2dd56fcee8f3337bd935" - integrity sha512-Md+Juc7M3uOdbAFwOYlTrccIZ7oCFuzrhKYQjdeUEW/sE1hv17Jp/Bws+ReOPpGVBTYCBoYo+G17V5Qo8QQ75A== - -css-value@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/css-value/-/css-value-0.0.1.tgz#5efd6c2eea5ea1fd6b6ac57ec0427b18452424ea" - integrity sha1-Xv1sLupeof1rasV+wEJ7GEUkJOo= - -debug@4, debug@4.3.3, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3: - version "4.3.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" - integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== - dependencies: - ms "2.1.2" - -debug@4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" - integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== - dependencies: - ms "2.1.2" - -debug@^2.6.8, debug@^2.6.9: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@^3.2.7: - version "3.2.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== - dependencies: - ms "^2.1.1" - -decamelize@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" - integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== - -decompress-response@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" - integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== - dependencies: - mimic-response "^3.1.0" - -deep-eql@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df" - integrity sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw== - dependencies: - type-detect "^4.0.0" - -deep-is@^0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" - integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== - -deepmerge@^4.0.0: - version "4.2.2" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" - integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== - -defaults@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" - integrity sha1-xlYFHpgX2f8I7YgUd/P+QBnz730= - dependencies: - clone "^1.0.2" - -defer-to-connect@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" - integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== - -define-properties@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== - dependencies: - object-keys "^1.0.12" - -del@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/del/-/del-6.0.0.tgz#0b40d0332cea743f1614f818be4feb717714c952" - integrity sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ== - dependencies: - globby "^11.0.1" - graceful-fs "^4.2.4" - is-glob "^4.0.1" - is-path-cwd "^2.2.0" - is-path-inside "^3.0.2" - p-map "^4.0.0" - rimraf "^3.0.2" - slash "^3.0.0" - -detect-libc@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" - integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= - -devtools-protocol@0.0.969999: - version "0.0.969999" - resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.969999.tgz#3d6be0a126b3607bb399ae2719b471dda71f3478" - integrity sha512-6GfzuDWU0OFAuOvBokXpXPLxjOJ5DZ157Ue3sGQQM3LgAamb8m0R0ruSfN0DDu+XG5XJgT50i6zZ/0o8RglreQ== - -devtools-protocol@^0.0.979353: - version "0.0.979353" - resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.979353.tgz#4aa394d344b6c1a37437d5e16bb3d10330d708af" - integrity sha512-/A7o8FU5n4i2WN/RH6opBbteawPbNgyKmmyl6Ts4zpQ5FVq/cGe2K/qGr8t80BLVu8KynTckHbdpaLCwxzRyFA== - -devtools@7.17.3: - version "7.17.3" - resolved "https://registry.yarnpkg.com/devtools/-/devtools-7.17.3.tgz#3f6b77b7e12df10080a5289658538b63f87d6c8e" - integrity sha512-y5O+z+q7cUuAKMY9ZNGexbb62MUimKAJX7OkFecix2Fl9+YFSmAQUUtHWrTt9qFkw5NJNMdiXZhQvk+JdfRygw== - dependencies: - "@types/node" "^17.0.4" - "@types/ua-parser-js" "^0.7.33" - "@wdio/config" "7.17.3" - "@wdio/logger" "7.17.3" - "@wdio/protocols" "7.17.3" - "@wdio/types" "7.17.3" - "@wdio/utils" "7.17.3" - chrome-launcher "^0.15.0" - edge-paths "^2.1.0" - puppeteer-core "^13.1.3" - query-selector-shadow-dom "^1.0.0" - ua-parser-js "^1.0.1" - uuid "^8.0.0" - -diff-sequences@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" - integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ== - -diff@5.0.0, diff@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" - integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== - -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - -doctrine@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" - integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== - dependencies: - esutils "^2.0.2" - -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - -eastasianwidth@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" - integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== - -easy-table@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/easy-table/-/easy-table-1.2.0.tgz#ba9225d7138fee307bfd4f0b5bc3c04bdc7c54eb" - integrity sha512-OFzVOv03YpvtcWGe5AayU5G2hgybsg3iqA6drU8UaoZyB9jLGMTrz9+asnLp/E+6qPh88yEI1gvyZFZ41dmgww== - dependencies: - ansi-regex "^5.0.1" - optionalDependencies: - wcwidth "^1.0.1" - -edge-paths@^2.1.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/edge-paths/-/edge-paths-2.2.1.tgz#d2d91513225c06514aeac9843bfce546abbf4391" - integrity sha512-AI5fC7dfDmCdKo3m5y7PkYE8m6bMqR6pvVpgtrZkkhcJXFLelUgkjrhk3kXXx8Kbw2cRaTT4LkOR7hqf39KJdw== - dependencies: - "@types/which" "^1.3.2" - which "^2.0.2" - -ejs@^3.0.1: - version "3.1.6" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.6.tgz#5bfd0a0689743bb5268b3550cceeebbc1702822a" - integrity sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw== - dependencies: - jake "^10.6.1" - -electron-to-chromium@^1.4.76: - version "1.4.81" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.81.tgz#a9ce8997232fb9fb0ec53de8931a85b18c0a7383" - integrity sha512-Gs7xVpIZ7tYYSDA+WgpzwpPvfGwUk3KSIjJ0akuj5XQHFdyQnsUoM76EA4CIHXNLPiVwTwOFay9RMb0ChG3OBw== - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -emoji-regex@^9.2.2: - version "9.2.2" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" - integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== - -end-of-stream@^1.1.0, end-of-stream@^1.4.1: - version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -error-ex@^1.2.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -es-abstract@^1.19.0, es-abstract@^1.19.1: - version "1.19.1" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3" - integrity sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w== - dependencies: - call-bind "^1.0.2" - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - get-intrinsic "^1.1.1" - get-symbol-description "^1.0.0" - has "^1.0.3" - has-symbols "^1.0.2" - internal-slot "^1.0.3" - is-callable "^1.2.4" - is-negative-zero "^2.0.1" - is-regex "^1.1.4" - is-shared-array-buffer "^1.0.1" - is-string "^1.0.7" - is-weakref "^1.0.1" - object-inspect "^1.11.0" - object-keys "^1.1.1" - object.assign "^4.1.2" - string.prototype.trimend "^1.0.4" - string.prototype.trimstart "^1.0.4" - unbox-primitive "^1.0.1" - -es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - -es6-promise@^4.2.8: - version "4.2.8" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" - integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== - -escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== - -escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - -escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - -escape-string-regexp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" - integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== - -eslint-cli@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/eslint-cli/-/eslint-cli-1.1.1.tgz#ae6979edd8ee6e78c6d413b525f4052cb2a94cfd" - integrity sha512-Gu+fYzt7M+jIb5szUHLl5Ex0vFY7zErbi78D7ZaaLunvVTxHRvbOlfzmJlIUWsV5WDM4qyu9TD7WnGgDaDgaMA== - dependencies: - chalk "^2.0.1" - debug "^2.6.8" - resolve "^1.3.3" - -eslint-config-standard@^14.1.1: - version "14.1.1" - resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-14.1.1.tgz#830a8e44e7aef7de67464979ad06b406026c56ea" - integrity sha512-Z9B+VR+JIXRxz21udPTL9HpFMyoMUEeX1G251EQ6e05WD9aPVtVBn09XUmZ259wCMlCDmYDSZG62Hhm+ZTJcUg== - -eslint-import-resolver-node@^0.3.6: - version "0.3.6" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz#4048b958395da89668252001dbd9eca6b83bacbd" - integrity sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw== - dependencies: - debug "^3.2.7" - resolve "^1.20.0" - -eslint-module-utils@^2.7.2: - version "2.7.3" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz#ad7e3a10552fdd0642e1e55292781bd6e34876ee" - integrity sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ== - dependencies: - debug "^3.2.7" - find-up "^2.1.0" - -eslint-plugin-chai-friendly@^0.7.2: - version "0.7.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-chai-friendly/-/eslint-plugin-chai-friendly-0.7.2.tgz#0ebfbb2c1244f5de2997f3963d155758234f2b0f" - integrity sha512-LOIfGx5sZZ5FwM1shr2GlYAWV9Omdi+1/3byuVagvQNoGUuU0iHhp7AfjA1uR+4dJ4Isfb4+FwBJgQajIw9iAg== - -eslint-plugin-es@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz#75a7cdfdccddc0589934aeeb384175f221c57893" - integrity sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ== - dependencies: - eslint-utils "^2.0.0" - regexpp "^3.0.0" - -eslint-plugin-import@^2.25.4: - version "2.25.4" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.25.4.tgz#322f3f916a4e9e991ac7af32032c25ce313209f1" - integrity sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA== - dependencies: - array-includes "^3.1.4" - array.prototype.flat "^1.2.5" - debug "^2.6.9" - doctrine "^2.1.0" - eslint-import-resolver-node "^0.3.6" - eslint-module-utils "^2.7.2" - has "^1.0.3" - is-core-module "^2.8.0" - is-glob "^4.0.3" - minimatch "^3.0.4" - object.values "^1.1.5" - resolve "^1.20.0" - tsconfig-paths "^3.12.0" - -eslint-plugin-json@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-json/-/eslint-plugin-json-3.1.0.tgz#251108ba1681c332e0a442ef9513bd293619de67" - integrity sha512-MrlG2ynFEHe7wDGwbUuFPsaT2b1uhuEFhJ+W1f1u+1C2EkXmTYJp4B1aAdQQ8M+CC3t//N/oRKiIVw14L2HR1g== - dependencies: - lodash "^4.17.21" - vscode-json-languageservice "^4.1.6" - -eslint-plugin-node@^11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz#c95544416ee4ada26740a30474eefc5402dc671d" - integrity sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g== - dependencies: - eslint-plugin-es "^3.0.0" - eslint-utils "^2.0.0" - ignore "^5.1.1" - minimatch "^3.0.4" - resolve "^1.10.1" - semver "^6.1.0" - -eslint-plugin-promise@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-6.0.0.tgz#017652c07c9816413a41e11c30adc42c3d55ff18" - integrity sha512-7GPezalm5Bfi/E22PnQxDWH2iW9GTvAlUNTztemeHb6c1BniSyoeTrM87JkC0wYdi6aQrZX9p2qEiAno8aTcbw== - -eslint-plugin-standard@^4.0.1: - version "4.1.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-4.1.0.tgz#0c3bf3a67e853f8bbbc580fb4945fbf16f41b7c5" - integrity sha512-ZL7+QRixjTR6/528YNGyDotyffm5OQst/sGxKDwGb9Uqs4In5Egi4+jbobhqJoyoCM6/7v/1A5fhQ7ScMtDjaQ== - -eslint-scope@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642" - integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== - dependencies: - esrecurse "^4.3.0" - estraverse "^5.2.0" - -eslint-utils@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" - integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== - dependencies: - eslint-visitor-keys "^1.1.0" - -eslint-utils@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" - integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== - dependencies: - eslint-visitor-keys "^2.0.0" - -eslint-visitor-keys@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" - integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== - -eslint-visitor-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" - integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== - -eslint-visitor-keys@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" - integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== - -eslint@^8.10.0: - version "8.10.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.10.0.tgz#931be395eb60f900c01658b278e05b6dae47199d" - integrity sha512-tcI1D9lfVec+R4LE1mNDnzoJ/f71Kl/9Cv4nG47jOueCMBrCCKYXr4AUVS7go6mWYGFD4+EoN6+eXSrEbRzXVw== - dependencies: - "@eslint/eslintrc" "^1.2.0" - "@humanwhocodes/config-array" "^0.9.2" - ajv "^6.10.0" - chalk "^4.0.0" - cross-spawn "^7.0.2" - debug "^4.3.2" - doctrine "^3.0.0" - escape-string-regexp "^4.0.0" - eslint-scope "^7.1.1" - eslint-utils "^3.0.0" - eslint-visitor-keys "^3.3.0" - espree "^9.3.1" - esquery "^1.4.0" - esutils "^2.0.2" - fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" - functional-red-black-tree "^1.0.1" - glob-parent "^6.0.1" - globals "^13.6.0" - ignore "^5.2.0" - import-fresh "^3.0.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - js-yaml "^4.1.0" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" - lodash.merge "^4.6.2" - minimatch "^3.0.4" - natural-compare "^1.4.0" - optionator "^0.9.1" - regexpp "^3.2.0" - strip-ansi "^6.0.1" - strip-json-comments "^3.1.0" - text-table "^0.2.0" - v8-compile-cache "^2.0.3" - -espree@^9.3.1: - version "9.3.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.1.tgz#8793b4bc27ea4c778c19908e0719e7b8f4115bcd" - integrity sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ== - dependencies: - acorn "^8.7.0" - acorn-jsx "^5.3.1" - eslint-visitor-keys "^3.3.0" - -esquery@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" - integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== - dependencies: - estraverse "^5.1.0" - -esrecurse@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== - dependencies: - estraverse "^5.2.0" - -estraverse@^5.1.0, estraverse@^5.2.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" - integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -execa@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" - integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== - dependencies: - cross-spawn "^7.0.3" - get-stream "^6.0.0" - human-signals "^2.1.0" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^4.0.1" - onetime "^5.1.2" - signal-exit "^3.0.3" - strip-final-newline "^2.0.0" - -exit-on-epipe@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz#0bdd92e87d5285d267daa8171d0eb06159689692" - integrity sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw== - -expect-webdriverio@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/expect-webdriverio/-/expect-webdriverio-3.2.0.tgz#dee513bc89e2b499bedef861739df1438a6c387c" - integrity sha512-NZplTSIrLVMs0CfBgiX936n9jRzucckvt+2Ecct2ki80s3bAS/uy253w+aPbXlginKygdeT5bNs0Tfmo8ZR88Q== - dependencies: - expect "^27.0.2" - jest-matcher-utils "^27.0.2" - -expect@^27.0.2: - version "27.5.1" - resolved "https://registry.yarnpkg.com/expect/-/expect-27.5.1.tgz#83ce59f1e5bdf5f9d2b94b61d2050db48f3fef74" - integrity sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw== - dependencies: - "@jest/types" "^27.5.1" - jest-get-type "^27.5.1" - jest-matcher-utils "^27.5.1" - jest-message-util "^27.5.1" - -external-editor@^3.0.3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" - integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== - dependencies: - chardet "^0.7.0" - iconv-lite "^0.4.24" - tmp "^0.0.33" - -extract-zip@2.0.1, extract-zip@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" - integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== - dependencies: - debug "^4.1.1" - get-stream "^5.1.0" - yauzl "^2.10.0" - optionalDependencies: - "@types/yauzl" "^2.9.1" - -fast-deep-equal@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" - integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= - -fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-glob@^3.2.9: - version "3.2.11" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" - integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -fast-levenshtein@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= - -fastq@^1.6.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" - integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== - dependencies: - reusify "^1.0.4" - -fd-slicer@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" - integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= - dependencies: - pend "~1.2.0" - -fibers@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/fibers/-/fibers-5.0.1.tgz#bb9b02aa022685185d21aed227363e456d87660d" - integrity sha512-VMC7Frt87Oo0AOJ6EcPFbi+tZmkQ4tD85aatwyWL6I9cYMJmm2e+pXUJsfGZ36U7MffXtjou2XIiWJMtHriErw== - dependencies: - detect-libc "^1.0.3" - -figures@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" - integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== - dependencies: - escape-string-regexp "^1.0.5" - -file-entry-cache@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" - integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== - dependencies: - flat-cache "^3.0.4" - -filelist@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.2.tgz#80202f21462d4d1c2e214119b1807c1bc0380e5b" - integrity sha512-z7O0IS8Plc39rTCq6i6iHxk43duYOn8uFJiWSewIq0Bww1RNybVHSCjahmcC87ZqAm4OTvFzlzeGu3XAzG1ctQ== - dependencies: - minimatch "^3.0.4" - -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - -find-cache-dir@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" - integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== - dependencies: - commondir "^1.0.1" - make-dir "^2.0.0" - pkg-dir "^3.0.0" - -find-up@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" - integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== - dependencies: - locate-path "^6.0.0" - path-exists "^4.0.0" - -find-up@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" - integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8= - dependencies: - path-exists "^2.0.0" - pinkie-promise "^2.0.0" - -find-up@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" - integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= - dependencies: - locate-path "^2.0.0" - -find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== - dependencies: - locate-path "^3.0.0" - -find-up@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - -flat-cache@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" - integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== - dependencies: - flatted "^3.1.0" - rimraf "^3.0.2" - -flat@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" - integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== - -flatted@^3.1.0: - version "3.2.5" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3" - integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== - -follow-redirects@^1.14.4: - version "1.14.9" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" - integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== - -fs-constants@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" - integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== - -fs-extra@^10.0.0: - version "10.0.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.1.tgz#27de43b4320e833f6867cc044bfce29fdf0ef3b8" - integrity sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - -fs-extra@^9.1.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" - integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== - dependencies: - at-least-node "^1.0.0" - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== - -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" - integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= - -gaze@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.3.tgz#c441733e13b927ac8c0ff0b4c3b033f28812924a" - integrity sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g== - dependencies: - globule "^1.0.0" - -gensync@^1.0.0-beta.2: - version "1.0.0-beta.2" - resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" - integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== - -get-caller-file@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -get-func-name@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" - integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE= - -get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" - integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== - dependencies: - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - -get-port@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" - integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== - -get-stream@^5.1.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" - integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== - dependencies: - pump "^3.0.0" - -get-stream@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" - integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== - -get-symbol-description@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" - integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.1" - -glob-parent@^5.1.2, glob-parent@~5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob-parent@^6.0.1: - version "6.0.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" - integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== - dependencies: - is-glob "^4.0.3" - -glob@7.2.0, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: - version "7.2.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" - integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@~7.1.1: - version "7.1.7" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" - integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -globals@^11.1.0: - version "11.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" - integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== - -globals@^13.6.0, globals@^13.9.0: - version "13.12.1" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.12.1.tgz#ec206be932e6c77236677127577aa8e50bf1c5cb" - integrity sha512-317dFlgY2pdJZ9rspXDks7073GpDmXdfbM3vYYp0HAMKGDh1FfWPleI2ljVNLQX5M5lXcAslTcPTrOrMEFOjyw== - dependencies: - type-fest "^0.20.2" - -globby@^11.0.1: - version "11.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" - integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.2.9" - ignore "^5.2.0" - merge2 "^1.4.1" - slash "^3.0.0" - -globule@^1.0.0: - version "1.3.3" - resolved "https://registry.yarnpkg.com/globule/-/globule-1.3.3.tgz#811919eeac1ab7344e905f2e3be80a13447973c2" - integrity sha512-mb1aYtDbIjTu4ShMB85m3UzjX9BVKe9WCzsnfMSZk+K5GpIbBOexgg4PPCt5eHDEG5/ZQAUX2Kct02zfiPLsKg== - dependencies: - glob "~7.1.1" - lodash "~4.17.10" - minimatch "~3.0.2" - -got@^11.0.2, got@^11.8.1: - version "11.8.3" - resolved "https://registry.yarnpkg.com/got/-/got-11.8.3.tgz#f496c8fdda5d729a90b4905d2b07dbd148170770" - integrity sha512-7gtQ5KiPh1RtGS9/Jbv1ofDpBFuq42gyfEib+ejaRBJuj/3tQFeR5+gw57e4ipaU8c/rCjvX6fkQz2lyDlGAOg== - dependencies: - "@sindresorhus/is" "^4.0.0" - "@szmarczak/http-timer" "^4.0.5" - "@types/cacheable-request" "^6.0.1" - "@types/responselike" "^1.0.0" - cacheable-lookup "^5.0.3" - cacheable-request "^7.0.2" - decompress-response "^6.0.0" - http2-wrapper "^1.0.0-beta.5.2" - lowercase-keys "^2.0.0" - p-cancelable "^2.0.0" - responselike "^2.0.0" - -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.9: - version "4.2.9" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" - integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== - -grapheme-splitter@^1.0.2: - version "1.0.4" - resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" - integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== - -growl@1.10.5: - version "1.10.5" - resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" - integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== - -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= - dependencies: - ansi-regex "^2.0.0" - -has-bigints@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" - integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-symbols@^1.0.1, has-symbols@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== - -has-tostringtag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" - integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== - dependencies: - has-symbols "^1.0.2" - -has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -he@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" - integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== - -hosted-git-info@^2.1.4: - version "2.8.9" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" - integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== - -http-cache-semantics@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" - integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== - -http2-wrapper@^1.0.0-beta.5.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d" - integrity sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg== - dependencies: - quick-lru "^5.1.1" - resolve-alpn "^1.0.0" - -https-proxy-agent@5.0.0, https-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" - integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== - dependencies: - agent-base "6" - debug "4" - -human-signals@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" - integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== - -iconv-lite@^0.4.24: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -ieee754@^1.1.13, ieee754@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - -ignore@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" - integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== - -ignore@^5.1.1, ignore@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" - integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== - -import-fresh@^3.0.0, import-fresh@^3.2.1: - version "3.3.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" - integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= - -indent-string@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" - integrity sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok= - -indent-string@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" - integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -inquirer@8.1.5: - version "8.1.5" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.1.5.tgz#2dc5159203c826d654915b5fe6990fd17f54a150" - integrity sha512-G6/9xUqmt/r+UvufSyrPpt84NYwhKZ9jLsgMbQzlx804XErNupor8WQdBnBRrXmBfTPpuwf1sV+ss2ovjgdXIg== - dependencies: - ansi-escapes "^4.2.1" - chalk "^4.1.1" - cli-cursor "^3.1.0" - cli-width "^3.0.0" - external-editor "^3.0.3" - figures "^3.0.0" - lodash "^4.17.21" - mute-stream "0.0.8" - ora "^5.4.1" - run-async "^2.4.0" - rxjs "^7.2.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - through "^2.3.6" - -internal-slot@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" - integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== - dependencies: - get-intrinsic "^1.1.0" - has "^1.0.3" - side-channel "^1.0.4" - -ip-regex@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5" - integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q== - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= - -is-bigint@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" - integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== - dependencies: - has-bigints "^1.0.1" - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-boolean-object@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" - integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-callable@^1.1.4, is-callable@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" - integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== - -is-core-module@^2.8.0, is-core-module@^2.8.1: - version "2.8.1" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" - integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA== - dependencies: - has "^1.0.3" - -is-date-object@^1.0.1: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" - integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== - dependencies: - has-tostringtag "^1.0.0" - -is-docker@^2.0.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" - integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-fullwidth-code-point@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz#fae3167c729e7463f8461ce512b080a49268aa88" - integrity sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ== - -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-interactive@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" - integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== - -is-negative-zero@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" - integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== - -is-number-object@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.6.tgz#6a7aaf838c7f0686a50b4553f7e54a96494e89f0" - integrity sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g== - dependencies: - has-tostringtag "^1.0.0" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-path-cwd@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" - integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== - -is-path-inside@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" - integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== - -is-plain-obj@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" - integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== - -is-plain-object@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" - integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== - dependencies: - isobject "^3.0.1" - -is-regex@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" - integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-shared-array-buffer@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz#97b0c85fbdacb59c9c446fe653b82cf2b5b7cfe6" - integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA== - -is-stream@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" - integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== - -is-string@^1.0.5, is-string@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" - integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== - dependencies: - has-tostringtag "^1.0.0" - -is-symbol@^1.0.2, is-symbol@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" - integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== - dependencies: - has-symbols "^1.0.2" - -is-unicode-supported@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" - integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== - -is-url@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52" - integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww== - -is-utf8@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" - integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= - -is-weakref@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" - integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== - dependencies: - call-bind "^1.0.2" - -is-wsl@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" - integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== - dependencies: - is-docker "^2.0.0" - -is2@^2.0.6: - version "2.0.7" - resolved "https://registry.yarnpkg.com/is2/-/is2-2.0.7.tgz#d084e10cab3bd45d6c9dfde7a48599fcbb93fcac" - integrity sha512-4vBQoURAXC6hnLFxD4VW7uc04XiwTTl/8ydYJxKvPwkWQrSjInkuM5VZVg6BGr1/natq69zDuvO9lGpLClJqvA== - dependencies: - deep-is "^0.1.3" - ip-regex "^4.1.0" - is-url "^1.2.4" - -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= - -isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= - -jake@^10.6.1: - version "10.8.2" - resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.2.tgz#ebc9de8558160a66d82d0eadc6a2e58fbc500a7b" - integrity sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A== - dependencies: - async "0.9.x" - chalk "^2.4.2" - filelist "^1.0.1" - minimatch "^3.0.4" - -jest-diff@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def" - integrity sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw== - dependencies: - chalk "^4.0.0" - diff-sequences "^27.5.1" - jest-get-type "^27.5.1" - pretty-format "^27.5.1" - -jest-get-type@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1" - integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw== - -jest-matcher-utils@^27.0.2, jest-matcher-utils@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz#9c0cdbda8245bc22d2331729d1091308b40cf8ab" - integrity sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw== - dependencies: - chalk "^4.0.0" - jest-diff "^27.5.1" - jest-get-type "^27.5.1" - pretty-format "^27.5.1" - -jest-message-util@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-27.5.1.tgz#bdda72806da10d9ed6425e12afff38cd1458b6cf" - integrity sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g== - dependencies: - "@babel/code-frame" "^7.12.13" - "@jest/types" "^27.5.1" - "@types/stack-utils" "^2.0.0" - chalk "^4.0.0" - graceful-fs "^4.2.9" - micromatch "^4.0.4" - pretty-format "^27.5.1" - slash "^3.0.0" - stack-utils "^2.0.3" - -js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-yaml@4.1.0, js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - -jsesc@^2.5.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" - integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== - -jsesc@~0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" - integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= - -json-buffer@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" - integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-stable-stringify-without-jsonify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" - integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= - -json-web-key@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/json-web-key/-/json-web-key-0.4.0.tgz#a8e7268d1741c3a87c51c5070ea7ea988e9a78b7" - integrity sha512-4MwQAsadU3y7ZTfhwup/2lYJUEB7mAOMOjfNvuHGJkgkvXaBaiWlFsuWNFpWkTG23ZlMm56TBB3BI9cVQTxjzA== - dependencies: - asn1.js "^5.0.1" - -json5@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== - dependencies: - minimist "^1.2.0" - -json5@^2.1.2: - version "2.2.0" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3" - integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA== - dependencies: - minimist "^1.2.5" - -jsonc-parser@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.0.0.tgz#abdd785701c7e7eaca8a9ec8cf070ca51a745a22" - integrity sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA== - -jsonfile@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" - integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== - dependencies: - universalify "^2.0.0" - optionalDependencies: - graceful-fs "^4.1.6" - -jsrsasign@^10.5.10: - version "10.5.10" - resolved "https://registry.yarnpkg.com/jsrsasign/-/jsrsasign-10.5.10.tgz#82379cabc27a5a765e1ede3c754a3ab801070a82" - integrity sha512-iwLwWdJVfMjC9ShDvkn1fco9qrjF9J8r4tzeIcgevmSq9Uy+7nSusKQtXCHaY1flffI46DhYuG8wkjn0Ie4HCQ== - -keyv@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.1.1.tgz#02c538bfdbd2a9308cc932d4096f05ae42bfa06a" - integrity sha512-tGv1yP6snQVDSM4X6yxrv2zzq/EvpW+oYiUz6aueW1u9CtS8RzUQYxxmFwgZlO2jSgCxQbchhxaqXXp2hnKGpQ== - dependencies: - json-buffer "3.0.1" - -kind-of@^6.0.2: - version "6.0.3" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== - -ky@^0.30.0: - version "0.30.0" - resolved "https://registry.yarnpkg.com/ky/-/ky-0.30.0.tgz#a3d293e4f6c4604a9a4694eceb6ce30e73d27d64" - integrity sha512-X/u76z4JtDVq10u1JA5UQfatPxgPaVDMYTrgHyiTpGN2z4TMEJkIHsoSBBSg9SWZEIXTKsi9kHgiQ9o3Y/4yog== - -lazystream@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638" - integrity sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw== - dependencies: - readable-stream "^2.0.5" - -levn@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" - integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== - dependencies: - prelude-ls "^1.2.1" - type-check "~0.4.0" - -lighthouse-logger@^1.0.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/lighthouse-logger/-/lighthouse-logger-1.3.0.tgz#ba6303e739307c4eee18f08249524e7dafd510db" - integrity sha512-BbqAKApLb9ywUli+0a+PcV04SyJ/N1q/8qgCNe6U97KbPCS1BTksEuHFLYdvc8DltuhfxIUBqDZsC0bBGtl3lA== - dependencies: - debug "^2.6.9" - marky "^1.2.2" - -lilconfig@2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.4.tgz#f4507d043d7058b380b6a8f5cb7bcd4b34cee082" - integrity sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA== - -lint-staged@^12.3.5: - version "12.3.5" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-12.3.5.tgz#8048ce048c3cac12f57200a06344a54dc91c8fa9" - integrity sha512-oOH36RUs1It7b9U/C7Nl/a0sLfoIBcMB8ramiB3nuJ6brBqzsWiUAFSR5DQ3yyP/OR7XKMpijtgKl2DV1lQ3lA== - dependencies: - cli-truncate "^3.1.0" - colorette "^2.0.16" - commander "^8.3.0" - debug "^4.3.3" - execa "^5.1.1" - lilconfig "2.0.4" - listr2 "^4.0.1" - micromatch "^4.0.4" - normalize-path "^3.0.0" - object-inspect "^1.12.0" - string-argv "^0.3.1" - supports-color "^9.2.1" - yaml "^1.10.2" - -listr2@^4.0.1: - version "4.0.5" - resolved "https://registry.yarnpkg.com/listr2/-/listr2-4.0.5.tgz#9dcc50221583e8b4c71c43f9c7dfd0ef546b75d5" - integrity sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA== - dependencies: - cli-truncate "^2.1.0" - colorette "^2.0.16" - log-update "^4.0.0" - p-map "^4.0.0" - rfdc "^1.3.0" - rxjs "^7.5.5" - through "^2.3.8" - wrap-ansi "^7.0.0" - -livereload-js@^3.3.1: - version "3.3.3" - resolved "https://registry.yarnpkg.com/livereload-js/-/livereload-js-3.3.3.tgz#3e4f5699f741fdf8be6dc9c46c523e4fc1abbd0d" - integrity sha512-a7Jipme3XIBIryJluWP5LQrEAvhobDPyScBe+q+MYwxBiMT2Ck7msy4tAdF8TAa33FMdJqX4guP81Yhiu6BkmQ== - -livereload@^0.9.3: - version "0.9.3" - resolved "https://registry.yarnpkg.com/livereload/-/livereload-0.9.3.tgz#a714816375ed52471408bede8b49b2ee6a0c55b1" - integrity sha512-q7Z71n3i4X0R9xthAryBdNGVGAO2R5X+/xXpmKeuPMrteg+W2U8VusTKV3YiJbXZwKsOlFlHe+go6uSNjfxrZw== - dependencies: - chokidar "^3.5.0" - livereload-js "^3.3.1" - opts ">= 1.2.0" - ws "^7.4.3" - -load-json-file@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" - integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA= - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - pinkie-promise "^2.0.0" - strip-bom "^2.0.0" - -locate-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" - integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= - dependencies: - p-locate "^2.0.0" - path-exists "^3.0.0" - -locate-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" - integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== - dependencies: - p-locate "^3.0.0" - path-exists "^3.0.0" - -locate-path@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" - integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== - dependencies: - p-locate "^4.1.0" - -locate-path@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" - integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== - dependencies: - p-locate "^5.0.0" - -lodash.clonedeep@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" - integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= - -lodash.debounce@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" - integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= - -lodash.defaults@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" - integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= - -lodash.difference@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c" - integrity sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw= - -lodash.flatten@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" - integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= - -lodash.flattendeep@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" - integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI= - -lodash.isobject@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d" - integrity sha1-PI+41bW/S/kK4G4U8qUwpO2TXh0= - -lodash.isplainobject@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" - integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= - -lodash.merge@^4.6.1, lodash.merge@^4.6.2: - version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" - integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== - -lodash.pickby@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/lodash.pickby/-/lodash.pickby-4.6.0.tgz#7dea21d8c18d7703a27c704c15d3b84a67e33aff" - integrity sha1-feoh2MGNdwOifHBMFdO4SmfjOv8= - -lodash.union@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" - integrity sha1-SLtQiECfFvGCFmZkHETdGqrjzYg= - -lodash.zip@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.zip/-/lodash.zip-4.2.0.tgz#ec6662e4896408ed4ab6c542a3990b72cc080020" - integrity sha1-7GZi5IlkCO1KtsVCo5kLcswIACA= - -lodash@^4.17.21, lodash@~4.17.10: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - -log-symbols@4.1.0, log-symbols@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" - integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== - dependencies: - chalk "^4.1.0" - is-unicode-supported "^0.1.0" - -log-update@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" - integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== - dependencies: - ansi-escapes "^4.3.0" - cli-cursor "^3.1.0" - slice-ansi "^4.0.0" - wrap-ansi "^6.2.0" - -loglevel-plugin-prefix@^0.8.4: - version "0.8.4" - resolved "https://registry.yarnpkg.com/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz#2fe0e05f1a820317d98d8c123e634c1bd84ff644" - integrity sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g== - -loglevel@^1.6.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.0.tgz#e7ec73a57e1e7b419cb6c6ac06bf050b67356114" - integrity sha512-G6A/nJLRgWOuuwdNuA6koovfEV1YpqqAG4pRUlFaz3jj2QNZ8M4vBqnVA+HBTmU/AMNUtlOsMmSpF6NyOjztbA== - -long@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/long/-/long-5.2.0.tgz#2696dadf4b4da2ce3f6f6b89186085d94d52fd61" - integrity sha512-9RTUNjK60eJbx3uz+TEGF7fUr29ZDxR5QzXcyDpeSfeH28S9ycINflOgOlppit5U+4kNTe83KQnMEerw7GmE8w== - -loupe@^2.3.1: - version "2.3.4" - resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.4.tgz#7e0b9bffc76f148f9be769cb1321d3dcf3cb25f3" - integrity sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ== - dependencies: - get-func-name "^2.0.0" - -lowercase-keys@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" - integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== - -lru-cache@^4.0.1: - version "4.1.5" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" - integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== - dependencies: - pseudomap "^1.0.2" - yallist "^2.1.2" - -make-dir@^2.0.0, make-dir@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" - integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== - dependencies: - pify "^4.0.1" - semver "^5.6.0" - -map-obj@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" - integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= - -marky@^1.2.2: - version "1.2.4" - resolved "https://registry.yarnpkg.com/marky/-/marky-1.2.4.tgz#d02bb4c08be2366687c778ecd2a328971ce23d7f" - integrity sha512-zd2/GiSn6U3/jeFVZ0J9CA1LzQ8RfIVvXkb/U0swFHF/zT+dVohTAWjmo2DcIuofmIIIROlwTbd+shSeXmxr0w== - -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - -merge2@^1.3.0, merge2@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - -micromatch@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" - integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== - dependencies: - braces "^3.0.1" - picomatch "^2.2.3" - -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - -mimic-response@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" - integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== - -mimic-response@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" - integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== - -minimalistic-assert@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" - integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== - -minimatch@3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -minimatch@^3.0.4: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimatch@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" - integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== - dependencies: - brace-expansion "^2.0.1" - -minimatch@~3.0.2: - version "3.0.8" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.8.tgz#5e6a59bd11e2ab0de1cfb843eb2d82e546c321c1" - integrity sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q== - dependencies: - brace-expansion "^1.1.7" - -minimist@^1.2.0, minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== - -mkdirp-classic@^0.5.2: - version "0.5.3" - resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" - integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== - -mkdirp@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" - integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== - -mocha@^9.0.0: - version "9.2.1" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.2.1.tgz#a1abb675aa9a8490798503af57e8782a78f1338e" - integrity sha512-T7uscqjJVS46Pq1XDXyo9Uvey9gd3huT/DD9cYBb4K2Xc/vbKRPUWK067bxDQRK0yIz6Jxk73IrnimvASzBNAQ== - dependencies: - "@ungap/promise-all-settled" "1.1.2" - ansi-colors "4.1.1" - browser-stdout "1.3.1" - chokidar "3.5.3" - debug "4.3.3" - diff "5.0.0" - escape-string-regexp "4.0.0" - find-up "5.0.0" - glob "7.2.0" - growl "1.10.5" - he "1.2.0" - js-yaml "4.1.0" - log-symbols "4.1.0" - minimatch "3.0.4" - ms "2.1.3" - nanoid "3.2.0" - serialize-javascript "6.0.0" - strip-json-comments "3.1.1" - supports-color "8.1.1" - which "2.0.2" - workerpool "6.2.0" - yargs "16.2.0" - yargs-parser "20.2.4" - yargs-unparser "2.0.0" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -ms@2.1.3, ms@^2.1.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -mute-stream@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" - integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== - -nanoid@3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" - integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA== - -natural-compare@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" - integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= - -node-fetch@2.6.7: - version "2.6.7" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" - integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== - dependencies: - whatwg-url "^5.0.0" - -node-forge@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.2.1.tgz#82794919071ef2eb5c509293325cec8afd0fd53c" - integrity sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w== - -node-jose@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/node-jose/-/node-jose-2.1.0.tgz#a2d12a7ff2d386f23979c1bf77f939449ce073d8" - integrity sha512-Zmm8vFPJabphGBc5Wz1/LUMPS+1cynqw16RIhgVNQMEI2yEQrvl7Gx2EwN9GhP8tkm8f7SH53K2nIx8TeNTIdg== - dependencies: - base64url "^3.0.1" - buffer "^6.0.3" - es6-promise "^4.2.8" - lodash "^4.17.21" - long "^5.2.0" - node-forge "^1.2.1" - pako "^2.0.4" - process "^0.11.10" - uuid "^8.3.2" - -node-releases@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.2.tgz#7139fe71e2f4f11b47d4d2986aaf8c48699e0c01" - integrity sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg== - -normalize-package-data@^2.3.2: - version "2.5.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" - integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== - dependencies: - hosted-git-info "^2.1.4" - resolve "^1.10.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -normalize-url@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" - integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== - -npm-run-path@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" - integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== - dependencies: - path-key "^3.0.0" - -object-inspect@^1.10.3, object-inspect@^1.11.0, object-inspect@^1.12.0, object-inspect@^1.9.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" - integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== - -object-keys@^1.0.12, object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object.assign@^4.1.0, object.assign@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" - integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== - dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - has-symbols "^1.0.1" - object-keys "^1.1.1" - -object.values@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.5.tgz#959f63e3ce9ef108720333082131e4a459b716ac" - integrity sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.1" - -once@^1.3.0, once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -onetime@^5.1.0, onetime@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" - integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== - dependencies: - mimic-fn "^2.1.0" - -optionator@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" - integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== - dependencies: - deep-is "^0.1.3" - fast-levenshtein "^2.0.6" - levn "^0.4.1" - prelude-ls "^1.2.1" - type-check "^0.4.0" - word-wrap "^1.2.3" - -"opts@>= 1.2.0": - version "2.0.2" - resolved "https://registry.yarnpkg.com/opts/-/opts-2.0.2.tgz#a17e189fbbfee171da559edd8a42423bc5993ce1" - integrity sha512-k41FwbcLnlgnFh69f4qdUfvDQ+5vaSDnVPFI/y5XuhKRq97EnVVneO9F1ESVCdiVu4fCS2L8usX3mU331hB7pg== - -ora@^5.4.1: - version "5.4.1" - resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" - integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== - dependencies: - bl "^4.1.0" - chalk "^4.1.0" - cli-cursor "^3.1.0" - cli-spinners "^2.5.0" - is-interactive "^1.0.0" - is-unicode-supported "^0.1.0" - log-symbols "^4.1.0" - strip-ansi "^6.0.0" - wcwidth "^1.0.1" - -os-tmpdir@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= - -p-cancelable@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf" - integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg== - -p-iteration@^1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/p-iteration/-/p-iteration-1.1.8.tgz#14df726d55af368beba81bcc92a26bb1b48e714a" - integrity sha512-IMFBSDIYcPNnW7uWYGrBqmvTiq7W0uB0fJn6shQZs7dlF3OvrHOre+JT9ikSZ7gZS3vWqclVgoQSvToJrns7uQ== - -p-limit@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" - integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== - dependencies: - p-try "^1.0.0" - -p-limit@^2.0.0, p-limit@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-limit@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" - -p-locate@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" - integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= - dependencies: - p-limit "^1.1.0" - -p-locate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" - integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== - dependencies: - p-limit "^2.0.0" - -p-locate@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" - integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== - dependencies: - p-limit "^2.2.0" - -p-locate@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" - integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== - dependencies: - p-limit "^3.0.2" - -p-map@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" - integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== - dependencies: - aggregate-error "^3.0.0" - -p-try@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" - integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -pako@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pako/-/pako-2.0.4.tgz#6cebc4bbb0b6c73b0d5b8d7e8476e2b2fbea576d" - integrity sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg== - -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== - dependencies: - callsites "^3.0.0" - -parse-json@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" - integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= - dependencies: - error-ex "^1.2.0" - -parse-ms@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-2.1.0.tgz#348565a753d4391fa524029956b172cb7753097d" - integrity sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA== - -path-exists@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" - integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s= - dependencies: - pinkie-promise "^2.0.0" - -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -path-key@^3.0.0, path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - -path-parse@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== - -path-type@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" - integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE= - dependencies: - graceful-fs "^4.1.2" - pify "^2.0.0" - pinkie-promise "^2.0.0" - -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - -pathval@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" - integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== - -pend@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" - integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= - -picocolors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" - integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== - -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: - version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -pify@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= - -pify@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" - integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== - -pinkie-promise@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" - integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= - dependencies: - pinkie "^2.0.0" - -pinkie@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" - integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= - -pirates@^4.0.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" - integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== - -pkg-dir@4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" - integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== - dependencies: - find-up "^4.0.0" - -pkg-dir@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" - integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== - dependencies: - find-up "^3.0.0" - -prelude-ls@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" - integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== - -prettier@^2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a" - integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg== - -pretty-format@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" - integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== - dependencies: - ansi-regex "^5.0.1" - ansi-styles "^5.0.0" - react-is "^17.0.1" - -pretty-ms@^7.0.0: - version "7.0.1" - resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-7.0.1.tgz#7d903eaab281f7d8e03c66f867e239dc32fb73e8" - integrity sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q== - dependencies: - parse-ms "^2.1.0" - -printj@~1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/printj/-/printj-1.3.1.tgz#9af6b1d55647a1587ac44f4c1654a4b95b8e12cb" - integrity sha512-GA3TdL8szPK4AQ2YnOe/b+Y1jUFwmmGMMK/qbY7VcE3Z7FU8JstbKiKRzO6CIiAKPhTO8m01NoQ0V5f3jc4OGg== - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -process@^0.11.10: - version "0.11.10" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" - integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= - -progress@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" - integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== - -proxy-from-env@1.1.0, proxy-from-env@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" - integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== - -pseudomap@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" - integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= - -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -punycode@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -puppeteer-core@^13.1.3: - version "13.5.1" - resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-13.5.1.tgz#74d902d8f4cbc003c4cb15c647bd070cc83539e7" - integrity sha512-dobVqWjV34ilyfQHR3BBnCYaekBYTi5MgegEYBRYd3s3uFy8jUpZEEWbaFjG9ETm+LGzR5Lmr0aF6LLuHtiuCg== - dependencies: - cross-fetch "3.1.5" - debug "4.3.3" - devtools-protocol "0.0.969999" - extract-zip "2.0.1" - https-proxy-agent "5.0.0" - pkg-dir "4.2.0" - progress "2.0.3" - proxy-from-env "1.1.0" - rimraf "3.0.2" - tar-fs "2.1.1" - unbzip2-stream "1.4.3" - ws "8.5.0" - -query-selector-shadow-dom@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.0.tgz#8fa7459a4620f094457640e74e953a9dbe61a38e" - integrity sha512-bK0/0cCI+R8ZmOF1QjT7HupDUYCxbf/9TJgAmSXQxZpftXmTAeil9DRoCnTDkWbvOyZzhcMBwKpptWcdkGFIMg== - -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== - -quick-lru@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" - integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== - -randombytes@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - -react-is@^17.0.1: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" - integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== - -read-pkg-up@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" - integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI= - dependencies: - find-up "^1.0.0" - read-pkg "^1.0.0" - -read-pkg@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" - integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg= - dependencies: - load-json-file "^1.0.0" - normalize-package-data "^2.3.2" - path-type "^1.0.0" - -readable-stream@^2.0.0, readable-stream@^2.0.5: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^3.0.0, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readdir-glob@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.1.tgz#f0e10bb7bf7bfa7e0add8baffdc54c3f7dbee6c4" - integrity sha512-91/k1EzZwDx6HbERR+zucygRFfiPl2zkIYZtv3Jjr6Mn7SkKcVct8aVO+sSRiGMc6fLf72du3d92/uY63YPdEA== - dependencies: - minimatch "^3.0.4" - -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - -recursive-readdir@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f" - integrity sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg== - dependencies: - minimatch "3.0.4" - -regenerate-unicode-properties@^10.0.1: - version "10.0.1" - resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz#7f442732aa7934a3740c779bb9b3340dccc1fb56" - integrity sha512-vn5DU6yg6h8hP/2OkQo3K7uVILvY4iu0oI4t3HFa81UPkhGJwkRwM10JEc3upjdhHjs/k8GJY1sRBhk5sr69Bw== - dependencies: - regenerate "^1.4.2" - -regenerate@^1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" - integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== - -regenerator-runtime@^0.13.4: - version "0.13.9" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" - integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== - -regenerator-transform@^0.14.2: - version "0.14.5" - resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.5.tgz#c98da154683671c9c4dcb16ece736517e1b7feb4" - integrity sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw== - dependencies: - "@babel/runtime" "^7.8.4" - -regexpp@^3.0.0, regexpp@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" - integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== - -regexpu-core@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.0.1.tgz#c531122a7840de743dcf9c83e923b5560323ced3" - integrity sha512-CriEZlrKK9VJw/xQGJpQM5rY88BtuL8DM+AEwvcThHilbxiTAy8vq4iJnd2tqq8wLmjbGZzP7ZcKFjbGkmEFrw== - dependencies: - regenerate "^1.4.2" - regenerate-unicode-properties "^10.0.1" - regjsgen "^0.6.0" - regjsparser "^0.8.2" - unicode-match-property-ecmascript "^2.0.0" - unicode-match-property-value-ecmascript "^2.0.0" - -regjsgen@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.6.0.tgz#83414c5354afd7d6627b16af5f10f41c4e71808d" - integrity sha512-ozE883Uigtqj3bx7OhL1KNbCzGyW2NQZPl6Hs09WTvCuZD5sTI4JY58bkbQWa/Y9hxIsvJ3M8Nbf7j54IqeZbA== - -regjsparser@^0.8.2: - version "0.8.4" - resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.8.4.tgz#8a14285ffcc5de78c5b95d62bbf413b6bc132d5f" - integrity sha512-J3LABycON/VNEu3abOviqGHuB/LOtOQj8SKmfP9anY5GfAVw/SPjwzSjxGjbZXIxbGfqTHtJw58C2Li/WkStmA== - dependencies: - jsesc "~0.5.0" - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= - -resolve-alpn@^1.0.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9" - integrity sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g== - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -resolve@^1.10.0, resolve@^1.10.1, resolve@^1.14.2, resolve@^1.20.0, resolve@^1.3.3: - version "1.22.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" - integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw== - dependencies: - is-core-module "^2.8.1" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -responselike@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.0.tgz#26391bcc3174f750f9a79eacc40a12a5c42d7723" - integrity sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw== - dependencies: - lowercase-keys "^2.0.0" - -resq@^1.9.1: - version "1.10.2" - resolved "https://registry.yarnpkg.com/resq/-/resq-1.10.2.tgz#cedf4f20d53f6e574b1e12afbda446ad9576c193" - integrity sha512-HmgVS3j+FLrEDBTDYysPdPVF9/hioDMJ/otOiQDKqk77YfZeeLOj0qi34yObumcud1gBpk+wpBTEg4kMicD++A== - dependencies: - fast-deep-equal "^2.0.1" - -restore-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" - integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== - dependencies: - onetime "^5.1.0" - signal-exit "^3.0.2" - -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - -rfdc@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" - integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== - -rgb2hex@0.2.5: - version "0.2.5" - resolved "https://registry.yarnpkg.com/rgb2hex/-/rgb2hex-0.2.5.tgz#f82230cd3ab1364fa73c99be3a691ed688f8dbdc" - integrity sha512-22MOP1Rh7sAo1BZpDG6R5RFYzR2lYEgwq7HEmyW2qcsOqR2lQKmn+O//xV3YG/0rrhMC6KVX2hU+ZXuaw9a5bw== - -rimraf@3.0.2, rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -run-async@^2.4.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" - integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== - -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - -rxjs@^7.2.0, rxjs@^7.5.5: - version "7.5.5" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.5.tgz#2ebad89af0f560f460ad5cc4213219e1f7dd4e9f" - integrity sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw== - dependencies: - tslib "^2.1.0" - -safe-buffer@^5.1.0, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -"semver@2 || 3 || 4 || 5", semver@^5.6.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" - integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== - -semver@^6.1.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -serialize-error@^8.0.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-8.1.0.tgz#3a069970c712f78634942ddd50fbbc0eaebe2f67" - integrity sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ== - dependencies: - type-fest "^0.20.2" - -serialize-javascript@6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" - integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== - dependencies: - randombytes "^2.1.0" - -shallow-clone@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" - integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== - dependencies: - kind-of "^6.0.2" - -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - -side-channel@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" - integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== - dependencies: - call-bind "^1.0.0" - get-intrinsic "^1.0.2" - object-inspect "^1.9.0" - -signal-exit@^3.0.2, signal-exit@^3.0.3: - version "3.0.7" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" - integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== - -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - -slice-ansi@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" - integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== - dependencies: - ansi-styles "^4.0.0" - astral-regex "^2.0.0" - is-fullwidth-code-point "^3.0.0" - -slice-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" - integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== - dependencies: - ansi-styles "^4.0.0" - astral-regex "^2.0.0" - is-fullwidth-code-point "^3.0.0" - -slice-ansi@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-5.0.0.tgz#b73063c57aa96f9cd881654b15294d95d285c42a" - integrity sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ== - dependencies: - ansi-styles "^6.0.0" - is-fullwidth-code-point "^4.0.0" - -source-map-support@^0.5.16: - version "0.5.21" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" - integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map@^0.5.0: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= - -source-map@^0.6.0: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -spdx-correct@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" - integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== - dependencies: - spdx-expression-parse "^3.0.0" - spdx-license-ids "^3.0.0" - -spdx-exceptions@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" - integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== - -spdx-expression-parse@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" - integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== - dependencies: - spdx-exceptions "^2.1.0" - spdx-license-ids "^3.0.0" - -spdx-license-ids@^3.0.0: - version "3.0.11" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz#50c0d8c40a14ec1bf449bae69a0ea4685a9d9f95" - integrity sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g== - -split2@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/split2/-/split2-3.2.2.tgz#bf2cf2a37d838312c249c89206fd7a17dd12365f" - integrity sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg== - dependencies: - readable-stream "^3.0.0" - -split2@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/split2/-/split2-4.1.0.tgz#101907a24370f85bb782f08adaabe4e281ecf809" - integrity sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ== - -stack-utils@^2.0.3: - version "2.0.5" - resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.5.tgz#d25265fca995154659dbbfba3b49254778d2fdd5" - integrity sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA== - dependencies: - escape-string-regexp "^2.0.0" - -stream-buffers@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-3.0.2.tgz#5249005a8d5c2d00b3a32e6e0a6ea209dc4f3521" - integrity sha512-DQi1h8VEBA/lURbSwFtEHnSTb9s2/pwLEaFuNhXwy1Dx3Sa0lOuYT2yNUr4/j2fs8oCAMANtrZ5OrPZtyVs3MQ== - -string-argv@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" - integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^5.0.0: - version "5.1.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" - integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== - dependencies: - eastasianwidth "^0.2.0" - emoji-regex "^9.2.2" - strip-ansi "^7.0.1" - -string.prototype.trimend@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" - integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - -string.prototype.trimstart@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" - integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -strip-ansi@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" - integrity sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw== - dependencies: - ansi-regex "^6.0.1" - -strip-bom@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" - integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= - dependencies: - is-utf8 "^0.2.0" - -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" - integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= - -strip-final-newline@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" - integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== - -strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" - integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== - -suffix@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/suffix/-/suffix-0.1.1.tgz#cc58231646a0ef1102f79478ef3a9248fd9c842f" - integrity sha1-zFgjFkag7xEC95R47zqSSP2chC8= - -supports-color@8.1.1: - version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= - -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -supports-color@^9.2.1: - version "9.2.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-9.2.1.tgz#599dc9d45acf74c6176e0d880bab1d7d718fe891" - integrity sha512-Obv7ycoCTG51N7y175StI9BlAXrmgZrFhZOb0/PyjHBher/NmsdBgbbQ1Inhq+gIhz6+7Gb+jWF2Vqi7Mf1xnQ== - -supports-preserve-symlinks-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" - integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== - -tar-fs@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" - integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== - dependencies: - chownr "^1.1.1" - mkdirp-classic "^0.5.2" - pump "^3.0.0" - tar-stream "^2.1.4" - -tar-stream@^2.1.4, tar-stream@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" - integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== - dependencies: - bl "^4.0.3" - end-of-stream "^1.4.1" - fs-constants "^1.0.0" - inherits "^2.0.3" - readable-stream "^3.1.1" - -tcp-port-used@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/tcp-port-used/-/tcp-port-used-1.0.2.tgz#9652b7436eb1f4cfae111c79b558a25769f6faea" - integrity sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA== - dependencies: - debug "4.3.1" - is2 "^2.0.6" - -text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= - -through@^2.3.6, through@^2.3.8: - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= - -tmp@^0.0.33: - version "0.0.33" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" - integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== - dependencies: - os-tmpdir "~1.0.2" - -to-fast-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" - integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= - -tsconfig-paths@^3.12.0: - version "3.13.0" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.13.0.tgz#f3e9b8f6876698581d94470c03c95b3a48c0e3d7" - integrity sha512-nWuffZppoaYK0vQ1SQmkSsQzJoHA4s6uzdb2waRpD806x9yfq153AdVsWz4je2qZcW+pENrMQXbGQ3sMCkXuhw== - dependencies: - "@types/json5" "^0.0.29" - json5 "^1.0.1" - minimist "^1.2.0" - strip-bom "^3.0.0" - -tslib@^2.1.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" - integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== - -type-check@^0.4.0, type-check@~0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" - integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== - dependencies: - prelude-ls "^1.2.1" - -type-detect@^4.0.0, type-detect@^4.0.5: - version "4.0.8" - resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" - integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== - -type-fest@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" - integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== - -type-fest@^0.21.3: - version "0.21.3" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" - integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== - -ua-parser-js@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.2.tgz#e2976c34dbfb30b15d2c300b2a53eac87c57a775" - integrity sha512-00y/AXhx0/SsnI51fTc0rLRmafiGOM4/O+ny10Ps7f+j/b8p/ZY11ytMgznXkOVo4GQ+KwQG5UQLkLGirsACRg== - -unbox-primitive@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" - integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw== - dependencies: - function-bind "^1.1.1" - has-bigints "^1.0.1" - has-symbols "^1.0.2" - which-boxed-primitive "^1.0.2" - -unbzip2-stream@1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" - integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== - dependencies: - buffer "^5.2.1" - through "^2.3.8" - -unicode-canonical-property-names-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" - integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== - -unicode-match-property-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" - integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== - dependencies: - unicode-canonical-property-names-ecmascript "^2.0.0" - unicode-property-aliases-ecmascript "^2.0.0" - -unicode-match-property-value-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz#1a01aa57247c14c568b89775a54938788189a714" - integrity sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw== - -unicode-property-aliases-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz#0a36cb9a585c4f6abd51ad1deddb285c165297c8" - integrity sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ== - -universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== - -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" - -util-deprecate@^1.0.1, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= - -uuid@^8.0.0, uuid@^8.3.2: - version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - -v8-compile-cache@^2.0.3: - version "2.3.0" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" - integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== - -validate-npm-package-license@^3.0.1: - version "3.0.4" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" - integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== - dependencies: - spdx-correct "^3.0.0" - spdx-expression-parse "^3.0.0" - -vscode-json-languageservice@^4.1.6: - version "4.2.0" - resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-4.2.0.tgz#df0693b69ba2fbf0a6add896087b6f1c9c38f06a" - integrity sha512-XNawv0Vdy/sUK0S+hGf7cq/qsVAbIniGJr89TvZOqMCNJmpgKTy1e8PL1aWW0uy6BfWMG7vxa5lZb3ypuFtuGQ== - dependencies: - jsonc-parser "^3.0.0" - vscode-languageserver-textdocument "^1.0.3" - vscode-languageserver-types "^3.16.0" - vscode-nls "^5.0.0" - vscode-uri "^3.0.3" - -vscode-languageserver-textdocument@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.4.tgz#3cd56dd14cec1d09e86c4bb04b09a246cb3df157" - integrity sha512-/xhqXP/2A2RSs+J8JNXpiiNVvvNM0oTosNVmQnunlKvq9o4mupHOBAnnzH0lwIPKazXKvAKsVp1kr+H/K4lgoQ== - -vscode-languageserver-types@^3.16.0: - version "3.16.0" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz#ecf393fc121ec6974b2da3efb3155644c514e247" - integrity sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA== - -vscode-nls@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.0.0.tgz#99f0da0bd9ea7cda44e565a74c54b1f2bc257840" - integrity sha512-u0Lw+IYlgbEJFF6/qAqG2d1jQmJl0eyAGJHoAJqr2HT4M2BNuQYSEiSE75f52pXHSJm8AlTjnLLbBFPrdz2hpA== - -vscode-uri@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.3.tgz#a95c1ce2e6f41b7549f86279d19f47951e4f4d84" - integrity sha512-EcswR2S8bpR7fD0YPeS7r2xXExrScVMxg4MedACaWHEtx9ftCF/qHG1xGkolzTPcEmjTavCQgbVzHUIdTMzFGA== - -wcwidth@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" - integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g= - dependencies: - defaults "^1.0.3" - -wdio-chromedriver-service@^7.2.8: - version "7.2.8" - resolved "https://registry.yarnpkg.com/wdio-chromedriver-service/-/wdio-chromedriver-service-7.2.8.tgz#18091be0c5f56cab3e6eb32613fe916b14dbd792" - integrity sha512-7JGmcTOBN15jvQPxTeC7t/xsQmlbttcDZStv36iUjbpuIXi7/Br+4pkttd+ES+w5eSJd2A5Ioe2ALRuTd3NeQQ== - dependencies: - "@wdio/logger" "^7.5.3" - fs-extra "^9.1.0" - split2 "^3.2.2" - tcp-port-used "^1.0.1" - -webdriver@7.17.3: - version "7.17.3" - resolved "https://registry.yarnpkg.com/webdriver/-/webdriver-7.17.3.tgz#145990fbb3451c07fbdcb496e7b72c5655cf0b9d" - integrity sha512-E1V/IKYjJoVjK9zhHfSCWeqORhgNlDuYydykm0h+CchEhMSgTmtTH/LYfXSx4myXzobdlIg6xhE7Jv7XPjSkAA== - dependencies: - "@types/node" "^17.0.4" - "@wdio/config" "7.17.3" - "@wdio/logger" "7.17.3" - "@wdio/protocols" "7.17.3" - "@wdio/types" "7.17.3" - "@wdio/utils" "7.17.3" - got "^11.0.2" - ky "^0.30.0" - lodash.merge "^4.6.1" - -webdriverio@7.17.4, webdriverio@^7.17.4: - version "7.17.4" - resolved "https://registry.yarnpkg.com/webdriverio/-/webdriverio-7.17.4.tgz#4261cf9631822d4d391bfb6953fb5a8e89f7e377" - integrity sha512-p7u2q7NJL7Et8FdSroq/Ltoi3KkKxERE79Srh9lFr6yRNPFqb46dJf/g4nljLhburnGkbNdYN15JWgyWYnnj9g== - dependencies: - "@types/aria-query" "^5.0.0" - "@types/node" "^17.0.4" - "@wdio/config" "7.17.3" - "@wdio/logger" "7.17.3" - "@wdio/protocols" "7.17.3" - "@wdio/repl" "7.17.3" - "@wdio/types" "7.17.3" - "@wdio/utils" "7.17.3" - archiver "^5.0.0" - aria-query "^5.0.0" - css-shorthand-properties "^1.1.1" - css-value "^0.0.1" - devtools "7.17.3" - devtools-protocol "^0.0.979353" - fs-extra "^10.0.0" - get-port "^5.1.1" - grapheme-splitter "^1.0.2" - lodash.clonedeep "^4.5.0" - lodash.isobject "^3.0.2" - lodash.isplainobject "^4.0.6" - lodash.zip "^4.2.0" - minimatch "^5.0.0" - puppeteer-core "^13.1.3" - query-selector-shadow-dom "^1.0.0" - resq "^1.9.1" - rgb2hex "0.2.5" - serialize-error "^8.0.0" - webdriver "7.17.3" - -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - -which-boxed-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" - integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== - dependencies: - is-bigint "^1.0.1" - is-boolean-object "^1.1.0" - is-number-object "^1.0.4" - is-string "^1.0.5" - is-symbol "^1.0.3" - -which@2.0.2, which@^2.0.1, which@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -which@^1.2.9: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -word-wrap@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== - -workerpool@6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.0.tgz#827d93c9ba23ee2019c3ffaff5c27fccea289e8b" - integrity sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A== - -wrap-ansi@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" - integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -ws@8.5.0: - version "8.5.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f" - integrity sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg== - -ws@^7.4.3: - version "7.5.7" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.7.tgz#9e0ac77ee50af70d58326ecff7e85eb3fa375e67" - integrity sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A== - -y18n@^5.0.5: - version "5.0.8" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" - integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== - -yallist@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" - integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= - -yaml@^1.10.2: - version "1.10.2" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" - integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== - -yargs-parser@20.2.4: - version "20.2.4" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" - integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== - -yargs-parser@^20.2.2: - version "20.2.9" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" - integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== - -yargs-parser@^21.0.0: - version "21.0.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.1.tgz#0267f286c877a4f0f728fceb6f8a3e4cb95c6e35" - integrity sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg== - -yargs-unparser@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" - integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== - dependencies: - camelcase "^6.0.0" - decamelize "^4.0.0" - flat "^5.0.2" - is-plain-obj "^2.1.0" - -yargs@16.2.0: - version "16.2.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" - integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.0" - y18n "^5.0.5" - yargs-parser "^20.2.2" - -yargs@^17.0.0: - version "17.3.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.3.1.tgz#da56b28f32e2fd45aefb402ed9c26f42be4c07b9" - integrity sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.3" - y18n "^5.0.5" - yargs-parser "^21.0.0" - -yarn-install@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/yarn-install/-/yarn-install-1.0.0.tgz#57f45050b82efd57182b3973c54aa05cb5d25230" - integrity sha1-V/RQULgu/VcYKzlzxUqgXLXSUjA= - dependencies: - cac "^3.0.3" - chalk "^1.1.3" - cross-spawn "^4.0.2" - -yauzl@^2.10.0: - version "2.10.0" - resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" - integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= - dependencies: - buffer-crc32 "~0.2.3" - fd-slicer "~1.1.0" - -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== - -zip-stream@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-4.1.0.tgz#51dd326571544e36aa3f756430b313576dc8fc79" - integrity sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A== - dependencies: - archiver-utils "^2.1.0" - compress-commons "^4.1.0" - readable-stream "^3.6.0"