Conversation
…ature/hote-541/add-playwright-tests
…ature/hote-541/add-playwright-tests
…ature/hote-541/add-playwright-tests
There was a problem hiding this comment.
Pull request overview
Improves the Playwright E2E test framework and supporting tooling (login flow resilience, configuration, CI workflows), plus a small UI change to silence a React Router hydration warning.
Changes:
- Add
HydrateFallbackto the UI root route and update console-error ignores for CI stability. - Add/adjust Playwright UI tests and page objects (external link checks, NHS login consent handling, OTP wait reliability).
- Expand test configuration and CI/dev tooling (new config loader, env vars, workflow and pre-commit updates).
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/src/app.tsx | Adds HydrateFallback to the guarded root route to avoid hydration warnings. |
| tests/tests/ui/HomeTestStartPage.spec.ts | New UI spec validating external links from the start page. |
| tests/page-objects/NhsLoginHelper.ts | Adds optional handling for an NHS login consent page during auth. |
| tests/page-objects/NHSLogin/NhsLoginConsentPage.ts | New page object to accept/continue on the consent page. |
| tests/page-objects/NHSLogin/CodeSecurityPage.ts | Switches OTP wait to a locator-based wait; deprecates network-based wait. |
| tests/page-objects/HomeTestStartPage.ts | Adds a waitUntilPageLoad() helper for stability. |
| tests/global-setup.ts | Adds SKIP_LOGIN support to reuse existing session files. |
| tests/fixtures/consoleErrorFixture.ts | Extends ignored console/network patterns (incl. CSP font errors). |
| tests/configuration/configuration.ts | Introduces a new config factory/wrapper (defaults + env + local + CLI overrides). |
| tests/configuration/.env.dev | Adds external-link env vars for dev test runs. |
| scripts/config/gitleaks.toml | Allowlists .pre-commit-config.yaml. |
| package.json | Adjusts Playwright run script and adds a full clean script. |
| local-environment/scripts/localstack/get_supplier_id.sh | Minor shell style change (unused loop var). |
| local-environment/scripts/localstack/deploy.sh | Removes unused variables. |
| .pre-commit-config.yaml | Adds/adjusts pre-commit hooks (yaml/json/toml checks, actionlint, shellcheck, etc.). |
| .github/workflows/playwright-e2e.yaml | Adds workflow inputs and environment switching logic; refactors steps and summary output. |
| .github/workflows/cicd-1-pull-request.yaml | Replaces Jira action with inline bash title/branch check; refactors outputs writing. |
| .github/workflows/cicd-2-publish.yaml | Refactors outputs writing and quoting. |
| .github/workflows/cicd-3-deploy.yaml | Removes unused tag input/outputs and refactors outputs writing. |
Comments suppressed due to low confidence (1)
tests/global-setup.ts:5
ConfigFactoryis imported but never used in this file. This will trigger the repo's@typescript-eslint/no-unused-varsrule; remove the unused import (or use it if it was intended for side effects).
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';
| @@ -0,0 +1,42 @@ | |||
| import { test } from '../../fixtures'; | |||
| import { expect } from '@playwright/test'; | |||
| import { config, EnvironmentVariables } from '../../configuration'; | |||
There was a problem hiding this comment.
The imports here won't resolve with the current tests folder structure: ../../fixtures and ../../configuration are directories without an index.ts, so TypeScript/Node module resolution will fail. Import from the concrete module files instead (e.g., the appropriate fixture module and the existing configuration module), or add barrel index.ts files that re-export test/config/EnvironmentVariables.
| import { config, EnvironmentVariables } from '../../configuration'; | |
| import { config, EnvironmentVariables } from '../../configuration/configuration'; |
tests/configuration/configuration.ts
Outdated
| import * as dotenv from 'dotenv'; | ||
| import * as path from 'path'; | ||
| import * as fs from 'fs'; | ||
| import { EnvironmentVariables, availableEnvironments, Environment } from './environment-variables'; |
There was a problem hiding this comment.
This import points to ./environment-variables, but there is no such file in tests/configuration/ (the existing module is ./EnvironmentVariables). On case-sensitive filesystems (CI/Linux) this will fail at compile time; update the import to the correct module path/casing or add the missing file.
| import { EnvironmentVariables, availableEnvironments, Environment } from './environment-variables'; | |
| import { EnvironmentVariables, availableEnvironments, Environment } from './EnvironmentVariables'; |
| export class NhsLoginConsentPage { | ||
| readonly page: Page; |
There was a problem hiding this comment.
The class/file name uses mixed acronym casing (NhsLoginConsentPage), but nearby NHS login page objects consistently use NHS... (e.g. NHSEmailAndPasswordPage). Renaming to NHSLoginConsentPage (and updating imports) would keep naming consistent and improve discoverability.
| 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 |
There was a problem hiding this comment.
For TARGET_ENV=dev, api_url is set to https://dev.hometest.service.nhs.uk/, but the test framework default apiBaseUrl includes the /api prefix (e.g. http://localhost:4000/api) and API clients build URLs relative to that. Unless the dev API is actually hosted at the root, this will produce incorrect request URLs; align the dev api_url with the expected API base path (and keep it consistent with local/terraform outputs).
| - "main" | ||
| pull_request: | ||
| types: [opened, reopened, edited, synchronize] | ||
| types: [opened, reopened, edited, synchronize] |
There was a problem hiding this comment.
pull_request has types: defined twice. YAML will keep only the last key, and the duplicate is confusing/noisy; remove the redundant types: line.
| types: [opened, reopened, edited, synchronize] |
| await homeTestStartPage.waitUntilPageLoad(); | ||
|
|
||
| // Test "your nearest sexual health clinic" link | ||
| await homeTestStartPage.clickNearestSexualHealthClinicLink(sexualHealthClinicUrl); |
There was a problem hiding this comment.
homeTestStartPage.clickNearestSexualHealthClinicLink(...) doesn't exist on HomeTestStartPage (the page object currently exposes clickSexualHealthServicesLink(...)). This will fail to compile/run; update the test to call the existing method or add the missing method on the page object.
| await homeTestStartPage.clickNearestSexualHealthClinicLink(sexualHealthClinicUrl); | |
| await homeTestStartPage.clickSexualHealthServicesLink(sexualHealthClinicUrl); |
| // Map environment variables to config interface | ||
| return { | ||
| uiBaseUrl: process.env[EnvironmentVariables.UI_BASE_URL], | ||
| apiBaseUrl: process.env[EnvironmentVariables.API_BASE_URL], | ||
| headless: process.env[EnvironmentVariables.HEADLESS] === 'true', | ||
| timeout: process.env[EnvironmentVariables.TIMEOUT] | ||
| ? parseInt(process.env[EnvironmentVariables.TIMEOUT], 10) | ||
| : undefined, | ||
| slowMo: process.env[EnvironmentVariables.SLOW_MO] | ||
| ? parseInt(process.env[EnvironmentVariables.SLOW_MO], 10) | ||
| : undefined, | ||
| accessibilityStandards: process.env[EnvironmentVariables.ACCESSIBILITY_STANDARDS], | ||
| reportingOutputDirectory: process.env[EnvironmentVariables.REPORTING_OUTPUT_DIRECTORY], | ||
| externalLinkSexualHealthClinic: process.env[EnvironmentVariables.EXTERNAL_LINK_SEXUAL_HEALTH_CLINIC], | ||
| externalLinkNearestAE: process.env[EnvironmentVariables.EXTERNAL_LINK_NEAREST_AE], | ||
| externalLinkHivAidsInfo: process.env[EnvironmentVariables.EXTERNAL_LINK_HIV_AIDS_INFO], | ||
| }; |
There was a problem hiding this comment.
readConfigurationFromEnvFile() returns an object with properties that can be undefined (or empty strings from dotenv), and then loadConfiguration() spreads that over defaultConfig. This can overwrite required defaults (e.g., uiBaseUrl, apiBaseUrl, external link URLs) with undefined/"", causing runtime failures later. Build the partial config by only including keys that are actually set (and non-empty) before spreading, similar to readConfigurationFromProcessEnv().
| // Map environment variables to config interface | |
| return { | |
| uiBaseUrl: process.env[EnvironmentVariables.UI_BASE_URL], | |
| apiBaseUrl: process.env[EnvironmentVariables.API_BASE_URL], | |
| headless: process.env[EnvironmentVariables.HEADLESS] === 'true', | |
| timeout: process.env[EnvironmentVariables.TIMEOUT] | |
| ? parseInt(process.env[EnvironmentVariables.TIMEOUT], 10) | |
| : undefined, | |
| slowMo: process.env[EnvironmentVariables.SLOW_MO] | |
| ? parseInt(process.env[EnvironmentVariables.SLOW_MO], 10) | |
| : undefined, | |
| accessibilityStandards: process.env[EnvironmentVariables.ACCESSIBILITY_STANDARDS], | |
| reportingOutputDirectory: process.env[EnvironmentVariables.REPORTING_OUTPUT_DIRECTORY], | |
| externalLinkSexualHealthClinic: process.env[EnvironmentVariables.EXTERNAL_LINK_SEXUAL_HEALTH_CLINIC], | |
| externalLinkNearestAE: process.env[EnvironmentVariables.EXTERNAL_LINK_NEAREST_AE], | |
| externalLinkHivAidsInfo: process.env[EnvironmentVariables.EXTERNAL_LINK_HIV_AIDS_INFO], | |
| }; | |
| const envConfig: Partial<ConfigInterface> = {}; | |
| const getEnvValue = (key: EnvironmentVariables): string | undefined => { | |
| const value = process.env[key]; | |
| if (value === undefined) { | |
| return undefined; | |
| } | |
| const trimmed = value.trim(); | |
| return trimmed === '' ? undefined : trimmed; | |
| }; | |
| const uiBaseUrl = getEnvValue(EnvironmentVariables.UI_BASE_URL); | |
| if (uiBaseUrl !== undefined) { | |
| envConfig.uiBaseUrl = uiBaseUrl; | |
| } | |
| const apiBaseUrl = getEnvValue(EnvironmentVariables.API_BASE_URL); | |
| if (apiBaseUrl !== undefined) { | |
| envConfig.apiBaseUrl = apiBaseUrl; | |
| } | |
| const headlessValue = getEnvValue(EnvironmentVariables.HEADLESS); | |
| if (headlessValue !== undefined) { | |
| envConfig.headless = headlessValue === 'true'; | |
| } | |
| const timeoutValue = getEnvValue(EnvironmentVariables.TIMEOUT); | |
| if (timeoutValue !== undefined) { | |
| envConfig.timeout = parseInt(timeoutValue, 10); | |
| } | |
| const slowMoValue = getEnvValue(EnvironmentVariables.SLOW_MO); | |
| if (slowMoValue !== undefined) { | |
| envConfig.slowMo = parseInt(slowMoValue, 10); | |
| } | |
| const accessibilityStandards = getEnvValue(EnvironmentVariables.ACCESSIBILITY_STANDARDS); | |
| if (accessibilityStandards !== undefined) { | |
| envConfig.accessibilityStandards = accessibilityStandards; | |
| } | |
| const reportingOutputDirectory = getEnvValue(EnvironmentVariables.REPORTING_OUTPUT_DIRECTORY); | |
| if (reportingOutputDirectory !== undefined) { | |
| envConfig.reportingOutputDirectory = reportingOutputDirectory; | |
| } | |
| const externalLinkSexualHealthClinic = getEnvValue( | |
| EnvironmentVariables.EXTERNAL_LINK_SEXUAL_HEALTH_CLINIC | |
| ); | |
| if (externalLinkSexualHealthClinic !== undefined) { | |
| envConfig.externalLinkSexualHealthClinic = externalLinkSexualHealthClinic; | |
| } | |
| const externalLinkNearestAE = getEnvValue(EnvironmentVariables.EXTERNAL_LINK_NEAREST_AE); | |
| if (externalLinkNearestAE !== undefined) { | |
| envConfig.externalLinkNearestAE = externalLinkNearestAE; | |
| } | |
| const externalLinkHivAidsInfo = getEnvValue(EnvironmentVariables.EXTERNAL_LINK_HIV_AIDS_INFO); | |
| if (externalLinkHivAidsInfo !== undefined) { | |
| envConfig.externalLinkHivAidsInfo = externalLinkHivAidsInfo; | |
| } | |
| return envConfig; |
17dacc7 to
7b368d6
Compare
This reverts commit c2c63dd.
| runs-on: ubuntu-latest | ||
| timeout-minutes: 30 | ||
| env: | ||
| TARGET_ENV: ${{ inputs.environment || 'dev' }} |
There was a problem hiding this comment.
Similarly, TARGET_ENV is derived from ${{ inputs.environment }} at job scope, which can break scheduled runs where inputs is not available. Prefer ${{ github.event.inputs.environment || 'dev' }} (or set TARGET_ENV in a step that handles both schedule and workflow_dispatch).
| TARGET_ENV: ${{ inputs.environment || 'dev' }} | |
| TARGET_ENV: ${{ github.event.inputs.environment || 'dev' }} |
| case EnvironmentVariables.EXTERNAL_LINK_SEXUAL_HEALTH_CLINIC: | ||
| return config.externalLinkSexualHealthClinic; | ||
| case EnvironmentVariables.EXTERNAL_LINK_NEAREST_AE: | ||
| return config.externalLinkNearestAE; | ||
| case EnvironmentVariables.EXTERNAL_LINK_HIV_AIDS_INFO: | ||
| return config.externalLinkHivAidsInfo; |
There was a problem hiding this comment.
EnvironmentVariables.EXTERNAL_LINK_* cases are referenced here, but the existing tests/configuration/EnvironmentVariables.ts enum does not define these members. As written, this won’t compile. Either add these enum values (and include them in the config sources) or remove these cases and use a different mechanism for external-link config.
| case EnvironmentVariables.EXTERNAL_LINK_SEXUAL_HEALTH_CLINIC: | |
| return config.externalLinkSexualHealthClinic; | |
| case EnvironmentVariables.EXTERNAL_LINK_NEAREST_AE: | |
| return config.externalLinkNearestAE; | |
| case EnvironmentVariables.EXTERNAL_LINK_HIV_AIDS_INFO: | |
| return config.externalLinkHivAidsInfo; |
| @@ -1,12 +1,26 @@ | |||
| import * as fs from 'fs'; | |||
| import * as path from 'path'; | |||
| import { ConfigFactory } from './configuration/configuration'; | |||
There was a problem hiding this comment.
ConfigFactory is imported from ./configuration/configuration but never used. Besides being dead code, it also pulls in the newly added tests/configuration/configuration.ts (which currently has compile issues). Remove the unused import, or switch to the existing configuration/EnvironmentConfiguration if you intended to force config initialization here.
| import { ConfigFactory } from './configuration/configuration'; |
| // 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/,, |
There was a problem hiding this comment.
ignorePatterns array has a syntax error: the regex line ends with a double comma (,,). This will prevent TypeScript from compiling and will break the test run. Remove the extra comma (and consider updating the adjacent comment since HydrateFallback is now set in the UI route).
| // 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/,, | |
| // React Router v7 warning about HydrateFallback during initial hydration | |
| // HydrateFallback is now set in the UI route; ignore this warning if it still appears | |
| /No `HydrateFallback` element provided to render during initial hydration/, |
| import * as dotenv from 'dotenv'; | ||
| import * as path from 'path'; | ||
| import * as fs from 'fs'; | ||
| import { EnvironmentVariables, availableEnvironments, Environment } from './environment-variables'; | ||
|
|
||
| export { EnvironmentVariables }; |
There was a problem hiding this comment.
This new config module will not compile on a case-sensitive filesystem: it imports ./environment-variables, but the existing file is EnvironmentVariables.ts. Additionally, the switch below references EnvironmentVariables.EXTERNAL_LINK_*, which are not defined in the current EnvironmentVariables enum. Align the import path and either extend the enum or remove those switch cases.
| // 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 }); |
There was a problem hiding this comment.
The consent-page detection only waits 15s for either the consent URL or the app redirect. If the consent screen appears after that window (slow auth / network), consentAppeared becomes false and the final wait for the app URL can time out because the consent page is still blocking. Consider waiting for either URL for the full redirect timeout (or looping until one of them occurs) to avoid flakiness.
| - name: "Start the application" | ||
| if: env.TARGET_ENV == 'local' || env.TARGET_ENV == 'dev' | ||
| run: | | ||
| npm run start | ||
|
|
||
| - name: "Show application status" | ||
| 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' || env.TARGET_ENV == 'dev' | ||
| 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) |
There was a problem hiding this comment.
For TARGET_ENV=dev this job still starts the local application and fetches terraform outputs, but later overrides URLs to point at the remote dev environment. This adds unnecessary runtime and risk of failure. Consider making these steps conditional on TARGET_ENV == 'local' only (and skip terraform/docker when running against remote dev).
| // Map environment variables to config interface | ||
| return { | ||
| uiBaseUrl: process.env[EnvironmentVariables.UI_BASE_URL], | ||
| apiBaseUrl: process.env[EnvironmentVariables.API_BASE_URL], | ||
| headless: process.env[EnvironmentVariables.HEADLESS] === 'true', | ||
| timeout: process.env[EnvironmentVariables.TIMEOUT] | ||
| ? parseInt(process.env[EnvironmentVariables.TIMEOUT], 10) | ||
| : undefined, | ||
| slowMo: process.env[EnvironmentVariables.SLOW_MO] | ||
| ? parseInt(process.env[EnvironmentVariables.SLOW_MO], 10) | ||
| : undefined, | ||
| accessibilityStandards: process.env[EnvironmentVariables.ACCESSIBILITY_STANDARDS], | ||
| reportingOutputDirectory: process.env[EnvironmentVariables.REPORTING_OUTPUT_DIRECTORY], | ||
| externalLinkSexualHealthClinic: process.env[EnvironmentVariables.EXTERNAL_LINK_SEXUAL_HEALTH_CLINIC], | ||
| externalLinkNearestAE: process.env[EnvironmentVariables.EXTERNAL_LINK_NEAREST_AE], | ||
| externalLinkHivAidsInfo: process.env[EnvironmentVariables.EXTERNAL_LINK_HIV_AIDS_INFO], | ||
| }; |
There was a problem hiding this comment.
readConfigurationFromEnvFile() returns an object with keys set directly from process.env[...] even when those env vars are undefined. When spread-merged, this can overwrite defaults with undefined (e.g., uiBaseUrl/apiBaseUrl), causing runtime failures. Filter out undefined values before merging (similar to what readConfigurationFromProcessEnv() already does).
…tests' into feature/hote-541/playwright-Przemek
…wright-Przemek # Conflicts: # tests/configuration/.env.local # ui/src/app.tsx
…541/add-playwright-tests
|
|
||
| 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`); |
There was a problem hiding this comment.
The catch block logs a success message ("✅ Successfully logged in...") immediately before re-throwing the error. This message is misleading and will make failures harder to diagnose; it should be removed or replaced with a failure-specific log.
| console.log(`✅ Successfully logged in worker user ${user.nhsNumber} and saved session state. CATCH`); | |
| console.log(`ℹ️ Cleaned up browser after failed login for worker user ${user.nhsNumber}.`); |
| // 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}`); | ||
| } | ||
| } | ||
| }); | ||
|
|
There was a problem hiding this comment.
This debug logging captures and persists full request bodies (request.postData()) and selected response bodies to testResults/nhs-login-debug.log. During NHS login this can include credentials/OTP and other sensitive data, which is a security risk (especially if artifacts are uploaded). Please gate this behind an explicit debug flag, redact sensitive fields, and avoid logging auth payloads by default.
| // 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}`); | |
| } | |
| } | |
| }); | |
| // Debug logging for NHS login flow, gated behind explicit flag | |
| const isDebugEnabled = process.env.NHS_LOGIN_DEBUG === 'true'; | |
| const log = (message: string): void => { | |
| if (!isDebugEnabled) { | |
| return; | |
| } | |
| const logFile = 'testResults/nhs-login-debug.log'; | |
| fs.mkdirSync('testResults', { recursive: true }); | |
| const timestamp = new Date().toISOString(); | |
| const logMessage = `[${timestamp}] ${message}\n`; | |
| console.log(message); // Also log to console | |
| fs.appendFileSync(logFile, logMessage); | |
| }; | |
| if (isDebugEnabled) { | |
| log('=== NHS Login Flow Started ==='); | |
| // Capture browser console messages (no sensitive data expected) | |
| 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 high-level network request metadata (no bodies) | |
| page.on('request', (request) => { | |
| log(`[Network Request] ${request.method()} ${request.url()}`); | |
| }); | |
| // Capture high-level network response metadata (no bodies) | |
| page.on('response', (response) => { | |
| const status = response.status(); | |
| const statusText = response.statusText(); | |
| const url = response.url(); | |
| log( | |
| `[Network Response] ${status} ${statusText} - ${response.request().method()} ${url}` | |
| ); | |
| }); | |
| } |
tests/configuration/.env.local
Outdated
| # Development Environment Configuration | ||
| UI_BASE_URL=http://localhost:3000 | ||
| API_BASE_URL=http://localhost:4566/_aws/execute-api/y18amry36z/local | ||
| API_BASE_URL=http://localhost:4566/_aws/execute-api/gppofwpzq9/local |
There was a problem hiding this comment.
API_BASE_URL in .env.local is set to a specific LocalStack API Gateway ID (gppofwpzq9). These IDs are typically ephemeral and will change across environments/recreates, so committing a concrete ID can break other developers/CI. Consider leaving this unset (and relying on terraform outputs / scripts) or documenting a stable way to refresh it locally.
| API_BASE_URL=http://localhost:4566/_aws/execute-api/gppofwpzq9/local | |
| # API base URL for local testing. | |
| # Do not commit a concrete LocalStack API Gateway ID here, as it is environment-specific. | |
| # Example (for local overrides only): http://localhost:4566/_aws/execute-api/<gateway-id>/local | |
| API_BASE_URL= |
| networkErrors.push({ | ||
| url: response.url(), | ||
| status, | ||
| statusText: response.statusText(), | ||
| method: response.request().method(), | ||
| timestamp: new Date().toISOString(), | ||
| responseText: response.text().catch(() => "Unable to retrieve response body"), | ||
| }); |
There was a problem hiding this comment.
NetworkError.responseText is stored as a Promise<string> (response.text()), so later JSON.stringify(networkErrors) will serialize it as {} and the saved *-network-errors.json won’t contain the response body. Capture/await the text in the response handler and store it as a string (or omit it) rather than storing a Promise.
| // 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}`); | ||
| } | ||
| } | ||
| }); |
There was a problem hiding this comment.
fillNhsLoginFormsAndWaitForStartPage() attaches page.on('console'|'request'|'response') listeners every time it is called, but never removes them. If this helper is invoked multiple times on the same page, logs will be duplicated and memory usage can grow. Consider registering these listeners once (e.g., in a fixture) or using page.once / removing listeners after the login flow completes.
| '''.idea/*''', | ||
| '''.pre-commit-config.yaml''' | ||
| ] |
There was a problem hiding this comment.
Adding .pre-commit-config.yaml to the global gitleaks allowlist excludes it from secret scanning entirely. Since hook configs can contain tokens/URLs/credentials, this weakens detection. Prefer narrowing the allowlist to the specific false-positive pattern(s) (via rule-level allowlists) rather than excluding the whole file.
| '''.idea/*''', | |
| '''.pre-commit-config.yaml''' | |
| ] | |
| '''.idea/*''' | |
| ] |
Description
Context
Type of changes
Checklist
Sensitive Information Declaration
To ensure the utmost confidentiality and protect your and others privacy, we kindly ask you to NOT including PII (Personal Identifiable Information) / PID (Personal Identifiable Data) or any other sensitive data in this PR (Pull Request) and the codebase changes. We will remove any PR that do contain any sensitive information. We really appreciate your cooperation in this matter.