From a1250fa6d2fcba1ff48298e90786238c5062dd00 Mon Sep 17 00:00:00 2001 From: Mikolaj Miotk Date: Wed, 18 Feb 2026 12:37:38 +0100 Subject: [PATCH 01/32] HOTE-541: Add pipeline for executing playwright tests --- .github/actions/init-mise/action.yaml | 37 + .../actions/perform-pre-commit/action.yaml | 17 - .github/actions/run-npm-tests/action.yaml | 17 +- .github/workflows/playwright-e2e.yaml | 168 ++ .github/workflows/stage-1-commit.yaml | 2 + .github/workflows/stage-2-test.yaml | 82 + .pre-commit-config.yaml | 42 + local-environment/infra/main.tf | 31 + local-environment/infra/outputs.tf | 10 + package-lock.json | 189 +- package.json | 10 +- scripts/terraform/post-apply-env-update.sh | 15 +- tests/.gitignore | 1 - tests/api/endpoints.ts | 6 + tests/configuration/index.ts | 2 +- tests/eslint.config.mjs | 32 + tests/fixtures/consoleErrorFixture.ts | 159 ++ tests/fixtures/index.ts | 4 +- tests/package-lock.json | 1638 +++++++++++++++++ tests/package.json | 19 +- tests/page-objects/NhsLoginHelper.ts | 8 + tests/playwright.config.ts | 4 + tests/tsconfig.json | 4 +- tests/utils/AccessibilityModule.ts | 2 +- tests/utils/users/BaseUserManager.ts | 131 +- 25 files changed, 2374 insertions(+), 256 deletions(-) create mode 100644 .github/actions/init-mise/action.yaml create mode 100644 .github/workflows/playwright-e2e.yaml create mode 100644 tests/eslint.config.mjs create mode 100644 tests/fixtures/consoleErrorFixture.ts create mode 100644 tests/package-lock.json diff --git a/.github/actions/init-mise/action.yaml b/.github/actions/init-mise/action.yaml new file mode 100644 index 00000000..ec57f127 --- /dev/null +++ b/.github/actions/init-mise/action.yaml @@ -0,0 +1,37 @@ +--- +name: "Initialize mise" +description: "Initialize mise" +runs: + using: "composite" + steps: + - name: Configure git + shell: bash + run: | + echo "::group::Configure git" + git config --global --add safe.directory '*' + echo "::endgroup::" + + - name: Install mise + uses: jdx/mise-action@v3 + with: + cache: true + install: true + + - name: Cache node_modules + uses: actions/cache@v5 + with: + path: | + node_modules + ui/node_modules + lambdas/node_modules + tests/node_modules + key: node-modules-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + node-modules-${{ runner.os }}- + + - name: Install npm dependencies + shell: bash + run: | + echo "::group::Install npm dependencies" + mise run install-npm + echo "::endgroup::" diff --git a/.github/actions/perform-pre-commit/action.yaml b/.github/actions/perform-pre-commit/action.yaml index 034ca333..c2a2c142 100644 --- a/.github/actions/perform-pre-commit/action.yaml +++ b/.github/actions/perform-pre-commit/action.yaml @@ -4,19 +4,6 @@ description: "Perform pre-commit checks" runs: using: "composite" steps: - - name: Configure git - shell: bash - run: | - echo "::group::Configure git" - git config --global --add safe.directory '*' - echo "::endgroup::" - - - name: Install mise - uses: jdx/mise-action@v3 - with: - cache: true - install: true - - name: Cache pre-commit hooks uses: actions/cache@v5 with: @@ -32,10 +19,6 @@ runs: mise exec -- pre-commit install --install-hooks echo "::endgroup::" - echo "::group::Install npm dependencies" - mise run install-npm - echo "::endgroup::" - - name: Run pre-commit shell: bash run: | diff --git a/.github/actions/run-npm-tests/action.yaml b/.github/actions/run-npm-tests/action.yaml index 7cb58f4b..2afd343f 100644 --- a/.github/actions/run-npm-tests/action.yaml +++ b/.github/actions/run-npm-tests/action.yaml @@ -25,20 +25,11 @@ inputs: runs: using: "composite" steps: - - name: "Setup Node.js" - uses: actions/setup-node@v6 - with: - node-version: "${{ inputs.nodejs-version }}" - cache: npm - cache-dependency-path: ${{ inputs.working-directory }}/package-lock.json - - - name: "Install dependencies" - shell: bash - run: npm --prefix ${{ inputs.working-directory }} ci - - name: "Run test suite" shell: bash - run: npm --prefix ${{ inputs.working-directory }} run test -- --ci + run: mise exec -- npm --prefix ${{ inputs.working-directory }} run test -- --ci + env: + FORCE_COLOR: true - name: "Upload test results" uses: actions/upload-artifact@v4 @@ -57,7 +48,7 @@ runs: retention-days: 30 - name: "Test Report" - uses: dorny/test-reporter@v1 + uses: dorny/test-reporter@v2 if: always() with: name: "${{ inputs.test-name }}" diff --git a/.github/workflows/playwright-e2e.yaml b/.github/workflows/playwright-e2e.yaml new file mode 100644 index 00000000..9dec2c87 --- /dev/null +++ b/.github/workflows/playwright-e2e.yaml @@ -0,0 +1,168 @@ +--- +name: "Playwright E2E Tests" + +on: + schedule: + - cron: '0 2 * * *' # Every day at 2am UTC + workflow_dispatch: + inputs: + browser: + description: "Browser to run tests on" + required: false + default: "chromium" + type: choice + options: + - chromium + - firefox + - webkit + - all + test_filter: + description: "Test filter pattern (e.g., 'HomeTest' or leave empty for all)" + required: false + default: "" + type: string + +jobs: + test-playwright: + name: "Playwright E2E tests" + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: "Checkout code" + uses: actions/checkout@v6 + + - name: "Install mise" + uses: jdx/mise-action@v3 + with: + install: true + cache: true + + - name: "Install Playwright browsers" + working-directory: tests + run: npx playwright install --with-deps + + - name: "Create nhs login private key file" + run: | + mkdir -p local-environment/infra/resources/secrets + cat > local-environment/infra/resources/secrets/nhs-login-private-key.pem << 'EOF' + ${{ secrets.NHS_LOGIN_PRIVATE_KEY }} + EOF + + - name: "Create credentials file" + working-directory: tests + run: | + cat > credentials.ts << 'EOF' + export const userPasswordGeneric = '${{ secrets.PLAYWRIGHT_PASSWORD }}'; + export const OTP = '${{ secrets.PLAYWRIGHT_OTP }}'; + EOF + + - name: "Start the application" + run: | + npm run start + + - name: "Show application status" + run: | + docker compose -f local-environment/docker-compose.yml ps + docker logs ui + + - name: "Get terraform outputs" + id: terraform + run: | + UI_URL=$(terraform -chdir=local-environment/infra output -raw ui_url) + API_URL=$(terraform -chdir=local-environment/infra output -raw api_base_url) + echo "ui_url=$UI_URL" >> $GITHUB_OUTPUT + echo "api_base_url=$API_URL" >> $GITHUB_OUTPUT + echo "UI URL: $UI_URL" + echo "API URL: $API_URL" + + - name: "Wait for UI to be reachable" + run: | + UI_URL="${{ steps.terraform.outputs.ui_url }}" + echo "Waiting for UI to be reachable at $UI_URL..." + timeout=120 + elapsed=0 + until curl -sf "$UI_URL" > /dev/null 2>&1; do + if [ $elapsed -ge $timeout ]; then + echo "Timeout: UI not reachable after ${timeout}s" + docker logs ui + exit 1 + fi + echo "Waiting... (${elapsed}s)" + sleep 5 + elapsed=$((elapsed + 5)) + done + echo "UI is reachable!" + + - name: "Run Playwright tests" + working-directory: tests + run: | + BROWSER="${{ inputs.browser }}" + BROWSER="${BROWSER:-chromium}" + FILTER="${{ inputs.test_filter }}" + + # Build the command + CMD="npx playwright test" + + # Add browser project + if [ "$BROWSER" != "all" ]; then + CMD="$CMD --project=$BROWSER" + fi + + # Add filter if provided + if [ -n "$FILTER" ]; then + CMD="$CMD --grep \"$FILTER\"" + fi + + echo "Running: $CMD" + eval $CMD + env: + CI: true + FORCE_COLOR: true + UI_BASE_URL: ${{ steps.terraform.outputs.ui_url }} + API_BASE_URL: ${{ steps.terraform.outputs.api_base_url }} + + - name: "Grab docker compose logs" + run: | + for service in $(docker compose -f local-environment/docker-compose.yml ps --services); do + docker compose -f local-environment/docker-compose.yml logs "$service" > "tests/testResults/docker-compose-${service}.log" 2>&1 + done + if: always() + + - name: "Publish Test Results" + uses: dorny/test-reporter@v2 + if: always() + with: + name: Playwright Test Results + path: tests/testResults/junit-results.xml + reporter: java-junit + fail-on-error: false + + - name: "Generate Job Summary" + if: always() + run: | + echo "## Playwright Test Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [ -f tests/testResults/junit-results.xml ]; then + TESTS=$(grep -oP 'tests="\K[0-9]+' tests/testResults/junit-results.xml | head -1) + FAILURES=$(grep -oP 'failures="\K[0-9]+' tests/testResults/junit-results.xml | head -1) + ERRORS=$(grep -oP 'errors="\K[0-9]+' tests/testResults/junit-results.xml | head -1) + TIME=$(grep -oP 'time="\K[0-9.]+' tests/testResults/junit-results.xml | head -1) + PASSED=$((TESTS - FAILURES - ERRORS)) + echo "| Metric | Value |" >> $GITHUB_STEP_SUMMARY + echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Total Tests | $TESTS |" >> $GITHUB_STEP_SUMMARY + echo "| :white_check_mark: Passed | $PASSED |" >> $GITHUB_STEP_SUMMARY + echo "| :x: Failed | $FAILURES |" >> $GITHUB_STEP_SUMMARY + echo "| :warning: Errors | $ERRORS |" >> $GITHUB_STEP_SUMMARY + echo "| :stopwatch: Duration | ${TIME}s |" >> $GITHUB_STEP_SUMMARY + else + echo ":warning: No test results found" >> $GITHUB_STEP_SUMMARY + fi + + - name: "Upload test results" + uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: tests/testResults/ + retention-days: 30 diff --git a/.github/workflows/stage-1-commit.yaml b/.github/workflows/stage-1-commit.yaml index 9b1fc0fe..5acfa953 100644 --- a/.github/workflows/stage-1-commit.yaml +++ b/.github/workflows/stage-1-commit.yaml @@ -99,5 +99,7 @@ jobs: steps: - name: Checkout uses: actions/checkout@v6 + - name: Initialize mise + uses: ./.github/actions/init-mise - name: Run pre-commit checks uses: ./.github/actions/perform-pre-commit diff --git a/.github/workflows/stage-2-test.yaml b/.github/workflows/stage-2-test.yaml index ab87bf18..d3d00300 100644 --- a/.github/workflows/stage-2-test.yaml +++ b/.github/workflows/stage-2-test.yaml @@ -41,6 +41,8 @@ jobs: steps: - name: "Checkout code" uses: actions/checkout@v6 + - name: Initialize mise + uses: ./.github/actions/init-mise - name: "Run UI tests" uses: ./.github/actions/run-npm-tests with: @@ -58,6 +60,8 @@ jobs: steps: - name: "Checkout code" uses: actions/checkout@v6 + - name: Initialize mise + uses: ./.github/actions/init-mise - name: "Run Lambda tests" uses: ./.github/actions/run-npm-tests with: @@ -78,6 +82,84 @@ jobs: - name: "Save the coverage check result" run: | echo "Nothing to save" + + # test-playwright: + # name: "Playwright E2E tests" + # needs: [test-unit-ui, test-unit-lambda] + # runs-on: ubuntu-latest + # timeout-minutes: 30 + # steps: + # - name: "Checkout code" + # uses: actions/checkout@v6 + # - name: "Install mise" + # uses: jdx/mise-action@v3 + # with: + # install: true + # cache: true + # - name: "Install Playwright browsers" + # working-directory: tests + # run: npx playwright install --with-deps + # - name: "Create nhs login private key file" + # run: | + # mkdir -p local-environment/infra/resources/secrets + # cat > local-environment/infra/resources/secrets/nhs-login-private-key.pem << 'EOF' + # ${{ secrets.NHS_LOGIN_PRIVATE_KEY }} + # EOF + # - name: "Create credentials file" + # working-directory: tests + # run: | + # cat > credentials.ts << 'EOF' + # export const userPasswordGeneric = '${{ secrets.PLAYWRIGHT_PASSWORD }}'; + # export const OTP = '${{ secrets.PLAYWRIGHT_OTP }}'; + # EOF + # - name: "Start the application" + # run: | + # npm run start + + # - name: "Show application status" + # run: | + # docker compose -f local-environment/docker-compose.yml ps + # docker logs ui + + # - name: "Wait for UI to be reachable" + # run: | + # echo "Waiting for UI to be reachable at http://localhost:3000..." + # timeout=120 + # elapsed=0 + # until curl -sf http://localhost:3000 > /dev/null 2>&1; do + # if [ $elapsed -ge $timeout ]; then + # echo "Timeout: UI not reachable after ${timeout}s" + # docker logs ui + # exit 1 + # fi + # echo "Waiting... (${elapsed}s)" + # sleep 5 + # elapsed=$((elapsed + 5)) + # done + # echo "UI is reachable!" + + # - name: "Run Playwright tests" + # # working-directory: tests + # run: npm run test:playwright + # env: + # CI: true + # # BASE_URL: ${{ vars.TEST_BASE_URL }} + # FORCE_COLOR: true + # - name: "Grab docker compose logs" + # run: | + # for service in $(docker compose -f local-environment/docker-compose.yml ps --services); do + # docker compose -f local-environment/docker-compose.yml logs "$service" > "tests/testResults/docker-compose-${service}.log" 2>&1 + # done + # if: always() + + # - name: "Upload test results" + # uses: actions/upload-artifact@v4 + # if: always() + # with: + # name: playwright-report + # path: tests/testResults/ + # retention-days: 30 + perform-static-analysis: name: "Perform static analysis" needs: [test-unit-ui, test-unit-lambda] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ebbfea23..446591d8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -70,3 +70,45 @@ repos: language: system files: ^lambdas/.*\.(ts|js)$ pass_filenames: false + + - id: eslint-tests + name: ESLint (Tests) + entry: npm --prefix tests run lint -- --fix + language: system + files: ^tests/.*\.(ts|js)$ + pass_filenames: false + + # - id: typescript-check-ui + # name: TypeScript (UI) + # entry: npm --prefix ui run build -- --noEmit + # language: system + # files: ^ui/.*\.tsx?$ + # pass_filenames: false + # # stages: [pre-push] # Slow, run on push + + # - id: typescript-check-lambdas + # name: TypeScript (Lambdas) + # entry: bash -c 'cd lambdas && npx tsc --noEmit' + # language: system + # files: ^lambdas/.*\.ts$ + # pass_filenames: false + # # stages: [pre-push] + + # - id: jest-unit-ui + # name: Jest Unit Tests (UI - affected only) + # entry: npm --prefix ui run test -- --bail --findRelatedTests --passWithNoTests + # language: system + # files: ^ui/.*\.(ts|tsx)$ + # # stages: [pre-push] + + # - id: jest-unit-lambdas + # name: Jest Unit Tests (Lambdas - affected only) + # entry: bash -c 'cd lambdas && npx jest --bail --findRelatedTests --passWithNoTests' + # language: system + # files: ^lambdas/.*\.ts$ + # # stages: [pre-push] + + # - repo: https://github.com/rhysd/actionlint + # rev: v1.7.11 + # hooks: + # - id: actionlint diff --git a/local-environment/infra/main.tf b/local-environment/infra/main.tf index 6ffe8329..b27c9463 100644 --- a/local-environment/infra/main.tf +++ b/local-environment/infra/main.tf @@ -4,6 +4,10 @@ terraform { source = "hashicorp/aws" version = "~> 6.0" } + null = { + source = "hashicorp/null" + version = "~> 3.0" + } } } @@ -32,6 +36,17 @@ locals { secret_txt_files = fileset(local.secrets_dir, "*.txt") secret_key_files = fileset(local.secrets_dir, "*.pem") + # Required secrets that must exist + required_secrets = [ + "nhs-login-private-key.pem" + ] + + # Validate required secrets exist + missing_secrets = [ + for secret in local.required_secrets : + secret if !fileexists("${local.secrets_dir}/${secret}") + ] + secret_json_map = { for f in local.secret_json_files : trimsuffix(f, ".json") => f @@ -48,6 +63,22 @@ locals { secret_file_map = merge(local.secret_json_map, local.secret_txt_map, local.secret_key_map) } +# Fail early if required secrets are missing +resource "null_resource" "validate_secrets" { + lifecycle { + precondition { + condition = length(local.missing_secrets) == 0 + error_message = <<-EOT + Missing required secret files in ${local.secrets_dir}: + ${join("\n - ", local.missing_secrets)} + + To generate the NHS Login private key, run: + openssl genrsa -out ${local.secrets_dir}/nhs-login-private-key.pem 2048 + EOT + } + } +} + resource "aws_secretsmanager_secret" "secrets" { for_each = local.secret_file_map name = each.key diff --git a/local-environment/infra/outputs.tf b/local-environment/infra/outputs.tf index 77fc196c..55df90a4 100644 --- a/local-environment/infra/outputs.tf +++ b/local-environment/infra/outputs.tf @@ -3,6 +3,11 @@ output "api_gateway_url" { value = "https://${aws_api_gateway_rest_api.api.id}.execute-api.${var.aws_region}.amazonaws.com/${var.environment}" } +output "api_base_url" { + description = "LocalStack API base URL for tests" + value = "http://localhost:4566/restapis/${aws_api_gateway_rest_api.api.id}/${var.environment}/_user_request_" +} + output "api_gateway_id" { description = "API Gateway ID" value = aws_api_gateway_rest_api.api.id @@ -42,3 +47,8 @@ output "order_placement_queue_url" { description = "SQS Queue URL for order placement" value = aws_sqs_queue.order_placement.url } + +output "ui_url" { + description = "URL of the UI application" + value = "http://localhost:3000" +} diff --git a/package-lock.json b/package-lock.json index 8ba012e5..7d0b3809 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,189 +8,21 @@ "name": "nhs-home-test-service", "version": "0.0.1", "hasInstallScript": true, - "dependencies": { - "jsonwebtoken": "^9.0.3", - "source-map-support": "^0.5.21", - "uuid": "^13.0.0" - }, "devDependencies": { - "@types/node": "^25.2.1", + "@types/node": "^25.2.3", "typescript": "^5.9.3" } }, "node_modules/@types/node": { - "version": "25.2.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.1.tgz", - "integrity": "sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg==", + "version": "25.2.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", + "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" } }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" - }, - "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==", - "license": "MIT" - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jsonwebtoken": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", - "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", - "license": "MIT", - "dependencies": { - "jws": "^4.0.1", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - } - }, - "node_modules/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", - "license": "MIT", - "dependencies": { - "jwa": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", - "license": "MIT" - }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", - "license": "MIT" - }, - "node_modules/lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", - "license": "MIT" - }, - "node_modules/lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", - "license": "MIT" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "license": "MIT" - }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", - "license": "MIT" - }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "license": "MIT" - }, - "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==", - "license": "MIT" - }, - "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==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "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==", - "license": "BSD-3-Clause", - "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==", - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -211,19 +43,6 @@ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, "license": "MIT" - }, - "node_modules/uuid": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", - "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist-node/bin/uuid" - } } } } diff --git a/package.json b/package.json index fb048168..3bf04ca6 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,10 @@ "description": "NHS Home Test Service", "private": true, "scripts": { - "postinstall": "npm --prefix ui install && npm --prefix lambdas install", + "postinstall": "npm --prefix ui install && npm --prefix lambdas install && npm --prefix tests install", "test": "npm --prefix ui run test && npm --prefix lambdas run test", + "test:playwright": "UI_BASE_URL=$(terraform -chdir=local-environment/infra output -raw ui_url) API_BASE_URL=$(terraform -chdir=local-environment/infra output -raw api_base_url) npm --prefix tests run test:chrome", "build:lambdas": "npm --prefix lambdas run build", "package:lambdas": "npm --prefix lambdas run package", @@ -44,12 +45,7 @@ "local:compose:down": "npm run local:compose -- down" }, "devDependencies": { - "@types/node": "^25.2.1", + "@types/node": "^25.2.3", "typescript": "^5.9.3" - }, - "dependencies": { - "jsonwebtoken": "^9.0.3", - "source-map-support": "^0.5.21", - "uuid": "^13.0.0" } } diff --git a/scripts/terraform/post-apply-env-update.sh b/scripts/terraform/post-apply-env-update.sh index 8a71f9e6..eb607e6c 100644 --- a/scripts/terraform/post-apply-env-update.sh +++ b/scripts/terraform/post-apply-env-update.sh @@ -3,20 +3,7 @@ set -euo pipefail LOGIN_ENDPOINT=$(terraform -chdir=local-environment/infra output -raw login_endpoint) -ORDER_RESULT_ENDPOINT=$(terraform -chdir=local-environment/infra output -raw order_result_endpoint) -API_BASE_URL="${ORDER_RESULT_ENDPOINT%/result}" printf 'NEXT_PUBLIC_LOGIN_LAMBDA_ENDPOINT=%s\n' "$LOGIN_ENDPOINT" > ./ui/.env.local -TESTS_ENV_FILE=./tests/configuration/.env.local -mkdir -p "$(dirname "$TESTS_ENV_FILE")" -touch "$TESTS_ENV_FILE" - -if grep -q '^API_BASE_URL=' "$TESTS_ENV_FILE"; then - sed -E "s|^API_BASE_URL=.*$|API_BASE_URL=$API_BASE_URL|" "$TESTS_ENV_FILE" > "${TESTS_ENV_FILE}.tmp" - mv "${TESTS_ENV_FILE}.tmp" "$TESTS_ENV_FILE" -else - printf '\nAPI_BASE_URL=%s\n' "$API_BASE_URL" >> "$TESTS_ENV_FILE" -fi - -echo "Updated ui and tests local env files from terraform outputs" +echo "Updated ui local env file from terraform outputs" diff --git a/tests/.gitignore b/tests/.gitignore index b48f6a11..4b0ee1e1 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -4,6 +4,5 @@ node_modules/ /playwright-report/ /blob-report/ /playwright/.cache/ -package-lock.json WorkerUser* credentials.ts diff --git a/tests/api/endpoints.ts b/tests/api/endpoints.ts index ac9c53b3..61a8e216 100644 --- a/tests/api/endpoints.ts +++ b/tests/api/endpoints.ts @@ -2,4 +2,10 @@ export const API_ENDPOINTS = { results: { base: '/result', }, + users: { + base: '/users', + list: '/users', + createUser: '/users', + getUser: (id: number) => `/users/${id}`, + }, } as const; diff --git a/tests/configuration/index.ts b/tests/configuration/index.ts index 9c31f3fd..3bfb69c4 100644 --- a/tests/configuration/index.ts +++ b/tests/configuration/index.ts @@ -1,2 +1,2 @@ -export { Configuration, config } from './configuration'; +export { ConfigFactory, ConfigInterface, AuthType, config } from './configuration'; export { EnvironmentVariables, availableEnvironments, Environment } from './environment-variables'; diff --git a/tests/eslint.config.mjs b/tests/eslint.config.mjs new file mode 100644 index 00000000..e86ca912 --- /dev/null +++ b/tests/eslint.config.mjs @@ -0,0 +1,32 @@ +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; +import globals from "globals"; + +export default tseslint.config( + eslint.configs.recommended, + ...tseslint.configs.recommended, + { + languageOptions: { + globals: { + ...globals.node, + }, + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + "@typescript-eslint/no-unused-vars": [ + "warn", + { argsIgnorePattern: "^_", varsIgnorePattern: "^_", caughtErrorsIgnorePattern: "^_|^error$" }, + ], + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-require-imports": "warn", + "no-useless-catch": "warn", + "no-empty-pattern": "off", // Common in Playwright fixtures + }, + }, + { + ignores: ["testResults/**", "node_modules/**", "*.config.*"], + } +); diff --git a/tests/fixtures/consoleErrorFixture.ts b/tests/fixtures/consoleErrorFixture.ts new file mode 100644 index 00000000..6655fefa --- /dev/null +++ b/tests/fixtures/consoleErrorFixture.ts @@ -0,0 +1,159 @@ +import { test as base, TestInfo, expect } from '@playwright/test'; + +interface ConsoleError { + type: string; + text: string; + location?: string; + timestamp: string; +} + +interface NetworkError { + url: string; + status: number; + statusText: string; + method: string; + timestamp: string; +} + +interface ErrorCaptureOptions { + /** Fail test on console errors (default: true) */ + failOnConsoleError?: boolean; + /** Fail test on HTTP 4xx/5xx responses (default: true) */ + failOnNetworkError?: boolean; + /** Ignore errors matching these patterns */ + ignorePatterns?: (string | RegExp)[]; + /** HTTP status codes to ignore (default: []) */ + ignoreStatusCodes?: number[]; +} + +interface ConsoleErrorFixture { + consoleErrors: ConsoleError[]; + networkErrors: NetworkError[]; + errorCaptureOptions: ErrorCaptureOptions; +} + +const defaultOptions: ErrorCaptureOptions = { + failOnConsoleError: true, + failOnNetworkError: true, + ignorePatterns: [ + // Network transient errors + /net::ERR_NETWORK_CHANGED/, + /net::ERR_CONNECTION_RESET/, + /net::ERR_INTERNET_DISCONNECTED/, + // External NHS resources not available in test environment + /NHSCookieConsent is not defined/, + /nhsapp is not defined/, + /"undefined" is not valid JSON/, + ], + ignoreStatusCodes: [], +}; + +function shouldIgnoreError(text: string, patterns: (string | RegExp)[]): boolean { + return patterns.some(pattern => { + if (typeof pattern === 'string') { + return text.includes(pattern); + } + return pattern.test(text); + }); +} + +export const consoleErrorFixture = base.extend({ + consoleErrors: [[], { option: true }], + networkErrors: [[], { option: true }], + errorCaptureOptions: [defaultOptions, { option: true }], + + page: async ({ page, consoleErrors, networkErrors, errorCaptureOptions }, use, testInfo: TestInfo) => { + const options = { ...defaultOptions, ...errorCaptureOptions }; + const errors: ConsoleError[] = []; + const netErrors: NetworkError[] = []; + + // Capture console errors and warnings + page.on('console', (msg) => { + if (msg.type() === 'error' || msg.type() === 'warning') { + const text = msg.text(); + if (!shouldIgnoreError(text, options.ignorePatterns || [])) { + errors.push({ + type: msg.type(), + text, + location: msg.location()?.url, + timestamp: new Date().toISOString(), + }); + } + } + }); + + // Capture uncaught exceptions + page.on('pageerror', (error) => { + if (!shouldIgnoreError(error.message, options.ignorePatterns || [])) { + errors.push({ + type: 'pageerror', + text: error.message, + location: error.stack, + timestamp: new Date().toISOString(), + }); + } + }); + + // Capture failed network requests (4xx, 5xx) + page.on('response', (response) => { + const status = response.status(); + if (status >= 400 && !options.ignoreStatusCodes?.includes(status)) { + const url = response.url(); + if (!shouldIgnoreError(url, options.ignorePatterns || [])) { + netErrors.push({ + url, + status, + statusText: response.statusText(), + method: response.request().method(), + timestamp: new Date().toISOString(), + }); + } + } + }); + + await use(page); + + // After test, process captured errors + const hasConsoleErrors = errors.length > 0; + const hasNetworkErrors = netErrors.length > 0; + + // Add errors to fixtures for inspection + consoleErrors.push(...errors); + networkErrors.push(...netErrors); + + // Attach errors to test report + if (hasConsoleErrors) { + await testInfo.attach('console-errors', { + body: JSON.stringify(errors, null, 2), + contentType: 'application/json', + }); + } + + if (hasNetworkErrors) { + await testInfo.attach('network-errors', { + body: JSON.stringify(netErrors, null, 2), + contentType: 'application/json', + }); + } + + // Build failure message if errors detected and should fail + const failureMessages: string[] = []; + + if (hasConsoleErrors && options.failOnConsoleError) { + failureMessages.push( + `Console errors detected:\n${errors.map((e, i) => ` ${i + 1}. [${e.type}] ${e.text}`).join('\n')}` + ); + } + + if (hasNetworkErrors && options.failOnNetworkError) { + failureMessages.push( + `Network errors detected:\n${netErrors.map((e, i) => ` ${i + 1}. ${e.method} ${e.url} => ${e.status} ${e.statusText}`).join('\n')}` + ); + } + + // Fail the test if errors were captured + if (failureMessages.length > 0) { + expect.soft(false, failureMessages.join('\n\n')).toBeTruthy(); + } + }, +}); diff --git a/tests/fixtures/index.ts b/tests/fixtures/index.ts index c23fa5eb..6a0495b8 100644 --- a/tests/fixtures/index.ts +++ b/tests/fixtures/index.ts @@ -4,11 +4,13 @@ import { configurationFixture } from './configurationFixture'; import { apiFixture } from './apiFixture'; import { accessibilityFixture } from './accessibilityFixture'; import { storageStateFixture } from './storageStateFixture'; +import { consoleErrorFixture } from './consoleErrorFixture'; export const test = mergeTests( pageObjectFixture, configurationFixture, apiFixture, accessibilityFixture, - storageStateFixture); + storageStateFixture, + consoleErrorFixture); export { expect } from '@playwright/test'; diff --git a/tests/package-lock.json b/tests/package-lock.json new file mode 100644 index 00000000..41538814 --- /dev/null +++ b/tests/package-lock.json @@ -0,0 +1,1638 @@ +{ + "name": "hometest-service-tests", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "hometest-service-tests", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@axe-core/playwright": "^4.11.1", + "axe-html-reporter": "^2.2.11", + "uuid": "^13.0.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.2", + "@playwright/test": "^1.58.2", + "@types/node": "^25.2.3", + "dotenv": "^17.2.3", + "eslint": "^9.39.2", + "globals": "^17.3.0", + "typescript": "^5.9.3", + "typescript-eslint": "^8.56.0" + } + }, + "node_modules/@axe-core/playwright": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.11.1.tgz", + "integrity": "sha512-mKEfoUIB1MkVTht0BGZFXtSAEKXMJoDkyV5YZ9jbBmZCcWDz71tegNsdTkIN8zc/yMi5Gm2kx7Z5YQ9PfWNAWw==", + "license": "MPL-2.0", + "dependencies": { + "axe-core": "~4.11.1" + }, + "peerDependencies": { + "playwright-core": ">= 1.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/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, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.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, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.2.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", + "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz", + "integrity": "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/type-utils": "8.56.0", + "@typescript-eslint/utils": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.56.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz", + "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz", + "integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.56.0", + "@typescript-eslint/types": "^8.56.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz", + "integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz", + "integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz", + "integrity": "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/utils": "8.56.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", + "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz", + "integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.56.0", + "@typescript-eslint/tsconfig-utils": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/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/@typescript-eslint/utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz", + "integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz", + "integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz", + "integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": 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, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "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, + "license": "MIT", + "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-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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, + "license": "Python-2.0" + }, + "node_modules/axe-core": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", + "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==", + "license": "MPL-2.0", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/axe-html-reporter": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/axe-html-reporter/-/axe-html-reporter-2.2.11.tgz", + "integrity": "sha512-WlF+xlNVgNVWiM6IdVrsh+N0Cw7qupe5HT9N6Uyi+aN7f6SSi92RDomiP1noW8OWIV85V6x404m5oKMeqRV3tQ==", + "license": "MIT", + "dependencies": { + "mustache": "^4.0.1" + }, + "engines": { + "node": ">=8.9.0" + }, + "peerDependencies": { + "axe-core": ">=3" + } + }, + "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, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "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/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, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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, + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "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, + "license": "BSD-2-Clause", + "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, + "license": "BSD-2-Clause", + "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, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "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, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.3.0.tgz", + "integrity": "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "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, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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, + "license": "MIT" + }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, + "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, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "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/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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "license": "Apache-2.0", + "peer": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "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, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.0.tgz", + "integrity": "sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.56.0", + "@typescript-eslint/parser": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/utils": "8.56.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "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, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/tests/package.json b/tests/package.json index 9213e0db..4a2eba58 100644 --- a/tests/package.json +++ b/tests/package.json @@ -2,10 +2,13 @@ "name": "hometest-service-tests", "version": "1.0.0", "description": "Playwright test framework for hometest-service", - "main": "index.js", + "private": true, "scripts": { "build": "tsc --noEmit", + "lint": "eslint .", + "lint:fix": "eslint . --fix", "test": "playwright test", + "test:chrome": "playwright test --project=chromium", "test:headed": "playwright test --headed", "test:debug": "playwright test --debug", "test:ui": "playwright test --ui", @@ -21,14 +24,18 @@ "author": "", "license": "ISC", "devDependencies": { - "@playwright/test": "^1.58.0", - "@types/node": "^22.19.3", + "@eslint/js": "^9.39.2", + "@playwright/test": "^1.58.2", + "@types/node": "^25.2.3", "dotenv": "^17.2.3", "eslint": "^9.39.2", - "typescript": "^5.9.3" + "globals": "^17.3.0", + "typescript": "^5.9.3", + "typescript-eslint": "^8.56.0" }, "dependencies": { - "@axe-core/playwright": "^4.11.0", - "axe-html-reporter": "^2.2.11" + "@axe-core/playwright": "^4.11.1", + "axe-html-reporter": "^2.2.11", + "uuid": "^13.0.0" } } diff --git a/tests/page-objects/NhsLoginHelper.ts b/tests/page-objects/NhsLoginHelper.ts index 4e27e57e..08030837 100644 --- a/tests/page-objects/NhsLoginHelper.ts +++ b/tests/page-objects/NhsLoginHelper.ts @@ -18,7 +18,15 @@ export default class NhsLoginHelper { ): Promise { const loginPage = new NHSEmailAndPasswordPage(page); const codeSecurityPage = new CodeSecurityPage(page); + + // Navigate to UI which will redirect through HomePage → LoginPage → NHS Login await page.goto(`${this.config.uiBaseUrl}`); + + // Wait for redirect to NHS Login (external login page) + // URL is access.sandpit.signin.nhs.uk or similar + await page.waitForURL(/signin\.nhs\.uk/, { timeout: 60000 }); + console.log(`Redirected to NHS Login: ${page.url()}`); + await loginPage.fillAuthFormWithCredentialsAndClickContinue(nhsLoginUser); await codeSecurityPage.fillAuthOneTimePasswordAndClickContinue( nhsLoginUser.otp diff --git a/tests/playwright.config.ts b/tests/playwright.config.ts index ffdae9b6..18e0ca63 100644 --- a/tests/playwright.config.ts +++ b/tests/playwright.config.ts @@ -44,7 +44,9 @@ export default defineConfig({ workers: getNumberOfWorkers(config.authType), /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: [ + ['list'], ['json', { outputFile: './testResults/test-results.json' }], + ['junit', { outputFile: './testResults/junit-results.xml' }], ['html', { outputFolder: './testResults/html', open: 'on-failure' }] ], /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ @@ -55,6 +57,8 @@ export default defineConfig({ trace: 'on-first-retry', /* Screenshot on failure */ screenshot: { mode: 'only-on-failure', fullPage: true }, + /* Video recording on failure */ + video: 'on-first-retry', }, /* Configure projects for major browsers */ diff --git a/tests/tsconfig.json b/tests/tsconfig.json index 22692a10..fcb591d5 100644 --- a/tests/tsconfig.json +++ b/tests/tsconfig.json @@ -10,6 +10,6 @@ "resolveJsonModule": true, "types": ["node", "@playwright/test"] }, - "include": ["tests/**/*.ts", "libs/**/*.ts", "playwright.config.ts"], - "exclude": ["node_modules"] + "include": ["**/*.ts"], + "exclude": ["node_modules", "testResults"] } diff --git a/tests/utils/AccessibilityModule.ts b/tests/utils/AccessibilityModule.ts index fd8bf846..e54e35de 100644 --- a/tests/utils/AccessibilityModule.ts +++ b/tests/utils/AccessibilityModule.ts @@ -17,7 +17,7 @@ export class AccessibilityModule { constructor() { // Get standards from configuration or use default const standardsConfig = config.get(EnvironmentVariables.ACCESSIBILITY_STANDARDS); - this.standards = standardsConfig.split(',').map(s => s.trim()); + this.standards = standardsConfig.split(',').map((s: string) => s.trim()); // Get absolute path to tests/testResults/accessibility // __dirname is tests/utils, so go up one level to tests, then into testResults/accessibility diff --git a/tests/utils/users/BaseUserManager.ts b/tests/utils/users/BaseUserManager.ts index a1460dce..c4575a07 100644 --- a/tests/utils/users/BaseUserManager.ts +++ b/tests/utils/users/BaseUserManager.ts @@ -9,6 +9,16 @@ import { defaultUserAgent } from '../../playwright.config'; import { ConfigFactory } from '../../configuration/configuration'; import { v4 as uuidv4 } from 'uuid'; +import * as fs from 'fs'; +import * as path from 'path'; + +interface NetworkError { + url: string; + status: number; + statusText: string; + method: string; + timestamp: string; +} export abstract class BaseUserManager { protected readonly workerUsers: TUser[]; @@ -42,8 +52,12 @@ export abstract class BaseUserManager { return this.workerUsers[index]; } + private static readonly SESSION_CACHE_DIR = './.session-cache'; + getWorkerUserSessionFilePath(index: number): string { - return `./WorkerUserSession${index}.json`; + const cacheDir = BaseUserManager.SESSION_CACHE_DIR; + fs.mkdirSync(cacheDir, { recursive: true }); + return path.join(cacheDir, `WorkerUserSession${index}.json`); } async initializeBrowserForInitialLogin(): Promise<{ @@ -62,6 +76,85 @@ export abstract class BaseUserManager { return { page, browser, context }; } + private setupNetworkErrorCapture(page: Page): NetworkError[] { + const networkErrors: NetworkError[] = []; + + page.on('response', (response) => { + const status = response.status(); + if (status >= 400) { + networkErrors.push({ + url: response.url(), + status, + statusText: response.statusText(), + method: response.request().method(), + timestamp: new Date().toISOString(), + }); + console.error(`❌ HTTP ${status} ${response.statusText()}: ${response.request().method()} ${response.url()}`); + } + }); + + page.on('console', (msg) => { + if (msg.type() === 'error') { + console.error(`❌ Console error: ${msg.text()}`); + } + }); + + return networkErrors; + } + + private async captureFailureArtifacts( + page: Page, + context: BrowserContext, + user: BaseTestUser, + error: Error, + networkErrors: NetworkError[] + ): Promise { + const outputDir = 'testResults/global-setup-failures'; + fs.mkdirSync(outputDir, { recursive: true }); + + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const prefix = `${outputDir}/global-setup-${user.nhsNumber}-${timestamp}`; + + try { + // Take screenshot + await page.screenshot({ + path: `${prefix}-screenshot.png`, + fullPage: true + }); + console.log(`📸 Screenshot saved: ${prefix}-screenshot.png`); + } catch (screenshotError) { + console.error('Failed to capture screenshot:', screenshotError); + } + + // Save network errors + if (networkErrors.length > 0) { + fs.writeFileSync( + `${prefix}-network-errors.json`, + JSON.stringify(networkErrors, null, 2) + ); + console.log(`🌐 Network errors saved: ${prefix}-network-errors.json`); + } + + // Save error details + fs.writeFileSync( + `${prefix}-error.txt`, + `Error: ${error.message}\n\nStack trace:\n${error.stack}\n\nNetwork errors:\n${JSON.stringify(networkErrors, null, 2)}` + ); + console.log(`📝 Error details saved: ${prefix}-error.txt`); + + // Save trace if tracing was enabled + if (this.config.enableTracingOnGlobalSetup) { + try { + await context.tracing.stop({ + path: `${prefix}-trace.zip` + }); + console.log(`🔍 Trace saved: ${prefix}-trace.zip`); + } catch (traceError) { + console.error('Failed to save trace:', traceError); + } + } + } + async loginWorkerUsers(): Promise { process.env.GLOBAL_START_TIME = new Date().toISOString(); console.log( @@ -81,6 +174,10 @@ export abstract class BaseUserManager { const user = this.workerUsers[i]; const { browser, page, context } = await this.initializeBrowserForInitialLogin(); + + // Setup network error capture + const networkErrors = this.setupNetworkErrorCapture(page); + if (this.config.enableTracingOnGlobalSetup) { await context.tracing.start({ name: `global-setup-${uuidv4()}`, @@ -88,17 +185,35 @@ export abstract class BaseUserManager { snapshots: true }); } - await this.loginWorkerUser(user, page); - await page.context().storageState({ - path: this.getWorkerUserSessionFilePath(i) - }); + try { + await this.loginWorkerUser(user, page); - if (this.config.enableTracingOnGlobalSetup) { - await context.tracing.stop({ - path: `testResults/global-setup-trace/global-setup-trace-${user.nhsNumber}.zip` + await page.context().storageState({ + path: this.getWorkerUserSessionFilePath(i) }); + + if (this.config.enableTracingOnGlobalSetup) { + await context.tracing.stop({ + path: `testResults/global-setup-trace/global-setup-trace-${user.nhsNumber}.zip` + }); + } + } catch (error) { + console.error(`\n❌ Global setup failed for user ${user.nhsNumber}:`); + console.error(` Error: ${(error as Error).message}`); + + if (networkErrors.length > 0) { + console.error(`\n🌐 Network errors detected during setup:`); + networkErrors.forEach((err, idx) => { + console.error(` ${idx + 1}. ${err.method} ${err.url} => ${err.status} ${err.statusText}`); + }); + } + + await this.captureFailureArtifacts(page, context, user, error as Error, networkErrors); + await browser.close(); + throw error; } + await browser.close(); } } From 2b64decb17f51091e5292a65fff6e842ff53b469 Mon Sep 17 00:00:00 2001 From: Mikolaj Miotk Date: Wed, 18 Feb 2026 12:38:51 +0100 Subject: [PATCH 02/32] remove commented code --- .github/workflows/stage-2-test.yaml | 77 ----------------------------- 1 file changed, 77 deletions(-) diff --git a/.github/workflows/stage-2-test.yaml b/.github/workflows/stage-2-test.yaml index d3d00300..38951b9c 100644 --- a/.github/workflows/stage-2-test.yaml +++ b/.github/workflows/stage-2-test.yaml @@ -83,83 +83,6 @@ jobs: run: | echo "Nothing to save" - # test-playwright: - # name: "Playwright E2E tests" - # needs: [test-unit-ui, test-unit-lambda] - # runs-on: ubuntu-latest - # timeout-minutes: 30 - # steps: - # - name: "Checkout code" - # uses: actions/checkout@v6 - # - name: "Install mise" - # uses: jdx/mise-action@v3 - # with: - # install: true - # cache: true - # - name: "Install Playwright browsers" - # working-directory: tests - # run: npx playwright install --with-deps - # - name: "Create nhs login private key file" - # run: | - # mkdir -p local-environment/infra/resources/secrets - # cat > local-environment/infra/resources/secrets/nhs-login-private-key.pem << 'EOF' - # ${{ secrets.NHS_LOGIN_PRIVATE_KEY }} - # EOF - # - name: "Create credentials file" - # working-directory: tests - # run: | - # cat > credentials.ts << 'EOF' - # export const userPasswordGeneric = '${{ secrets.PLAYWRIGHT_PASSWORD }}'; - # export const OTP = '${{ secrets.PLAYWRIGHT_OTP }}'; - # EOF - # - name: "Start the application" - # run: | - # npm run start - - # - name: "Show application status" - # run: | - # docker compose -f local-environment/docker-compose.yml ps - # docker logs ui - - # - name: "Wait for UI to be reachable" - # run: | - # echo "Waiting for UI to be reachable at http://localhost:3000..." - # timeout=120 - # elapsed=0 - # until curl -sf http://localhost:3000 > /dev/null 2>&1; do - # if [ $elapsed -ge $timeout ]; then - # echo "Timeout: UI not reachable after ${timeout}s" - # docker logs ui - # exit 1 - # fi - # echo "Waiting... (${elapsed}s)" - # sleep 5 - # elapsed=$((elapsed + 5)) - # done - # echo "UI is reachable!" - - # - name: "Run Playwright tests" - # # working-directory: tests - # run: npm run test:playwright - # env: - # CI: true - # # BASE_URL: ${{ vars.TEST_BASE_URL }} - # FORCE_COLOR: true - # - name: "Grab docker compose logs" - # run: | - # for service in $(docker compose -f local-environment/docker-compose.yml ps --services); do - # docker compose -f local-environment/docker-compose.yml logs "$service" > "tests/testResults/docker-compose-${service}.log" 2>&1 - # done - # if: always() - - # - name: "Upload test results" - # uses: actions/upload-artifact@v4 - # if: always() - # with: - # name: playwright-report - # path: tests/testResults/ - # retention-days: 30 - perform-static-analysis: name: "Perform static analysis" needs: [test-unit-ui, test-unit-lambda] From 604c910abaf74fecfad1b033f53d7f3a1bdf1554 Mon Sep 17 00:00:00 2001 From: Mikolaj Miotk Date: Wed, 18 Feb 2026 12:39:29 +0100 Subject: [PATCH 03/32] remove commented code v2 --- local-environment/infra/main.tf | 3 --- 1 file changed, 3 deletions(-) diff --git a/local-environment/infra/main.tf b/local-environment/infra/main.tf index b27c9463..2566c616 100644 --- a/local-environment/infra/main.tf +++ b/local-environment/infra/main.tf @@ -71,9 +71,6 @@ resource "null_resource" "validate_secrets" { error_message = <<-EOT Missing required secret files in ${local.secrets_dir}: ${join("\n - ", local.missing_secrets)} - - To generate the NHS Login private key, run: - openssl genrsa -out ${local.secrets_dir}/nhs-login-private-key.pem 2048 EOT } } From d79e0d1e27b81d92778cd5a925ac8f5860d539a6 Mon Sep 17 00:00:00 2001 From: Mikolaj Miotk Date: Wed, 18 Feb 2026 12:40:09 +0100 Subject: [PATCH 04/32] test pipeline --- .github/workflows/playwright-e2e.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/playwright-e2e.yaml b/.github/workflows/playwright-e2e.yaml index 9dec2c87..c47269d0 100644 --- a/.github/workflows/playwright-e2e.yaml +++ b/.github/workflows/playwright-e2e.yaml @@ -4,6 +4,9 @@ name: "Playwright E2E Tests" on: schedule: - cron: '0 2 * * *' # Every day at 2am UTC + pull_request: + branches: + - main workflow_dispatch: inputs: browser: From 9e53b89704f56b437def1a658658320c606c8255 Mon Sep 17 00:00:00 2001 From: Mikolaj Miotk Date: Wed, 18 Feb 2026 12:41:20 +0100 Subject: [PATCH 05/32] test pipeline - remove --- .github/workflows/playwright-e2e.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/playwright-e2e.yaml b/.github/workflows/playwright-e2e.yaml index c47269d0..9dec2c87 100644 --- a/.github/workflows/playwright-e2e.yaml +++ b/.github/workflows/playwright-e2e.yaml @@ -4,9 +4,6 @@ name: "Playwright E2E Tests" on: schedule: - cron: '0 2 * * *' # Every day at 2am UTC - pull_request: - branches: - - main workflow_dispatch: inputs: browser: From a997c9d1ece672c10126eed30d640ab51bc4fdf8 Mon Sep 17 00:00:00 2001 From: Mikolaj Miotk Date: Wed, 18 Feb 2026 12:51:58 +0100 Subject: [PATCH 06/32] cleanup v5 --- .pre-commit-config.yaml | 35 ----------------------------------- 1 file changed, 35 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 446591d8..417f95d2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -77,38 +77,3 @@ repos: language: system files: ^tests/.*\.(ts|js)$ pass_filenames: false - - # - id: typescript-check-ui - # name: TypeScript (UI) - # entry: npm --prefix ui run build -- --noEmit - # language: system - # files: ^ui/.*\.tsx?$ - # pass_filenames: false - # # stages: [pre-push] # Slow, run on push - - # - id: typescript-check-lambdas - # name: TypeScript (Lambdas) - # entry: bash -c 'cd lambdas && npx tsc --noEmit' - # language: system - # files: ^lambdas/.*\.ts$ - # pass_filenames: false - # # stages: [pre-push] - - # - id: jest-unit-ui - # name: Jest Unit Tests (UI - affected only) - # entry: npm --prefix ui run test -- --bail --findRelatedTests --passWithNoTests - # language: system - # files: ^ui/.*\.(ts|tsx)$ - # # stages: [pre-push] - - # - id: jest-unit-lambdas - # name: Jest Unit Tests (Lambdas - affected only) - # entry: bash -c 'cd lambdas && npx jest --bail --findRelatedTests --passWithNoTests' - # language: system - # files: ^lambdas/.*\.ts$ - # # stages: [pre-push] - - # - repo: https://github.com/rhysd/actionlint - # rev: v1.7.11 - # hooks: - # - id: actionlint From 665621d9bf9e6a76096273459c539e604f1d6844 Mon Sep 17 00:00:00 2001 From: Mikolaj Miotk Date: Wed, 18 Feb 2026 14:22:59 +0100 Subject: [PATCH 07/32] Add additional configs --- tests/configuration/configuration.ts | 38 ++++++++++++++++++- tests/fixtures/consoleErrorFixture.ts | 4 ++ tests/global-setup.ts | 15 +++++++- .../page-objects/NHSLogin/CodeSecurityPage.ts | 6 ++- 4 files changed, 60 insertions(+), 3 deletions(-) diff --git a/tests/configuration/configuration.ts b/tests/configuration/configuration.ts index 1a3182f2..757b1625 100644 --- a/tests/configuration/configuration.ts +++ b/tests/configuration/configuration.ts @@ -48,16 +48,52 @@ export class ConfigFactory { const defaultConfig = this.loadDefaultConfiguration(); // start with default configuration const envConfig = this.readConfigurationFromEnvFile(); // override with values from .env file const localConfig = this.readConfigurationFromLocalFile(); // override with local JSON file + const cliEnvConfig = this.readConfigurationFromProcessEnv(); // highest priority: CLI env vars const cachedConfig = { ...defaultConfig, ...envConfig, - ...localConfig + ...localConfig, + ...cliEnvConfig }; return cachedConfig; } + /** + * Read configuration directly from process.env (CLI environment variables) + * This has the highest priority and overrides all other configuration sources + */ + private static readConfigurationFromProcessEnv(): Partial { + const config: Partial = {}; + + // Only include values that are explicitly set in environment + if (process.env[EnvironmentVariables.UI_BASE_URL]) { + config.uiBaseUrl = process.env[EnvironmentVariables.UI_BASE_URL]; + } + if (process.env[EnvironmentVariables.API_BASE_URL]) { + config.apiBaseUrl = process.env[EnvironmentVariables.API_BASE_URL]; + } + if (process.env[EnvironmentVariables.HEADLESS] !== undefined) { + config.headless = process.env[EnvironmentVariables.HEADLESS] === 'true'; + } + if (process.env[EnvironmentVariables.TIMEOUT]) { + config.timeout = parseInt(process.env[EnvironmentVariables.TIMEOUT], 10); + } + if (process.env[EnvironmentVariables.SLOW_MO]) { + config.slowMo = parseInt(process.env[EnvironmentVariables.SLOW_MO], 10); + } + if (process.env[EnvironmentVariables.ACCESSIBILITY_STANDARDS]) { + config.accessibilityStandards = process.env[EnvironmentVariables.ACCESSIBILITY_STANDARDS]; + } + + if (Object.keys(config).length > 0) { + console.log('✅ Applied CLI environment variable overrides:', Object.keys(config).join(', ')); + } + + return config; + } + private static loadDefaultConfiguration(): ConfigInterface { return { uiBaseUrl: 'http://localhost:3000', diff --git a/tests/fixtures/consoleErrorFixture.ts b/tests/fixtures/consoleErrorFixture.ts index 6655fefa..2c65a508 100644 --- a/tests/fixtures/consoleErrorFixture.ts +++ b/tests/fixtures/consoleErrorFixture.ts @@ -39,11 +39,15 @@ const defaultOptions: ErrorCaptureOptions = { // Network transient errors /net::ERR_NETWORK_CHANGED/, /net::ERR_CONNECTION_RESET/, + /net::ERR_CONNECTION_REFUSED/, /net::ERR_INTERNET_DISCONNECTED/, // External NHS resources not available in test environment /NHSCookieConsent is not defined/, /nhsapp is not defined/, /"undefined" is not valid JSON/, + // CSP font violations from external NHS assets + /Content Security Policy directive.*font-src/, + /assets\.nhs\.uk.*font/i, ], ignoreStatusCodes: [], }; diff --git a/tests/global-setup.ts b/tests/global-setup.ts index ebec09b7..1bdc47f5 100644 --- a/tests/global-setup.ts +++ b/tests/global-setup.ts @@ -1,3 +1,5 @@ +import * as fs from 'fs'; +import * as path from 'path'; import { ConfigFactory } from './configuration/configuration'; import { CredentialsHelper } from './utils/CredentialsHelper'; import { UserManagerFactory } from './utils/users/UserManagerFactory'; @@ -7,8 +9,19 @@ async function globalSetup() { const config = ConfigFactory.getConfig(); console.log(`Tests will run on environment: ${process.env.ENV ?? 'local'}`); - // Add user credentials to env variable + // Add user credentials to env variable await new CredentialsHelper().addCredentialsToEnvVariable(); + + // Check if we should skip login (for reusing existing sessions) + if (process.env.SKIP_LOGIN === 'true') { + const sessionDir = path.resolve(__dirname, '.session-cache'); + if (fs.existsSync(sessionDir) && fs.readdirSync(sessionDir).length > 0) { + console.log('⏭️ SKIP_LOGIN=true - Reusing existing session files'); + return; + } + console.log('⚠️ SKIP_LOGIN=true but no session files found, proceeding with login'); + } + await new UserManagerFactory().getUserManager().loginWorkerUsers(); } diff --git a/tests/page-objects/NHSLogin/CodeSecurityPage.ts b/tests/page-objects/NHSLogin/CodeSecurityPage.ts index 35ebd36b..303e571d 100644 --- a/tests/page-objects/NHSLogin/CodeSecurityPage.ts +++ b/tests/page-objects/NHSLogin/CodeSecurityPage.ts @@ -25,7 +25,8 @@ export class CodeSecurityPage { async fillAuthOneTimePasswordAndClickContinue( oneTimePassword: string ): Promise { - await this.waitForOtpTrigger(); + // Wait for OTP input field to be visible (indicates OTP page loaded) + await this.securityCodeField.waitFor({ timeout: 30000 }); await this.fillAuthOneTimePassword(oneTimePassword); await this.continueBtn.click(); } @@ -34,6 +35,9 @@ export class CodeSecurityPage { await this.rememberDeviceCheckbox.click(); } + /** + * @deprecated Use waitFor on securityCodeField instead - network wait is unreliable + */ async waitForOtpTrigger(): Promise { await this.page.waitForResponse( (response) => From 4b7323472ac765813fce7d25640bb97a7339c8e4 Mon Sep 17 00:00:00 2001 From: Mikolaj Miotk Date: Wed, 18 Feb 2026 14:33:15 +0100 Subject: [PATCH 08/32] run tests against dev env --- .github/workflows/playwright-e2e.yaml | 38 +++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/.github/workflows/playwright-e2e.yaml b/.github/workflows/playwright-e2e.yaml index 9dec2c87..32f7d46f 100644 --- a/.github/workflows/playwright-e2e.yaml +++ b/.github/workflows/playwright-e2e.yaml @@ -1,11 +1,20 @@ --- name: "Playwright E2E Tests" +run-name: "E2E Tests [${{ inputs.environment || 'local' }}] ${{ inputs.browser || 'chromium' }}${{ inputs.test_filter && format(' - {0}', inputs.test_filter) || '' }}" on: schedule: - cron: '0 2 * * *' # Every day at 2am UTC workflow_dispatch: inputs: + environment: + description: "Target environment" + required: false + default: "local" + type: choice + options: + - local + - dev browser: description: "Browser to run tests on" required: false @@ -27,6 +36,8 @@ jobs: name: "Playwright E2E tests" runs-on: ubuntu-latest timeout-minutes: 30 + env: + TARGET_ENV: ${{ inputs.environment || 'local' }} steps: - name: "Checkout code" uses: actions/checkout@v6 @@ -42,6 +53,7 @@ jobs: run: npx playwright install --with-deps - name: "Create nhs login private key file" + if: env.TARGET_ENV == 'local' run: | mkdir -p local-environment/infra/resources/secrets cat > local-environment/infra/resources/secrets/nhs-login-private-key.pem << 'EOF' @@ -57,15 +69,18 @@ jobs: EOF - name: "Start the application" + if: env.TARGET_ENV == 'local' run: | npm run start - name: "Show application status" + if: env.TARGET_ENV == 'local' run: | docker compose -f local-environment/docker-compose.yml ps docker logs ui - name: "Get terraform outputs" + if: env.TARGET_ENV == 'local' id: terraform run: | UI_URL=$(terraform -chdir=local-environment/infra output -raw ui_url) @@ -75,16 +90,29 @@ jobs: echo "UI URL: $UI_URL" echo "API URL: $API_URL" + - name: "Set environment URLs" + id: urls + run: | + if [ "$TARGET_ENV" == "dev" ]; then + echo "ui_url=https://dev.hometest.service.nhs.uk" >> $GITHUB_OUTPUT + echo "api_url=https://dev.hometest.service.nhs.uk/" >> $GITHUB_OUTPUT + else + echo "ui_url=${{ steps.terraform.outputs.ui_url }}" >> $GITHUB_OUTPUT + echo "api_url=${{ steps.terraform.outputs.api_base_url }}" >> $GITHUB_OUTPUT + fi + - name: "Wait for UI to be reachable" run: | - UI_URL="${{ steps.terraform.outputs.ui_url }}" + UI_URL="${{ steps.urls.outputs.ui_url }}" echo "Waiting for UI to be reachable at $UI_URL..." timeout=120 elapsed=0 until curl -sf "$UI_URL" > /dev/null 2>&1; do if [ $elapsed -ge $timeout ]; then echo "Timeout: UI not reachable after ${timeout}s" - docker logs ui + if [ "$TARGET_ENV" == "local" ]; then + docker logs ui + fi exit 1 fi echo "Waiting... (${elapsed}s)" @@ -118,15 +146,15 @@ jobs: env: CI: true FORCE_COLOR: true - UI_BASE_URL: ${{ steps.terraform.outputs.ui_url }} - API_BASE_URL: ${{ steps.terraform.outputs.api_base_url }} + UI_BASE_URL: ${{ steps.urls.outputs.ui_url }} + API_BASE_URL: ${{ steps.urls.outputs.api_url }} - name: "Grab docker compose logs" + if: always() && env.TARGET_ENV == 'local' run: | for service in $(docker compose -f local-environment/docker-compose.yml ps --services); do docker compose -f local-environment/docker-compose.yml logs "$service" > "tests/testResults/docker-compose-${service}.log" 2>&1 done - if: always() - name: "Publish Test Results" uses: dorny/test-reporter@v2 From cae21bd156251b8b245a28ed6bd6d41db518f61a Mon Sep 17 00:00:00 2001 From: Mikolaj Miotk Date: Wed, 18 Feb 2026 14:34:10 +0100 Subject: [PATCH 09/32] run tests against dev env v2 --- .github/workflows/playwright-e2e.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/playwright-e2e.yaml b/.github/workflows/playwright-e2e.yaml index 32f7d46f..3f371aa5 100644 --- a/.github/workflows/playwright-e2e.yaml +++ b/.github/workflows/playwright-e2e.yaml @@ -1,6 +1,6 @@ --- name: "Playwright E2E Tests" -run-name: "E2E Tests [${{ inputs.environment || 'local' }}] ${{ inputs.browser || 'chromium' }}${{ inputs.test_filter && format(' - {0}', inputs.test_filter) || '' }}" +run-name: "E2E Tests: env: [${{ inputs.environment || 'local' }}], browser: ${{ inputs.browser || 'chromium' }}, filter: ${{ inputs.test_filter && format(' - {0}', inputs.test_filter) || '' }}" on: schedule: From ea3a3c9825c81f55685702e29acc3035d9f4d920 Mon Sep 17 00:00:00 2001 From: Mikolaj Miotk Date: Wed, 18 Feb 2026 14:39:38 +0100 Subject: [PATCH 10/32] run tests against dev env v3 --- .github/workflows/playwright-e2e.yaml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/playwright-e2e.yaml b/.github/workflows/playwright-e2e.yaml index 3f371aa5..064959c5 100644 --- a/.github/workflows/playwright-e2e.yaml +++ b/.github/workflows/playwright-e2e.yaml @@ -42,18 +42,14 @@ jobs: - name: "Checkout code" uses: actions/checkout@v6 - - name: "Install mise" - uses: jdx/mise-action@v3 - with: - install: true - cache: true + - name: Initialize mise + uses: ./.github/actions/init-mise - name: "Install Playwright browsers" working-directory: tests run: npx playwright install --with-deps - name: "Create nhs login private key file" - if: env.TARGET_ENV == 'local' run: | mkdir -p local-environment/infra/resources/secrets cat > local-environment/infra/resources/secrets/nhs-login-private-key.pem << 'EOF' @@ -129,7 +125,7 @@ jobs: FILTER="${{ inputs.test_filter }}" # Build the command - CMD="npx playwright test" + CMD="ENV=dev npx playwright test" # Add browser project if [ "$BROWSER" != "all" ]; then From a4f7e444fa450d2e20ed20d708e5e984ea80f78e Mon Sep 17 00:00:00 2001 From: Mikolaj Miotk Date: Wed, 18 Feb 2026 14:42:39 +0100 Subject: [PATCH 11/32] run tests against dev env v4 --- .github/workflows/playwright-e2e.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/playwright-e2e.yaml b/.github/workflows/playwright-e2e.yaml index 064959c5..0589f0c9 100644 --- a/.github/workflows/playwright-e2e.yaml +++ b/.github/workflows/playwright-e2e.yaml @@ -1,6 +1,6 @@ --- name: "Playwright E2E Tests" -run-name: "E2E Tests: env: [${{ inputs.environment || 'local' }}], browser: ${{ inputs.browser || 'chromium' }}, filter: ${{ inputs.test_filter && format(' - {0}', inputs.test_filter) || '' }}" +run-name: "E2E Tests: env: ${{ inputs.environment || 'local' }}, browser: ${{ inputs.browser || 'chromium' }}, filter: ${{ inputs.test_filter && format(' - {0}', inputs.test_filter) || '' }}" on: schedule: @@ -125,7 +125,7 @@ jobs: FILTER="${{ inputs.test_filter }}" # Build the command - CMD="ENV=dev npx playwright test" + CMD="HEADLESS=true ENV=dev npx playwright test" # Add browser project if [ "$BROWSER" != "all" ]; then From 11122193f4641a682fc816881e36d1105286304b Mon Sep 17 00:00:00 2001 From: Mikolaj Miotk Date: Wed, 18 Feb 2026 14:47:00 +0100 Subject: [PATCH 12/32] run tests against dev env v5 --- tests/configuration/.env.dev | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/configuration/.env.dev b/tests/configuration/.env.dev index 71e19849..2def9ad7 100644 --- a/tests/configuration/.env.dev +++ b/tests/configuration/.env.dev @@ -4,3 +4,8 @@ API_BASE_URL= HEADLESS=false TIMEOUT=30000 SLOW_MO=0 + +# External Links +EXTERNAL_LINK_SEXUAL_HEALTH_CLINIC=https://www.nhs.uk/service-search/sexual-health-services/find-a-sexual-health-clinic/ +EXTERNAL_LINK_NEAREST_AE=https://www.nhs.uk/service-search/find-an-accident-and-emergency-service/ +EXTERNAL_LINK_HIV_AIDS_INFO=https://www.nhs.uk/conditions/hiv-and-aids/ From 5fb96c438d52dcd8a2644ee97b415e3c07fb6123 Mon Sep 17 00:00:00 2001 From: Mikolaj Miotk Date: Wed, 18 Feb 2026 15:55:38 +0100 Subject: [PATCH 13/32] add clean all --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 3bf04ca6..8bb2ef22 100644 --- a/package.json +++ b/package.json @@ -7,11 +7,12 @@ "postinstall": "npm --prefix ui install && npm --prefix lambdas install && npm --prefix tests install", "test": "npm --prefix ui run test && npm --prefix lambdas run test", - "test:playwright": "UI_BASE_URL=$(terraform -chdir=local-environment/infra output -raw ui_url) API_BASE_URL=$(terraform -chdir=local-environment/infra output -raw api_base_url) npm --prefix tests run test:chrome", + "test:playwright": "ENV=dev UI_BASE_URL=$(terraform -chdir=local-environment/infra output -raw ui_url) API_BASE_URL=$(terraform -chdir=local-environment/infra output -raw api_base_url) npm --prefix tests run test:chrome", "build:lambdas": "npm --prefix lambdas run build", "package:lambdas": "npm --prefix lambdas run package", + "clean:all": "npm run stop && rm -rf ./node_modules */node_modules */dist && docker system prune -f --volumes", "start": "npm ci && npm run local:start", "stop": "npm run local:stop", From 885c2a205ea991b38bc5b9e23ddee6ef79db07d8 Mon Sep 17 00:00:00 2001 From: Mikolaj Miotk Date: Wed, 18 Feb 2026 16:18:06 +0100 Subject: [PATCH 14/32] add some checks --- .github/workflows/cicd-1-pull-request.yaml | 28 +++++----- .github/workflows/cicd-2-publish.yaml | 20 +++---- .github/workflows/cicd-3-deploy.yaml | 28 +++++----- .github/workflows/playwright-e2e.yaml | 52 ++++++++++--------- .pre-commit-config.yaml | 22 +++++++- .../scripts/localstack/deploy.sh | 2 - .../scripts/localstack/get_supplier_id.sh | 2 +- 7 files changed, 86 insertions(+), 68 deletions(-) diff --git a/.github/workflows/cicd-1-pull-request.yaml b/.github/workflows/cicd-1-pull-request.yaml index 5e6f2f72..4ee1381d 100644 --- a/.github/workflows/cicd-1-pull-request.yaml +++ b/.github/workflows/cicd-1-pull-request.yaml @@ -32,28 +32,30 @@ jobs: id: variables run: | datetime=$(date -u +'%Y-%m-%dT%H:%M:%S%z') - BUILD_DATETIME=$datetime make version-create-effective-file - echo "build_datetime_london=$(TZ=Europe/London date --date=$datetime +'%Y-%m-%dT%H:%M:%S%z')" >> $GITHUB_OUTPUT - echo "build_datetime=$datetime" >> $GITHUB_OUTPUT - echo "build_timestamp=$(date --date=$datetime -u +'%Y%m%d%H%M%S')" >> $GITHUB_OUTPUT - echo "build_epoch=$(date --date=$datetime -u +'%s')" >> $GITHUB_OUTPUT - echo "nodejs_version=$(grep "^v" .nvmrc | cut -f2 -d'v')" >> $GITHUB_OUTPUT - echo "python_version=$(grep "^python\s" .tool-versions | cut -f2 -d' ')" >> $GITHUB_OUTPUT - echo "terraform_version=$(grep "^terraform\s" .tool-versions | cut -f2 -d' ')" >> $GITHUB_OUTPUT - echo "version=$(head -n 1 .version 2> /dev/null || echo unknown)" >> $GITHUB_OUTPUT + BUILD_DATETIME="$datetime" make version-create-effective-file + { + echo "build_datetime_london=$(TZ=Europe/London date --date="$datetime" +'%Y-%m-%dT%H:%M:%S%z')" + echo "build_datetime=$datetime" + echo "build_timestamp=$(date --date="$datetime" -u +'%Y%m%d%H%M%S')" + echo "build_epoch=$(date --date="$datetime" -u +'%s')" + echo "nodejs_version=$(grep "^nodejs\s" .tool-versions | cut -f2 -d' ')" + echo "python_version=$(grep "^python\s" .tool-versions | cut -f2 -d' ')" + echo "terraform_version=$(grep "^terraform\s" .tool-versions | cut -f2 -d' ')" + echo "version=$(head -n 1 .version 2> /dev/null || echo unknown)" + } >> "$GITHUB_OUTPUT" - name: "Check if pull request exists for this branch" id: pr_exists env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - branch_name=${GITHUB_HEAD_REF:-$(echo $GITHUB_REF | sed 's#refs/heads/##')} + branch_name=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} echo "Current branch is '$branch_name'" - if gh pr list --head $branch_name | grep -q .; then + if gh pr list --head "$branch_name" | grep -q .; then echo "Pull request exists" - echo "does_pull_request_exist=true" >> $GITHUB_OUTPUT + echo "does_pull_request_exist=true" >> "$GITHUB_OUTPUT" else echo "Pull request doesn't exist" - echo "does_pull_request_exist=false" >> $GITHUB_OUTPUT + echo "does_pull_request_exist=false" >> "$GITHUB_OUTPUT" fi - name: "List variables" run: | diff --git a/.github/workflows/cicd-2-publish.yaml b/.github/workflows/cicd-2-publish.yaml index 229a1e94..2fc60852 100644 --- a/.github/workflows/cicd-2-publish.yaml +++ b/.github/workflows/cicd-2-publish.yaml @@ -28,14 +28,16 @@ jobs: id: variables run: | datetime=$(date -u +'%Y-%m-%dT%H:%M:%S%z') - echo "build_datetime=$datetime" >> $GITHUB_OUTPUT - echo "build_timestamp=$(date --date=$datetime -u +'%Y%m%d%H%M%S')" >> $GITHUB_OUTPUT - echo "build_epoch=$(date --date=$datetime -u +'%s')" >> $GITHUB_OUTPUT - echo "nodejs_version=$(grep "^nodejs\s" .tool-versions | cut -f2 -d' ')" >> $GITHUB_OUTPUT - echo "python_version=$(grep "^python\s" .tool-versions | cut -f2 -d' ')" >> $GITHUB_OUTPUT - echo "terraform_version=$(grep "^terraform\s" .tool-versions | cut -f2 -d' ')" >> $GITHUB_OUTPUT - # TODO: Get the version, but it may not be the .version file as this should come from the CI/CD Pull Request Workflow - echo "version=$(head -n 1 .version 2> /dev/null || echo unknown)" >> $GITHUB_OUTPUT + { + echo "build_datetime=$datetime" + echo "build_timestamp=$(date --date="$datetime" -u +'%Y%m%d%H%M%S')" + echo "build_epoch=$(date --date="$datetime" -u +'%s')" + echo "nodejs_version=$(grep "^nodejs\s" .tool-versions | cut -f2 -d' ')" + echo "python_version=$(grep "^python\s" .tool-versions | cut -f2 -d' ')" + echo "terraform_version=$(grep "^terraform\s" .tool-versions | cut -f2 -d' ')" + # TODO: Get the version, but it may not be the .version file as this should come from the CI/CD Pull Request Workflow + echo "version=$(head -n 1 .version 2> /dev/null || echo unknown)" + } >> "$GITHUB_OUTPUT" - name: "List variables" run: | export BUILD_DATETIME="${{ steps.variables.outputs.build_datetime }}" @@ -89,7 +91,7 @@ jobs: steps: - name: "Check prerequisites for notification" id: check - run: echo "secret_exist=${{ secrets.TEAMS_NOTIFICATION_WEBHOOK_URL != '' }}" >> $GITHUB_OUTPUT + run: echo "secret_exist=${{ secrets.TEAMS_NOTIFICATION_WEBHOOK_URL != '' }}" >> "$GITHUB_OUTPUT" - name: "Notify on publishing packages" if: steps.check.outputs.secret_exist == 'true' uses: nhs-england-tools/notify-msteams-action@v1.0.0 diff --git a/.github/workflows/cicd-3-deploy.yaml b/.github/workflows/cicd-3-deploy.yaml index 34892977..aa2466c8 100644 --- a/.github/workflows/cicd-3-deploy.yaml +++ b/.github/workflows/cicd-3-deploy.yaml @@ -3,11 +3,6 @@ name: "CI/CD deploy" on: workflow_dispatch: - inputs: - tag: - description: "This is the tag that is oging to be deployed" - required: true - default: "latest" jobs: metadata: @@ -22,23 +17,24 @@ jobs: python_version: ${{ steps.variables.outputs.python_version }} terraform_version: ${{ steps.variables.outputs.terraform_version }} version: ${{ steps.variables.outputs.version }} - tag: ${{ steps.variables.outputs.tag }} steps: - name: "Checkout code" uses: actions/checkout@v6 + - name: "Set CI/CD variables" id: variables run: | datetime=$(date -u +'%Y-%m-%dT%H:%M:%S%z') - echo "build_datetime=$datetime" >> $GITHUB_OUTPUT - echo "build_timestamp=$(date --date=$datetime -u +'%Y%m%d%H%M%S')" >> $GITHUB_OUTPUT - echo "build_epoch=$(date --date=$datetime -u +'%s')" >> $GITHUB_OUTPUT - echo "nodejs_version=$(grep "^nodejs\s" .tool-versions | cut -f2 -d' ')" >> $GITHUB_OUTPUT - echo "python_version=$(grep "^python\s" .tool-versions | cut -f2 -d' ')" >> $GITHUB_OUTPUT - echo "terraform_version=$(grep "^terraform\s" .tool-versions | cut -f2 -d' ')" >> $GITHUB_OUTPUT - # TODO: Get the version, but it may not be the .version file as this should come from the CI/CD Pull Request Workflow - echo "version=$(head -n 1 .version 2> /dev/null || echo unknown)" >> $GITHUB_OUTPUT - echo "tag=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT + { + echo "build_datetime=$datetime" + echo "build_timestamp=$(date --date="$datetime" -u +'%Y%m%d%H%M%S')" + echo "build_epoch=$(date --date="$datetime" -u +'%s')" + echo "nodejs_version=$(grep "^nodejs\s" .tool-versions | cut -f2 -d' ')" + echo "python_version=$(grep "^python\s" .tool-versions | cut -f2 -d' ')" + echo "terraform_version=$(grep "^terraform\s" .tool-versions | cut -f2 -d' ')" + # TODO: Get the version, but it may not be the .version file as this should come from the CI/CD Pull Request Workflow + echo "version=$(head -n 1 .version 2> /dev/null || echo unknown)" + } >> "$GITHUB_OUTPUT" - name: "List variables" run: | export BUILD_DATETIME="${{ steps.variables.outputs.build_datetime }}" @@ -48,8 +44,8 @@ jobs: export PYTHON_VERSION="${{ steps.variables.outputs.python_version }}" export TERRAFORM_VERSION="${{ steps.variables.outputs.terraform_version }}" export VERSION="${{ steps.variables.outputs.version }}" - export TAG="${{ steps.variables.outputs.tag }}" make list-variables + deploy: name: "Deploy to an environment" runs-on: ubuntu-latest diff --git a/.github/workflows/playwright-e2e.yaml b/.github/workflows/playwright-e2e.yaml index 0589f0c9..a09ed617 100644 --- a/.github/workflows/playwright-e2e.yaml +++ b/.github/workflows/playwright-e2e.yaml @@ -81,8 +81,8 @@ jobs: run: | UI_URL=$(terraform -chdir=local-environment/infra output -raw ui_url) API_URL=$(terraform -chdir=local-environment/infra output -raw api_base_url) - echo "ui_url=$UI_URL" >> $GITHUB_OUTPUT - echo "api_base_url=$API_URL" >> $GITHUB_OUTPUT + echo "ui_url=$UI_URL" >> "$GITHUB_OUTPUT" + echo "api_base_url=$API_URL" >> "$GITHUB_OUTPUT" echo "UI URL: $UI_URL" echo "API URL: $API_URL" @@ -90,11 +90,11 @@ jobs: id: urls run: | if [ "$TARGET_ENV" == "dev" ]; then - echo "ui_url=https://dev.hometest.service.nhs.uk" >> $GITHUB_OUTPUT - echo "api_url=https://dev.hometest.service.nhs.uk/" >> $GITHUB_OUTPUT + echo "ui_url=https://dev.hometest.service.nhs.uk" >> "$GITHUB_OUTPUT" + echo "api_url=https://dev.hometest.service.nhs.uk/" >> "$GITHUB_OUTPUT" else - echo "ui_url=${{ steps.terraform.outputs.ui_url }}" >> $GITHUB_OUTPUT - echo "api_url=${{ steps.terraform.outputs.api_base_url }}" >> $GITHUB_OUTPUT + echo "ui_url=${{ steps.terraform.outputs.ui_url }}" >> "$GITHUB_OUTPUT" + echo "api_url=${{ steps.terraform.outputs.api_base_url }}" >> "$GITHUB_OUTPUT" fi - name: "Wait for UI to be reachable" @@ -138,7 +138,7 @@ jobs: fi echo "Running: $CMD" - eval $CMD + eval "$CMD" env: CI: true FORCE_COLOR: true @@ -164,24 +164,26 @@ jobs: - name: "Generate Job Summary" if: always() run: | - echo "## Playwright Test Results" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - if [ -f tests/testResults/junit-results.xml ]; then - TESTS=$(grep -oP 'tests="\K[0-9]+' tests/testResults/junit-results.xml | head -1) - FAILURES=$(grep -oP 'failures="\K[0-9]+' tests/testResults/junit-results.xml | head -1) - ERRORS=$(grep -oP 'errors="\K[0-9]+' tests/testResults/junit-results.xml | head -1) - TIME=$(grep -oP 'time="\K[0-9.]+' tests/testResults/junit-results.xml | head -1) - PASSED=$((TESTS - FAILURES - ERRORS)) - echo "| Metric | Value |" >> $GITHUB_STEP_SUMMARY - echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| Total Tests | $TESTS |" >> $GITHUB_STEP_SUMMARY - echo "| :white_check_mark: Passed | $PASSED |" >> $GITHUB_STEP_SUMMARY - echo "| :x: Failed | $FAILURES |" >> $GITHUB_STEP_SUMMARY - echo "| :warning: Errors | $ERRORS |" >> $GITHUB_STEP_SUMMARY - echo "| :stopwatch: Duration | ${TIME}s |" >> $GITHUB_STEP_SUMMARY - else - echo ":warning: No test results found" >> $GITHUB_STEP_SUMMARY - fi + { + echo "## Playwright Test Results" + echo "" + if [ -f tests/testResults/junit-results.xml ]; then + TESTS=$(grep -oP 'tests="\K[0-9]+' tests/testResults/junit-results.xml | head -1) + FAILURES=$(grep -oP 'failures="\K[0-9]+' tests/testResults/junit-results.xml | head -1) + ERRORS=$(grep -oP 'errors="\K[0-9]+' tests/testResults/junit-results.xml | head -1) + TIME=$(grep -oP 'time="\K[0-9.]+' tests/testResults/junit-results.xml | head -1) + PASSED=$((TESTS - FAILURES - ERRORS)) + echo "| Metric | Value |" + echo "|--------|-------|" + echo "| Total Tests | $TESTS |" + echo "| :white_check_mark: Passed | $PASSED |" + echo "| :x: Failed | $FAILURES |" + echo "| :warning: Errors | $ERRORS |" + echo "| :stopwatch: Duration | ${TIME}s |" + else + echo ":warning: No test results found" + fi + } >> "$GITHUB_STEP_SUMMARY" - name: "Upload test results" uses: actions/upload-artifact@v4 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 417f95d2..9989b1de 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,13 +8,20 @@ repos: args: [--markdown-linebreak-ext=md] - id: end-of-file-fixer - id: check-yaml + args: [--allow-multiple-documents] - id: check-json + - id: check-toml - id: check-added-large-files + args: ["--maxkb=500"] - id: check-case-conflict - id: check-merge-conflict + - id: check-symlinks - id: detect-private-key - id: check-executables-have-shebangs - - id: forbid-submodules + - id: mixed-line-ending + args: [--fix=lf] + - id: no-commit-to-branch + args: [--branch, main, --branch, master, --branch, develop] - repo: local hooks: @@ -44,7 +51,6 @@ repos: rev: v0.47.0 hooks: - id: markdownlint - # exclude: ^infrastructure/src args: [--fix] - repo: https://github.com/antonbabenko/pre-commit-terraform @@ -77,3 +83,15 @@ repos: language: system files: ^tests/.*\.(ts|js)$ pass_filenames: false + + - repo: https://github.com/rhysd/actionlint + rev: v1.7.11 + hooks: + - id: actionlint + files: ^\.github/workflows/ + + - repo: https://github.com/shellcheck-py/shellcheck-py + rev: v0.11.0.1 + hooks: + - id: shellcheck + args: [--severity=warning] diff --git a/local-environment/scripts/localstack/deploy.sh b/local-environment/scripts/localstack/deploy.sh index d0502a1e..077ffe7b 100755 --- a/local-environment/scripts/localstack/deploy.sh +++ b/local-environment/scripts/localstack/deploy.sh @@ -1,9 +1,7 @@ #!/bin/bash set -ex -SCRIPT_DIR=$(dirname "$0") ENDPOINT_URL="http://localstack:4566" -ROLE_ARN="arn:aws:iam::000000000000:role/lambda-exec" export AWS_ACCESS_KEY_ID="test" export AWS_SECRET_ACCESS_KEY="test" diff --git a/local-environment/scripts/localstack/get_supplier_id.sh b/local-environment/scripts/localstack/get_supplier_id.sh index 3a5f892e..44b9fd21 100644 --- a/local-environment/scripts/localstack/get_supplier_id.sh +++ b/local-environment/scripts/localstack/get_supplier_id.sh @@ -1,7 +1,7 @@ #!/bin/bash set -e -for i in {1..10}; do +for _ in {1..10}; do SUPPLIER_ID=$(docker exec postgres-db psql "postgresql://app_user:STRONG_APP_PASSWORD@localhost:5432/local_hometest_db" -A -t -c "SET search_path TO hometest; SELECT supplier_id FROM supplier LIMIT 1;" 2>/dev/null | grep -v '^$' | grep -v '^SET$' | head -n 1 || echo "") if [[ -n "$SUPPLIER_ID" ]]; then echo "{\"supplier_id\": \"$SUPPLIER_ID\"}" From ce3a7234fd42be52f602f2785c57098ccfb183c9 Mon Sep 17 00:00:00 2001 From: Mikolaj Miotk Date: Wed, 18 Feb 2026 17:15:09 +0100 Subject: [PATCH 15/32] add pr title check --- .github/workflows/cicd-1-pull-request.yaml | 36 +++++++++++++++++++++- lambdas/package-lock.json | 12 -------- tests/package-lock.json | 7 ----- ui/package-lock.json | 21 ------------- 4 files changed, 35 insertions(+), 41 deletions(-) diff --git a/.github/workflows/cicd-1-pull-request.yaml b/.github/workflows/cicd-1-pull-request.yaml index 4ee1381d..f2e93196 100644 --- a/.github/workflows/cicd-1-pull-request.yaml +++ b/.github/workflows/cicd-1-pull-request.yaml @@ -8,9 +8,43 @@ on: branches: - "**" pull_request: - types: [opened, reopened] + types: [opened, reopened, edited, synchronize] jobs: + pr-title-check: + name: "Verify Jira ticket in PR" + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 1 + steps: + - name: "Check PR title for HOTE- Jira ID" + env: + PR_TITLE: ${{ github.event.pull_request.title }} + PR_BRANCH: ${{ github.head_ref }} + run: | + JIRA_PATTERN='HOTE-[0-9]+' + + echo "PR Title: $PR_TITLE" + echo "PR Branch: $PR_BRANCH" + + if [[ "$PR_TITLE" =~ $JIRA_PATTERN ]]; then + TICKET="${BASH_REMATCH[0]}" + echo "✅ Found Jira ticket '$TICKET' in PR title" + exit 0 + fi + + if [[ "$PR_BRANCH" =~ $JIRA_PATTERN ]]; then + TICKET="${BASH_REMATCH[0]}" + echo "⚠️ Jira ticket '$TICKET' found in branch name but NOT in PR title" + echo "Please include the Jira ticket ID in the PR title, e.g.: '$TICKET: '" + exit 1 + fi + + echo "❌ No Jira ticket ID found in PR title or branch name" + echo "" + echo "PR title must contain a Jira ticket ID matching pattern: HOTE-" + echo "Example: 'HOTE-123: Add user authentication'" + exit 1 metadata: name: "Set CI/CD metadata" runs-on: ubuntu-latest diff --git a/lambdas/package-lock.json b/lambdas/package-lock.json index aba13b3d..cdbff854 100644 --- a/lambdas/package-lock.json +++ b/lambdas/package-lock.json @@ -1007,7 +1007,6 @@ "version": "7.29.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -3872,7 +3871,6 @@ "node_modules/@middy/core": { "version": "6.4.5", "license": "MIT", - "peer": true, "engines": { "node": ">=20" }, @@ -4745,7 +4743,6 @@ "node_modules/@types/node": { "version": "25.1.0", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -4821,7 +4818,6 @@ "integrity": "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.55.0", "@typescript-eslint/types": "8.55.0", @@ -5307,7 +5303,6 @@ "version": "8.15.0", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5339,7 +5334,6 @@ "node_modules/ajv": { "version": "6.12.6", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5787,7 +5781,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6427,7 +6420,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7467,7 +7459,6 @@ "version": "30.2.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -8717,7 +8708,6 @@ "node_modules/pg": { "version": "8.16.3", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -9706,7 +9696,6 @@ "version": "10.9.2", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -9803,7 +9792,6 @@ "version": "5.9.3", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/tests/package-lock.json b/tests/package-lock.json index 41538814..e4efe39d 100644 --- a/tests/package-lock.json +++ b/tests/package-lock.json @@ -330,7 +330,6 @@ "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -561,7 +560,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -624,7 +622,6 @@ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==", "license": "MPL-2.0", - "peer": true, "engines": { "node": ">=4" } @@ -788,7 +785,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -1351,7 +1347,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -1383,7 +1378,6 @@ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", "license": "Apache-2.0", - "peer": true, "bin": { "playwright-core": "cli.js" }, @@ -1532,7 +1526,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/ui/package-lock.json b/ui/package-lock.json index f1ac0322..92a62bcd 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -111,7 +111,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -699,7 +698,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -723,7 +721,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2595,7 +2592,6 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -2875,7 +2871,6 @@ "integrity": "sha512-+0/4J266CBGPUq/ELg7QUHhN25WYjE0wYTPSQJn1xeu8DOlIOPxXxrNGiLmfAWl7HMMgWFWXpt9IDjMWrF5Iow==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2886,7 +2881,6 @@ "integrity": "sha512-tORuanb01iEzWvMGVGv2ZDhYZVeRMrw453DCSAIn/5yvcSVnMoUMTyf33nQJLahYEnv9xqrTNbgz4qY5EfSh0g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2897,7 +2891,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -2978,7 +2971,6 @@ "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", @@ -3485,7 +3477,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3897,7 +3888,6 @@ "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/types": "^7.26.0" } @@ -4006,7 +3996,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4911,7 +4900,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5097,7 +5085,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -7767,7 +7754,6 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -8547,7 +8533,6 @@ "resolved": "https://registry.npmjs.org/nhsuk-frontend/-/nhsuk-frontend-9.6.4.tgz", "integrity": "sha512-y0fi91jhgS1whD7jhNXKbpJ2Lmje/h5qBZ0aXmBbZdNo56805u7SsPJYxq7Uw6ffT86zQzQIxEwPwrjgSm5Whg==", "license": "MIT", - "peer": true, "workspaces": [ "." ], @@ -9200,7 +9185,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -9210,7 +9194,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -10298,7 +10281,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -10391,7 +10373,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -10592,7 +10573,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11206,7 +11186,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } From 7e79e0067f779c34ac35191132f23ec8c6be701c Mon Sep 17 00:00:00 2001 From: Mikolaj Miotk Date: Fri, 20 Feb 2026 09:28:58 +0100 Subject: [PATCH 16/32] cleanup v3 --- lambdas/package-lock.json | 11 ----------- ui/package-lock.json | 21 --------------------- 2 files changed, 32 deletions(-) diff --git a/lambdas/package-lock.json b/lambdas/package-lock.json index 62b69fd2..ae9a0a15 100644 --- a/lambdas/package-lock.json +++ b/lambdas/package-lock.json @@ -799,7 +799,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -4670,7 +4669,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.13.tgz", "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -4766,7 +4764,6 @@ "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -5299,7 +5296,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5741,7 +5737,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6440,7 +6435,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7476,7 +7470,6 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -8919,7 +8912,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz", "integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.11.0", "pg-pool": "^3.11.0", @@ -9905,7 +9897,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -10040,7 +10031,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -10147,7 +10137,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/ui/package-lock.json b/ui/package-lock.json index c81690d2..37b5b3da 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -111,7 +111,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -699,7 +698,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -723,7 +721,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2595,7 +2592,6 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -2875,7 +2871,6 @@ "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2886,7 +2881,6 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2897,7 +2891,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -2978,7 +2971,6 @@ "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -3485,7 +3477,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3897,7 +3888,6 @@ "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/types": "^7.26.0" } @@ -4006,7 +3996,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4911,7 +4900,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5044,7 +5032,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -7799,7 +7786,6 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -8579,7 +8565,6 @@ "resolved": "https://registry.npmjs.org/nhsuk-frontend/-/nhsuk-frontend-9.6.4.tgz", "integrity": "sha512-y0fi91jhgS1whD7jhNXKbpJ2Lmje/h5qBZ0aXmBbZdNo56805u7SsPJYxq7Uw6ffT86zQzQIxEwPwrjgSm5Whg==", "license": "MIT", - "peer": true, "workspaces": [ "." ], @@ -9251,7 +9236,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -9261,7 +9245,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -10349,7 +10332,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -10442,7 +10424,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -10643,7 +10624,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11257,7 +11237,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } From f28800696c3cccfe92bc625697ba40571cd90ac9 Mon Sep 17 00:00:00 2001 From: Mikolaj Miotk Date: Fri, 20 Feb 2026 13:22:51 +0100 Subject: [PATCH 17/32] Fix failing tests --- lambdas/package-lock.json | 11 +++++++ package.json | 2 +- tests/configuration/.env.local | 2 +- tests/fixtures/consoleErrorFixture.ts | 3 ++ tests/package-lock.json | 7 ++++ .../NHSLogin/NhsLoginConsentPage.ts | 33 +++++++++++++++++++ tests/page-objects/NhsLoginHelper.ts | 17 +++++++++- ui/package-lock.json | 21 ++++++++++++ ui/src/app.tsx | 1 + 9 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 tests/page-objects/NHSLogin/NhsLoginConsentPage.ts diff --git a/lambdas/package-lock.json b/lambdas/package-lock.json index ae9a0a15..62b69fd2 100644 --- a/lambdas/package-lock.json +++ b/lambdas/package-lock.json @@ -799,6 +799,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -4669,6 +4670,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.13.tgz", "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -4764,6 +4766,7 @@ "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -5296,6 +5299,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5737,6 +5741,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6435,6 +6440,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7470,6 +7476,7 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -8912,6 +8919,7 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz", "integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.11.0", "pg-pool": "^3.11.0", @@ -9897,6 +9905,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -10031,6 +10040,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -10137,6 +10147,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index f0229556..e4d6e99a 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "build:lambdas": "npm --prefix lambdas run build", "package:lambdas": "npm --prefix lambdas run package", - "clean:all": "npm run stop && rm -rf ./node_modules */node_modules */dist && docker system prune -f --volumes", + "clean:all": "npm run stop && rm -rf ./node_modules */node_modules */dist */build && docker system prune -f --volumes", "start": "npm ci && npm run local:start", "stop": "npm run local:stop", "local:start": "npm run local:backend:start && npm run local:terraform:init && npm run local:deploy && npm run local:frontend:start", diff --git a/tests/configuration/.env.local b/tests/configuration/.env.local index cbc13829..0716ea61 100644 --- a/tests/configuration/.env.local +++ b/tests/configuration/.env.local @@ -1,6 +1,6 @@ # Development Environment Configuration UI_BASE_URL=http://localhost:3000 -API_BASE_URL=http://localhost:4566/_aws/execute-api/hqf84ekgez/local +API_BASE_URL=http://localhost:4566/_aws/execute-api/pilma91vcg/local HEADLESS=true TIMEOUT=30000 SLOW_MO=0 diff --git a/tests/fixtures/consoleErrorFixture.ts b/tests/fixtures/consoleErrorFixture.ts index 2c65a508..d7d55866 100644 --- a/tests/fixtures/consoleErrorFixture.ts +++ b/tests/fixtures/consoleErrorFixture.ts @@ -48,6 +48,9 @@ const defaultOptions: ErrorCaptureOptions = { // CSP font violations from external NHS assets /Content Security Policy directive.*font-src/, /assets\.nhs\.uk.*font/i, + // React Router v7 warning: root route has a loader but no HydrateFallback defined + // This is a known issue to be fixed in the UI app (add HydrateFallback to root route) + /No `HydrateFallback` element provided to render during initial hydration/, ], ignoreStatusCodes: [], }; diff --git a/tests/package-lock.json b/tests/package-lock.json index da5fc6b6..664f91af 100644 --- a/tests/package-lock.json +++ b/tests/package-lock.json @@ -330,6 +330,7 @@ "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -560,6 +561,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -622,6 +624,7 @@ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==", "license": "MPL-2.0", + "peer": true, "engines": { "node": ">=4" } @@ -785,6 +788,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -1347,6 +1351,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -1378,6 +1383,7 @@ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", "license": "Apache-2.0", + "peer": true, "bin": { "playwright-core": "cli.js" }, @@ -1526,6 +1532,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/tests/page-objects/NHSLogin/NhsLoginConsentPage.ts b/tests/page-objects/NHSLogin/NhsLoginConsentPage.ts new file mode 100644 index 00000000..cc907742 --- /dev/null +++ b/tests/page-objects/NHSLogin/NhsLoginConsentPage.ts @@ -0,0 +1,33 @@ +import { type Locator, type Page } from '@playwright/test'; + +export class NhsLoginConsentPage { + readonly page: Page; + // NHS Login consent page has an "agree" submit button — try common text variants + readonly agreeToShareBtn: Locator; + readonly continueBtn: Locator; + readonly doNotAgreeToShareInformationLink: Locator; + + constructor(page: Page) { + this.page = page; + this.agreeToShareBtn = page.getByRole('button', { + name: /I agree to share this information/i + }); + // Fallback: some NHS Login consent pages just have a "Continue" button to accept + this.continueBtn = page.locator('button[type="submit"]'); + this.doNotAgreeToShareInformationLink = page.getByRole('link', { + name: /I do not agree/i + }); + } + + async agreeAndContinue(): Promise { + // Wait for the page to fully load (do-not-agree link is the stable indicator) + await this.doNotAgreeToShareInformationLink.waitFor({ timeout: 15000 }); + + if (await this.agreeToShareBtn.isVisible()) { + await this.agreeToShareBtn.click(); + } else { + // Fall back to submit button (the main CTA that accepts consent) + await this.continueBtn.click(); + } + } +} diff --git a/tests/page-objects/NhsLoginHelper.ts b/tests/page-objects/NhsLoginHelper.ts index 08030837..0b73e791 100644 --- a/tests/page-objects/NhsLoginHelper.ts +++ b/tests/page-objects/NhsLoginHelper.ts @@ -3,6 +3,7 @@ import { ConfigFactory, type ConfigInterface } from '../configuration/configurat import type { NHSLoginUser } from '../utils/users/BaseUser'; import { NHSEmailAndPasswordPage } from './NHSLogin/NHSEmailAndPasswordPage'; import { CodeSecurityPage } from './NHSLogin/CodeSecurityPage'; +import { NhsLoginConsentPage } from './NHSLogin/NhsLoginConsentPage'; export default class NhsLoginHelper { @@ -18,6 +19,7 @@ export default class NhsLoginHelper { ): Promise { const loginPage = new NHSEmailAndPasswordPage(page); const codeSecurityPage = new CodeSecurityPage(page); + const consentPage = new NhsLoginConsentPage(page); // Navigate to UI which will redirect through HomePage → LoginPage → NHS Login await page.goto(`${this.config.uiBaseUrl}`); @@ -31,7 +33,20 @@ export default class NhsLoginHelper { await codeSecurityPage.fillAuthOneTimePasswordAndClickContinue( nhsLoginUser.otp ); - await page.waitForURL('**/get-self-test-kit-for-HIV'); + + // Handle NHS Login consent page if it appears (first login or expired consent) + // Race between consent page and direct app redirect — whichever arrives first + const consentAppeared = await Promise.race([ + page.waitForURL(/nhs-login-consent/, { timeout: 15000 }).then(() => true), + page.waitForURL('**/get-self-test-kit-for-HIV', { timeout: 15000 }).then(() => false) + ]).catch(() => false); + + if (consentAppeared) { + console.log('Consent page detected, agreeing to share information...'); + await consentPage.agreeAndContinue(); + } + + await page.waitForURL('**/get-self-test-kit-for-HIV', { timeout: 60000 }); } public async loginNhsUser(page: Page, user: NHSLoginUser): Promise { diff --git a/ui/package-lock.json b/ui/package-lock.json index 37b5b3da..c81690d2 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -111,6 +111,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -698,6 +699,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -721,6 +723,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2592,6 +2595,7 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -2871,6 +2875,7 @@ "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2881,6 +2886,7 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2891,6 +2897,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -2971,6 +2978,7 @@ "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -3477,6 +3485,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3888,6 +3897,7 @@ "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/types": "^7.26.0" } @@ -3996,6 +4006,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4900,6 +4911,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5032,6 +5044,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -7786,6 +7799,7 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -8565,6 +8579,7 @@ "resolved": "https://registry.npmjs.org/nhsuk-frontend/-/nhsuk-frontend-9.6.4.tgz", "integrity": "sha512-y0fi91jhgS1whD7jhNXKbpJ2Lmje/h5qBZ0aXmBbZdNo56805u7SsPJYxq7Uw6ffT86zQzQIxEwPwrjgSm5Whg==", "license": "MIT", + "peer": true, "workspaces": [ "." ], @@ -9236,6 +9251,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -9245,6 +9261,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -10332,6 +10349,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -10424,6 +10442,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -10624,6 +10643,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11237,6 +11257,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/ui/src/app.tsx b/ui/src/app.tsx index f033165d..9e9ee330 100644 --- a/ui/src/app.tsx +++ b/ui/src/app.tsx @@ -60,6 +60,7 @@ const router = createBrowserRouter([ path: "/", element: , loader: requireAuth, + HydrateFallback: () => null, errorElement: ( From 1fbba35cc595213f79e7e3163ff4fda50460e027 Mon Sep 17 00:00:00 2001 From: Mikolaj Miotk Date: Fri, 20 Feb 2026 13:58:48 +0100 Subject: [PATCH 18/32] name tags missing v2 --- package.json | 2 +- tests/page-objects/HomeTestStartPage.ts | 5 +++++ tests/tests/ui/HomeTestStartPage.spec.ts | 10 ++++++---- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index e4d6e99a..c1f664bb 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "postinstall": "npm --prefix ui install && npm --prefix lambdas install && npm --prefix tests install", "test": "npm --prefix ui run test && npm --prefix lambdas run test", - "test:playwright": "ENV=dev UI_BASE_URL=$(terraform -chdir=local-environment/infra output -raw ui_url) API_BASE_URL=$(terraform -chdir=local-environment/infra output -raw api_base_url) npm --prefix tests run test:chrome", + "test:playwright": "HEADLESS=false ENV=local UI_BASE_URL=$(terraform -chdir=local-environment/infra output -raw ui_url) API_BASE_URL=$(terraform -chdir=local-environment/infra output -raw api_base_url) npm --prefix tests run test:chrome", "build:lambdas": "npm --prefix lambdas run build", "package:lambdas": "npm --prefix lambdas run package", diff --git a/tests/page-objects/HomeTestStartPage.ts b/tests/page-objects/HomeTestStartPage.ts index 598a2801..73816790 100644 --- a/tests/page-objects/HomeTestStartPage.ts +++ b/tests/page-objects/HomeTestStartPage.ts @@ -27,6 +27,11 @@ export class HomeTestStartPage extends BasePage { await this.page.goto(`${this.config.uiBaseUrl}/get-self-test-kit-for-HIV`); } + async waitUntilPageLoad(): Promise { + // Wait for the requireAuth loader to complete and the page to render + await this.headerText.waitFor({ timeout: 30000 }); + } + async clickFindClinicLink(expectedUrl: string): Promise { await this.findClinicLink.click(); await this.page.waitForURL(expectedUrl); diff --git a/tests/tests/ui/HomeTestStartPage.spec.ts b/tests/tests/ui/HomeTestStartPage.spec.ts index e4eb09d4..d0d36756 100644 --- a/tests/tests/ui/HomeTestStartPage.spec.ts +++ b/tests/tests/ui/HomeTestStartPage.spec.ts @@ -21,22 +21,24 @@ test.describe('HIV Start Test Page', () => { // Test "Find a sexual health clinic" link await homeTestStartPage.clickFindClinicLink(sexualHealthClinicUrl); expect(homeTestStartPage.page.url()).toBe(sexualHealthClinicUrl); - await homeTestStartPage.page.goBack(); + await homeTestStartPage.navigate(); + await homeTestStartPage.waitUntilPageLoad(); // Test "your nearest A&E" link await homeTestStartPage.clickNearestAELink(nearestAEUrl); expect(homeTestStartPage.page.url()).toBe(nearestAEUrl); - await homeTestStartPage.page.goBack(); + await homeTestStartPage.navigate(); + await homeTestStartPage.waitUntilPageLoad(); // Test "your nearest sexual health clinic" link await homeTestStartPage.clickNearestSexualHealthClinicLink(sexualHealthClinicUrl); expect(homeTestStartPage.page.url()).toBe(sexualHealthClinicUrl); - await homeTestStartPage.page.goBack(); + await homeTestStartPage.navigate(); + await homeTestStartPage.waitUntilPageLoad(); // Test "Learn more about HIV and AIDS" link await homeTestStartPage.clickLearnMoreHIVAidsLink(hivAidsInfoUrl); expect(homeTestStartPage.page.url()).toBe(hivAidsInfoUrl); - await homeTestStartPage.page.goBack(); }); }); From 703ae3c31c613240a59641ffb551cd166edd463f Mon Sep 17 00:00:00 2001 From: Mikolaj Miotk Date: Fri, 20 Feb 2026 14:04:31 +0100 Subject: [PATCH 19/32] name tags missing v3 --- lambdas/package-lock.json | 11 ----------- tests/package-lock.json | 7 ------- ui/package-lock.json | 21 --------------------- 3 files changed, 39 deletions(-) diff --git a/lambdas/package-lock.json b/lambdas/package-lock.json index 62b69fd2..ae9a0a15 100644 --- a/lambdas/package-lock.json +++ b/lambdas/package-lock.json @@ -799,7 +799,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -4670,7 +4669,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.13.tgz", "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -4766,7 +4764,6 @@ "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -5299,7 +5296,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5741,7 +5737,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6440,7 +6435,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7476,7 +7470,6 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -8919,7 +8912,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz", "integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.11.0", "pg-pool": "^3.11.0", @@ -9905,7 +9897,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -10040,7 +10031,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -10147,7 +10137,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/tests/package-lock.json b/tests/package-lock.json index 664f91af..da5fc6b6 100644 --- a/tests/package-lock.json +++ b/tests/package-lock.json @@ -330,7 +330,6 @@ "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -561,7 +560,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -624,7 +622,6 @@ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==", "license": "MPL-2.0", - "peer": true, "engines": { "node": ">=4" } @@ -788,7 +785,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -1351,7 +1347,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -1383,7 +1378,6 @@ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", "license": "Apache-2.0", - "peer": true, "bin": { "playwright-core": "cli.js" }, @@ -1532,7 +1526,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/ui/package-lock.json b/ui/package-lock.json index c81690d2..37b5b3da 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -111,7 +111,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -699,7 +698,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -723,7 +721,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2595,7 +2592,6 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -2875,7 +2871,6 @@ "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2886,7 +2881,6 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2897,7 +2891,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -2978,7 +2971,6 @@ "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -3485,7 +3477,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3897,7 +3888,6 @@ "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/types": "^7.26.0" } @@ -4006,7 +3996,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4911,7 +4900,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5044,7 +5032,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -7799,7 +7786,6 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -8579,7 +8565,6 @@ "resolved": "https://registry.npmjs.org/nhsuk-frontend/-/nhsuk-frontend-9.6.4.tgz", "integrity": "sha512-y0fi91jhgS1whD7jhNXKbpJ2Lmje/h5qBZ0aXmBbZdNo56805u7SsPJYxq7Uw6ffT86zQzQIxEwPwrjgSm5Whg==", "license": "MIT", - "peer": true, "workspaces": [ "." ], @@ -9251,7 +9236,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -9261,7 +9245,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -10349,7 +10332,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -10442,7 +10424,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -10643,7 +10624,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11257,7 +11237,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } From c142a558d932e4845f86a10364ec1f9371658278 Mon Sep 17 00:00:00 2001 From: Piotr Kasperski Date: Tue, 3 Mar 2026 16:15:39 +0100 Subject: [PATCH 20/32] fix: correct import path for NhsLoginHelper in SandBoxUserManager --- tests/utils/users/SandBoxUserManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils/users/SandBoxUserManager.ts b/tests/utils/users/SandBoxUserManager.ts index 6e0bfccc..36048ad8 100644 --- a/tests/utils/users/SandBoxUserManager.ts +++ b/tests/utils/users/SandBoxUserManager.ts @@ -1,7 +1,7 @@ import { BaseUserManager } from "./BaseUserManager"; import { ConfigFactory } from "../../configuration/EnvironmentConfiguration"; import type { NHSLoginUser } from "./BaseUser"; -import NhsLoginHelper from "../../page-objects/NHSLoginHelper"; +import NhsLoginHelper from "../../page-objects/NhsLoginHelper"; import type { Page } from "@playwright/test"; export class SandBoxUserManager extends BaseUserManager { From f15ccdf2e236acbe53458e426d78cf0475eb5df6 Mon Sep 17 00:00:00 2001 From: Piotr Kasperski Date: Tue, 3 Mar 2026 16:27:01 +0100 Subject: [PATCH 21/32] fix: update Playwright command to use TARGET_ENV and set default API/UI base URLs in .env.dev --- .github/workflows/playwright-e2e.yaml | 4 ++-- tests/configuration/.env.dev | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/playwright-e2e.yaml b/.github/workflows/playwright-e2e.yaml index de26a2df..8e4cf477 100644 --- a/.github/workflows/playwright-e2e.yaml +++ b/.github/workflows/playwright-e2e.yaml @@ -124,8 +124,8 @@ jobs: BROWSER="${BROWSER:-chromium}" FILTER="${{ inputs.test_filter }}" - # Build the command - CMD="HEADLESS=true ENV=dev npx playwright test" + # Build the command - use TARGET_ENV to match the environment being tested + CMD="HEADLESS=true ENV=$TARGET_ENV npx playwright test" # Add browser project if [ "$BROWSER" != "all" ]; then diff --git a/tests/configuration/.env.dev b/tests/configuration/.env.dev index 2def9ad7..f9c907b7 100644 --- a/tests/configuration/.env.dev +++ b/tests/configuration/.env.dev @@ -1,6 +1,7 @@ # Development Environment Configuration -UI_BASE_URL= -API_BASE_URL= +# These default URLs will be overridden by CI environment variables when needed +UI_BASE_URL=https://dev.hometest.service.nhs.uk +API_BASE_URL=https://dev.hometest.service.nhs.uk/ HEADLESS=false TIMEOUT=30000 SLOW_MO=0 From 7b368d62e33f77caa74f8d73f7509c5a667b86f0 Mon Sep 17 00:00:00 2001 From: Piotr Kasperski Date: Wed, 4 Mar 2026 10:10:09 +0100 Subject: [PATCH 22/32] fix: update default environment to 'dev' in Playwright E2E workflow --- .github/workflows/playwright-e2e.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/playwright-e2e.yaml b/.github/workflows/playwright-e2e.yaml index 8e4cf477..2897aa64 100644 --- a/.github/workflows/playwright-e2e.yaml +++ b/.github/workflows/playwright-e2e.yaml @@ -1,6 +1,6 @@ --- name: "Playwright E2E Tests" -run-name: "E2E Tests: env: ${{ inputs.environment || 'local' }}, browser: ${{ inputs.browser || 'chromium' }}, filter: ${{ inputs.test_filter && format(' - {0}', inputs.test_filter) || '' }}" +run-name: "E2E Tests: env: ${{ inputs.environment || 'dev' }}, browser: ${{ inputs.browser || 'chromium' }}, filter: ${{ inputs.test_filter && format(' - {0}', inputs.test_filter) || '' }}" on: schedule: @@ -10,7 +10,7 @@ on: environment: description: "Target environment" required: false - default: "local" + default: "dev" type: choice options: - local @@ -37,7 +37,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 env: - TARGET_ENV: ${{ inputs.environment || 'local' }} + TARGET_ENV: ${{ inputs.environment || 'dev' }} steps: - name: "Checkout code" uses: actions/checkout@v6 From c2c63dd2ab53d4980c569ceb85e751034d76b767 Mon Sep 17 00:00:00 2001 From: Piotr Kasperski Date: Thu, 12 Mar 2026 09:20:05 +0100 Subject: [PATCH 23/32] wait 15 minutes before start testing --- .github/workflows/playwright-e2e.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/playwright-e2e.yaml b/.github/workflows/playwright-e2e.yaml index 2897aa64..dde33fbd 100644 --- a/.github/workflows/playwright-e2e.yaml +++ b/.github/workflows/playwright-e2e.yaml @@ -101,7 +101,7 @@ jobs: run: | UI_URL="${{ steps.urls.outputs.ui_url }}" echo "Waiting for UI to be reachable at $UI_URL..." - timeout=120 + timeout=900 elapsed=0 until curl -sf "$UI_URL" > /dev/null 2>&1; do if [ $elapsed -ge $timeout ]; then From 78451c8467d9e30d9dadbfbbaad353f304077ec6 Mon Sep 17 00:00:00 2001 From: Piotr Kasperski Date: Thu, 12 Mar 2026 09:21:34 +0100 Subject: [PATCH 24/32] wait 15 minutes before start testing --- .github/workflows/playwright-e2e.yaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/playwright-e2e.yaml b/.github/workflows/playwright-e2e.yaml index dde33fbd..584815aa 100644 --- a/.github/workflows/playwright-e2e.yaml +++ b/.github/workflows/playwright-e2e.yaml @@ -101,7 +101,7 @@ jobs: run: | UI_URL="${{ steps.urls.outputs.ui_url }}" echo "Waiting for UI to be reachable at $UI_URL..." - timeout=900 + timeout=120 elapsed=0 until curl -sf "$UI_URL" > /dev/null 2>&1; do if [ $elapsed -ge $timeout ]; then @@ -117,6 +117,12 @@ jobs: done echo "UI is reachable!" + - name: "Wait 15 minutes before running tests" + run: | + echo "Waiting 15 minutes before starting Playwright tests..." + sleep 900 + echo "Wait complete, starting tests now" + - name: "Run Playwright tests" working-directory: tests run: | From 814a444180c40ef829ecad558e28783870774331 Mon Sep 17 00:00:00 2001 From: Piotr Kasperski Date: Thu, 12 Mar 2026 09:32:32 +0100 Subject: [PATCH 25/32] Revert "wait 15 minutes before start testing" This reverts commit 78451c8467d9e30d9dadbfbbaad353f304077ec6. --- .github/workflows/playwright-e2e.yaml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/playwright-e2e.yaml b/.github/workflows/playwright-e2e.yaml index 584815aa..dde33fbd 100644 --- a/.github/workflows/playwright-e2e.yaml +++ b/.github/workflows/playwright-e2e.yaml @@ -101,7 +101,7 @@ jobs: run: | UI_URL="${{ steps.urls.outputs.ui_url }}" echo "Waiting for UI to be reachable at $UI_URL..." - timeout=120 + timeout=900 elapsed=0 until curl -sf "$UI_URL" > /dev/null 2>&1; do if [ $elapsed -ge $timeout ]; then @@ -117,12 +117,6 @@ jobs: done echo "UI is reachable!" - - name: "Wait 15 minutes before running tests" - run: | - echo "Waiting 15 minutes before starting Playwright tests..." - sleep 900 - echo "Wait complete, starting tests now" - - name: "Run Playwright tests" working-directory: tests run: | From 9a0a04e37d396194b55dc053fd3599f72e2548e1 Mon Sep 17 00:00:00 2001 From: Piotr Kasperski Date: Thu, 12 Mar 2026 09:34:17 +0100 Subject: [PATCH 26/32] Revert "wait 15 minutes before start testing" This reverts commit c2c63dd2ab53d4980c569ceb85e751034d76b767. --- .github/workflows/playwright-e2e.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/playwright-e2e.yaml b/.github/workflows/playwright-e2e.yaml index dde33fbd..2897aa64 100644 --- a/.github/workflows/playwright-e2e.yaml +++ b/.github/workflows/playwright-e2e.yaml @@ -101,7 +101,7 @@ jobs: run: | UI_URL="${{ steps.urls.outputs.ui_url }}" echo "Waiting for UI to be reachable at $UI_URL..." - timeout=900 + timeout=120 elapsed=0 until curl -sf "$UI_URL" > /dev/null 2>&1; do if [ $elapsed -ge $timeout ]; then From 9fe732ee06236c6ba33d5eb321d4f7895fdcd528 Mon Sep 17 00:00:00 2001 From: Piotr Kasperski Date: Thu, 12 Mar 2026 10:48:16 +0100 Subject: [PATCH 27/32] Revert "fix: update default environment to 'dev' in Playwright E2E workflow" This reverts commit 7b368d62e33f77caa74f8d73f7509c5a667b86f0. --- .github/workflows/playwright-e2e.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/playwright-e2e.yaml b/.github/workflows/playwright-e2e.yaml index 2897aa64..8e4cf477 100644 --- a/.github/workflows/playwright-e2e.yaml +++ b/.github/workflows/playwright-e2e.yaml @@ -1,6 +1,6 @@ --- name: "Playwright E2E Tests" -run-name: "E2E Tests: env: ${{ inputs.environment || 'dev' }}, browser: ${{ inputs.browser || 'chromium' }}, filter: ${{ inputs.test_filter && format(' - {0}', inputs.test_filter) || '' }}" +run-name: "E2E Tests: env: ${{ inputs.environment || 'local' }}, browser: ${{ inputs.browser || 'chromium' }}, filter: ${{ inputs.test_filter && format(' - {0}', inputs.test_filter) || '' }}" on: schedule: @@ -10,7 +10,7 @@ on: environment: description: "Target environment" required: false - default: "dev" + default: "local" type: choice options: - local @@ -37,7 +37,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 env: - TARGET_ENV: ${{ inputs.environment || 'dev' }} + TARGET_ENV: ${{ inputs.environment || 'local' }} steps: - name: "Checkout code" uses: actions/checkout@v6 From 513d78004235d308ea9a9d9845edcdfd2f216a3e Mon Sep 17 00:00:00 2001 From: Piotr Kasperski Date: Thu, 12 Mar 2026 10:48:42 +0100 Subject: [PATCH 28/32] Revert "fix: update Playwright command to use TARGET_ENV and set default API/UI base URLs in .env.dev" This reverts commit f15ccdf2e236acbe53458e426d78cf0475eb5df6. --- .github/workflows/playwright-e2e.yaml | 4 ++-- tests/configuration/.env.dev | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/playwright-e2e.yaml b/.github/workflows/playwright-e2e.yaml index 8e4cf477..de26a2df 100644 --- a/.github/workflows/playwright-e2e.yaml +++ b/.github/workflows/playwright-e2e.yaml @@ -124,8 +124,8 @@ jobs: BROWSER="${BROWSER:-chromium}" FILTER="${{ inputs.test_filter }}" - # Build the command - use TARGET_ENV to match the environment being tested - CMD="HEADLESS=true ENV=$TARGET_ENV npx playwright test" + # Build the command + CMD="HEADLESS=true ENV=dev npx playwright test" # Add browser project if [ "$BROWSER" != "all" ]; then diff --git a/tests/configuration/.env.dev b/tests/configuration/.env.dev index f9c907b7..2def9ad7 100644 --- a/tests/configuration/.env.dev +++ b/tests/configuration/.env.dev @@ -1,7 +1,6 @@ # Development Environment Configuration -# These default URLs will be overridden by CI environment variables when needed -UI_BASE_URL=https://dev.hometest.service.nhs.uk -API_BASE_URL=https://dev.hometest.service.nhs.uk/ +UI_BASE_URL= +API_BASE_URL= HEADLESS=false TIMEOUT=30000 SLOW_MO=0 From cecee7c0db72ef88a8b413bc2f554b38671548ae Mon Sep 17 00:00:00 2001 From: Przemyslaw Date: Thu, 12 Mar 2026 11:07:32 +0100 Subject: [PATCH 29/32] feat: enhance NHS login flow with detailed logging and network error capture --- .github/workflows/cicd-1-pull-request.yaml | 1 - tests/configuration/.env.local | 2 +- tests/fixtures/consoleErrorFixture.ts | 2 +- tests/page-objects/NhsLoginHelper.ts | 68 +++++++++++++++++++++- tests/utils/users/BaseUserManager.ts | 5 +- 5 files changed, 72 insertions(+), 6 deletions(-) diff --git a/.github/workflows/cicd-1-pull-request.yaml b/.github/workflows/cicd-1-pull-request.yaml index 59dc93dd..726976bd 100644 --- a/.github/workflows/cicd-1-pull-request.yaml +++ b/.github/workflows/cicd-1-pull-request.yaml @@ -9,7 +9,6 @@ on: - "main" pull_request: types: [opened, reopened, edited, synchronize] - types: [opened, reopened, edited, synchronize] jobs: pr-title-check: diff --git a/tests/configuration/.env.local b/tests/configuration/.env.local index 935aa129..4ebdf403 100644 --- a/tests/configuration/.env.local +++ b/tests/configuration/.env.local @@ -1,6 +1,6 @@ # Development Environment Configuration UI_BASE_URL=http://localhost:3000 -API_BASE_URL=http://localhost:4566/_aws/execute-api/5yjxchqjgy/local +API_BASE_URL=http://localhost:4566/_aws/execute-api/gppofwpzq9/local HEADLESS=true TIMEOUT=30000 SLOW_MO=0 diff --git a/tests/fixtures/consoleErrorFixture.ts b/tests/fixtures/consoleErrorFixture.ts index 6084f0ac..637e46c1 100644 --- a/tests/fixtures/consoleErrorFixture.ts +++ b/tests/fixtures/consoleErrorFixture.ts @@ -50,7 +50,7 @@ const defaultOptions: ErrorCaptureOptions = { /assets\.nhs\.uk.*font/i, // React Router v7 warning: root route has a loader but no HydrateFallback defined // This is a known issue to be fixed in the UI app (add HydrateFallback to root route) - /No `HydrateFallback` element provided to render during initial hydration/,, + /No `HydrateFallback` element provided to render during initial hydration/, ], ignoreStatusCodes: [], }; diff --git a/tests/page-objects/NhsLoginHelper.ts b/tests/page-objects/NhsLoginHelper.ts index ebb209a3..d16a617f 100644 --- a/tests/page-objects/NhsLoginHelper.ts +++ b/tests/page-objects/NhsLoginHelper.ts @@ -7,6 +7,7 @@ import type { NHSLoginUser } from '../utils/users/BaseUser'; import { NHSEmailAndPasswordPage } from './NHSLogin/NHSEmailAndPasswordPage'; import { CodeSecurityPage } from './NHSLogin/CodeSecurityPage'; import { NhsLoginConsentPage } from './NHSLogin/NhsLoginConsentPage'; +import * as fs from 'fs'; export default class NhsLoginHelper { readonly config: ConfigInterface; @@ -22,28 +23,91 @@ export default class NhsLoginHelper { const codeSecurityPage = new CodeSecurityPage(page); const consentPage = new NhsLoginConsentPage(page); + // Log file for debugging + const logFile = 'testResults/nhs-login-debug.log'; + fs.mkdirSync('testResults', { recursive: true }); + const log = (message: string) => { + const timestamp = new Date().toISOString(); + const logMessage = `[${timestamp}] ${message}\n`; + console.log(message); // Also log to console + fs.appendFileSync(logFile, logMessage); + }; + + log('=== NHS Login Flow Started ==='); + + // Capture all browser console messages + page.on('console', (msg) => { + const type = msg.type(); + const text = msg.text(); + const location = msg.location(); + + log(`[Browser Console - ${type.toUpperCase()}] ${text}`); + if (location.url) { + log(` ↳ ${location.url}:${location.lineNumber}:${location.columnNumber}`); + } + }); + + // Capture all network requests and responses + page.on('request', (request) => { + log(`[Network Request] ${request.method()} ${request.url()}`); + const postData = request.postData(); + if (postData) { + log(` ↳ Body: ${postData.substring(0, 200)}${postData.length > 200 ? '...' : ''}`); + } + }); + + page.on('response', async (response) => { + const status = response.status(); + const statusText = response.statusText(); + const url = response.url(); + + log(`[Network Response] ${status} ${statusText} - ${response.request().method()} ${url}`); + + // Log response body for non-2xx responses or specific content types + if (status >= 400 || url.includes('/api/')) { + try { + const contentType = response.headers()['content-type'] || ''; + if (contentType.includes('application/json')) { + const body = await response.text(); + log(` ↳ Response Body: ${body.substring(0, 500)}${body.length > 500 ? '...' : ''}`); + } + } catch (error) { + log(` ↳ Could not read response body: ${error}`); + } + } + }); + + log(`Navigating to: ${this.config.uiBaseUrl}`); await page.goto(`${this.config.uiBaseUrl}`); await page.waitForURL(/signin\.nhs\.uk/, { timeout: 60000 }); - console.log(`Redirected to NHS Login: ${page.url()}`); + log(`Redirected to NHS Login: ${page.url()}`); + log('Filling login credentials...'); await loginPage.fillAuthFormWithCredentialsAndClickContinue(nhsLoginUser); + + log('Entering OTP code...'); await codeSecurityPage.fillAuthOneTimePasswordAndClickContinue( nhsLoginUser.otp ); // Handle NHS Login consent page if it appears (first login or expired consent) // Race between consent page and direct app redirect — whichever arrives first + log('Waiting for consent page or redirect...'); const consentAppeared = await Promise.race([ page.waitForURL(/nhs-login-consent/, { timeout: 15000 }).then(() => true), page.waitForURL('**/get-self-test-kit-for-HIV', { timeout: 15000 }).then(() => false) ]).catch(() => false); if (consentAppeared) { - console.log('Consent page detected, agreeing to share information...'); + log('Consent page detected, agreeing to share information...'); await consentPage.agreeAndContinue(); + } else { + log('No consent page - direct redirect to app'); } + log('Waiting for final redirect to app...'); await page.waitForURL('**/get-self-test-kit-for-HIV', { timeout: 60000 }); + log('=== NHS Login Flow Completed Successfully ==='); } public async loginNhsUser(page: Page, user: NHSLoginUser): Promise { diff --git a/tests/utils/users/BaseUserManager.ts b/tests/utils/users/BaseUserManager.ts index 204de9bb..c7399c47 100644 --- a/tests/utils/users/BaseUserManager.ts +++ b/tests/utils/users/BaseUserManager.ts @@ -14,8 +14,10 @@ interface NetworkError { statusText: string; method: string; timestamp: string; + responseText: Promise; } + export abstract class BaseUserManager { protected readonly workerUsers: TUser[]; private readonly numberOfWorkerUsers: number; @@ -86,6 +88,7 @@ export abstract class BaseUserManager { statusText: response.statusText(), method: response.request().method(), timestamp: new Date().toISOString(), + responseText: response.text().catch(() => "Unable to retrieve response body"), }); console.error( `❌ HTTP ${status} ${response.statusText()}: ${response.request().method()} ${response.url()}`, @@ -200,7 +203,7 @@ export abstract class BaseUserManager { console.error(`\n🌐 Network errors detected during setup:`); networkErrors.forEach((err, idx) => { console.error( - ` ${idx + 1}. ${err.method} ${err.url} => ${err.status} ${err.statusText}`, + ` ${idx + 1}. ${err.method}, ${err.url} => ${err.status} ${err.statusText}`, ); }); } From 6300abb2899f07e51a0b413a719e7de1cf3feb6b Mon Sep 17 00:00:00 2001 From: Przemyslaw Date: Thu, 12 Mar 2026 12:04:18 +0100 Subject: [PATCH 30/32] feat: add logging for successful worker user login and session state saving --- tests/utils/users/BaseUserManager.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/utils/users/BaseUserManager.ts b/tests/utils/users/BaseUserManager.ts index c7399c47..dd4958f3 100644 --- a/tests/utils/users/BaseUserManager.ts +++ b/tests/utils/users/BaseUserManager.ts @@ -210,10 +210,12 @@ export abstract class BaseUserManager { await this.captureFailureArtifacts(page, context, user, error as Error, networkErrors); await browser.close(); + console.log(`✅ Successfully logged in worker user ${user.nhsNumber} and saved session state. CATCH`); throw error; } await browser.close(); + console.log(`✅ Successfully logged in worker user ${user.nhsNumber} and saved session state.`); } } getSpecialUser(key: SpecialUserKey): TUser { From 4a156f4d3c54ea1a8c450925b5eb465d3d41462b Mon Sep 17 00:00:00 2001 From: Piotr Kasperski Date: Thu, 12 Mar 2026 12:47:37 +0100 Subject: [PATCH 31/32] Start application on dev too --- .github/workflows/playwright-e2e.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/playwright-e2e.yaml b/.github/workflows/playwright-e2e.yaml index de26a2df..8efdae17 100644 --- a/.github/workflows/playwright-e2e.yaml +++ b/.github/workflows/playwright-e2e.yaml @@ -65,18 +65,18 @@ jobs: EOF - name: "Start the application" - if: env.TARGET_ENV == 'local' + if: env.TARGET_ENV == 'local' || env.TARGET_ENV == 'dev' run: | npm run start - name: "Show application status" - if: env.TARGET_ENV == 'local' + if: env.TARGET_ENV == 'local' || env.TARGET_ENV == 'dev' run: | docker compose -f local-environment/docker-compose.yml ps docker logs ui - name: "Get terraform outputs" - if: env.TARGET_ENV == 'local' + if: env.TARGET_ENV == 'local' || env.TARGET_ENV == 'dev' id: terraform run: | UI_URL=$(terraform -chdir=local-environment/infra output -raw ui_url) @@ -106,7 +106,7 @@ jobs: until curl -sf "$UI_URL" > /dev/null 2>&1; do if [ $elapsed -ge $timeout ]; then echo "Timeout: UI not reachable after ${timeout}s" - if [ "$TARGET_ENV" == "local" ]; then + if [ "$TARGET_ENV" == "local" ] || [ "$TARGET_ENV" == "dev" ]; then docker logs ui fi exit 1 @@ -146,7 +146,7 @@ jobs: API_BASE_URL: ${{ steps.urls.outputs.api_url }} - name: "Grab docker compose logs" - if: always() && env.TARGET_ENV == 'local' + if: always() && (env.TARGET_ENV == 'local' || env.TARGET_ENV == 'dev') run: | for service in $(docker compose -f local-environment/docker-compose.yml ps --services); do docker compose -f local-environment/docker-compose.yml logs "$service" > "tests/testResults/docker-compose-${service}.log" 2>&1 From bcc44f42d0c911a125a869050fa56a4f080b3721 Mon Sep 17 00:00:00 2001 From: Przemyslaw Date: Thu, 12 Mar 2026 14:17:53 +0100 Subject: [PATCH 32/32] Resolving conflicts --- tests/tests/ui/HomeTestStartPage.spec.ts | 42 ------------------------ 1 file changed, 42 deletions(-) delete mode 100644 tests/tests/ui/HomeTestStartPage.spec.ts diff --git a/tests/tests/ui/HomeTestStartPage.spec.ts b/tests/tests/ui/HomeTestStartPage.spec.ts deleted file mode 100644 index 40e8d35b..00000000 --- a/tests/tests/ui/HomeTestStartPage.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { test } from '../../fixtures'; -import { expect } from '@playwright/test'; -import { config, EnvironmentVariables } from '../../configuration'; - - -test.describe.configure({ mode: 'serial' }); - -test.describe('HIV Start Test Page', () => { - test.beforeEach(async ({ homeTestStartPage }) => { - await homeTestStartPage.navigate(); - }); - - test('Opening external links', async ({ homeTestStartPage }) => { - await homeTestStartPage.waitUntilPageLoad(); - - const sexualHealthClinicUrl = config.get(EnvironmentVariables.EXTERNAL_LINK_SEXUAL_HEALTH_CLINIC); - const nearestAEUrl = config.get(EnvironmentVariables.EXTERNAL_LINK_NEAREST_AE); - const hivAidsInfoUrl = config.get(EnvironmentVariables.EXTERNAL_LINK_HIV_AIDS_INFO); - - // Test "Find a sexual health clinic" link - await homeTestStartPage.clickFindClinicLink(sexualHealthClinicUrl); - expect(homeTestStartPage.page.url()).toBe(sexualHealthClinicUrl); - await homeTestStartPage.navigate(); - await homeTestStartPage.waitUntilPageLoad(); - - // Test "your nearest A&E" link - await homeTestStartPage.clickNearestAELink(nearestAEUrl); - expect(homeTestStartPage.page.url()).toBe(nearestAEUrl); - await homeTestStartPage.navigate(); - await homeTestStartPage.waitUntilPageLoad(); - - // Test "your nearest sexual health clinic" link - await homeTestStartPage.clickNearestSexualHealthClinicLink(sexualHealthClinicUrl); - expect(homeTestStartPage.page.url()).toBe(sexualHealthClinicUrl); - await homeTestStartPage.navigate(); - await homeTestStartPage.waitUntilPageLoad(); - - // Test "Learn more about HIV and AIDS" link - await homeTestStartPage.clickLearnMoreHIVAidsLink(hivAidsInfoUrl); - expect(homeTestStartPage.page.url()).toBe(hivAidsInfoUrl); - }); -});