diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 774df9b7..4062cb61 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -51,8 +51,8 @@ jobs:
}
shell: pwsh
env:
- NotificationConfigurations__ApiKey: ${{ secrets.NOTIFICATIONCONFIGURATIONS__APIKEY }}
- NHSLoginOIDC__privateKeyb64: ${{ secrets.NHSLOGINOIDC__PRIVATEKEYB64 }}
+ NOTIFICATIONCONFIGURATIONS__APIKEY: ${{ secrets.NOTIFICATIONCONFIGURATIONS__APIKEY }}
+ NHSLOGINOIDC__PRIVATEKEYB64: ${{ secrets.NHSLOGINOIDC__PRIVATEKEYB64 }}
add_tag:
name: Tag and Release
runs-on: ubuntu-latest
diff --git a/LondonDataServices.IDecide.Portal.Client/LondonDataServices.IDecide.Portal.Client.esproj b/LondonDataServices.IDecide.Portal.Client/LondonDataServices.IDecide.Portal.Client.esproj
index 80064ad8..8265961c 100644
--- a/LondonDataServices.IDecide.Portal.Client/LondonDataServices.IDecide.Portal.Client.esproj
+++ b/LondonDataServices.IDecide.Portal.Client/LondonDataServices.IDecide.Portal.Client.esproj
@@ -15,6 +15,7 @@
+
diff --git a/LondonDataServices.IDecide.Portal.Client/tests/helpers/helper.ts b/LondonDataServices.IDecide.Portal.Client/tests/helpers/helper.ts
index af0f16dc..01e52421 100644
--- a/LondonDataServices.IDecide.Portal.Client/tests/helpers/helper.ts
+++ b/LondonDataServices.IDecide.Portal.Client/tests/helpers/helper.ts
@@ -2,10 +2,16 @@
export async function clickStartButton(page) {
await page.getByTestId('start-login-button').click();
}
+
export async function clickStartAnotherPersonButton(page) {
await page.getByTestId('start-another-person-button').click();
}
-export async function fillPoaFields(page: import('@playwright/test').Page, firstName = 'John', surname = 'Doe') {
+
+export async function fillPoaFields(
+ page: import('@playwright/test').Page,
+ firstName = 'John',
+ surname = 'Doe'
+) {
await page.locator('#poa-firstname').fill(firstName);
await page.locator('#poa-surname').fill(surname);
@@ -15,4 +21,106 @@ export async function fillPoaFields(page: import('@playwright/test').Page, first
const randomIndex = Math.floor(Math.random() * (options.length - 1)) + 1;
const randomValue = await options[randomIndex].getAttribute('value');
await page.locator('#poa-relationship').selectOption(randomValue!);
+}
+
+// Helper function to populate NHS login credentials
+export async function fillNhsLoginCredentials(
+ page: import('@playwright/test').Page,
+ email = 'testuserlive@demo.signin.nhs.uk',
+ password = 'Passw0rd$1'
+) {
+ await page.locator('input[type="email"]').fill(email);
+ await page.locator('input[type="password"]').fill(password);
+}
+
+// Helper function to click the Continue button
+export async function clickContinueButton(page: import('@playwright/test').Page) {
+ await page.getByRole('button', { name: /continue/i }).click();
+}
+
+// Helper function to fill the OTP code
+export async function fillOtpCode(
+ page: import('@playwright/test').Page,
+ otpCode = '190696'
+) {
+ await page.locator('#otp-input').fill(otpCode);
+}
+
+// Helper function to check the "Remember this device" checkbox
+export async function checkRememberDeviceCheckbox(page: import('@playwright/test').Page) {
+ await page.locator('#rmd').check();
+}
+
+// Helper function to handle OTP if prompted and click continue
+export async function handleOtpIfPrompted(
+ page: import('@playwright/test').Page,
+ otpCode = '190696'
+) {
+ // Wait for navigation after login to settle
+ await page.waitForLoadState('networkidle');
+
+ const currentUrl = page.url();
+
+ // Check if we're locked out due to too many OTP attempts
+ if (currentUrl.includes('/otp-attempts-exceeded')) {
+ throw new Error(
+ 'OTP attempts exceeded. NHS Login has locked you out for 15 minutes. ' +
+ 'Please wait before running tests again.'
+ );
+ }
+
+ // Check if we're on the OTP page by URL
+ const isOnOtpPage = currentUrl.includes('/enter-mobile-code');
+
+ if (isOnOtpPage) {
+ const otpInput = page.locator('#otp-input');
+ const continueButton = page.getByRole('button', { name: /continue/i });
+
+ // Wait for OTP input to be visible
+ await otpInput.waitFor({ state: 'visible', timeout: 10000 });
+
+ // Check for existing error before attempting to fill OTP
+ const errorAlert = page.locator('div[role="alert"]:has-text("Error")');
+ const hasExistingError = await errorAlert.isVisible().catch(() => false);
+
+ if (hasExistingError) {
+ const errorText = await errorAlert.textContent();
+ throw new Error(
+ `OTP page shows existing error: ${errorText}. ` +
+ 'Tests aborted to prevent lockout. Please resolve the error before continuing.'
+ );
+ }
+
+ // Fill OTP and remember device
+ await otpInput.fill(otpCode);
+ await page.locator('#rmd').check();
+
+ // Click continue button
+ await continueButton.click();
+
+ // Wait for navigation away from OTP page
+ await page.waitForLoadState('networkidle');
+
+ const newUrl = page.url();
+
+ // Check if we got locked out after submission
+ if (newUrl.includes('/otp-attempts-exceeded')) {
+ throw new Error(
+ 'Invalid OTP code caused lockout. NHS Login has locked you out for 15 minutes. ' +
+ `The OTP code "${otpCode}" appears to be incorrect.`
+ );
+ }
+
+ // Check if we're still on the OTP page with an error
+ if (newUrl.includes('/enter-mobile-code')) {
+ const postSubmitError = await errorAlert.isVisible().catch(() => false);
+ if (postSubmitError) {
+ const errorText = await errorAlert.textContent();
+ throw new Error(
+ `OTP submission failed: ${errorText}. ` +
+ 'Tests aborted to prevent further failed attempts and lockout.'
+ );
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/LondonDataServices.IDecide.Portal.Client/tests/homepageNhsLogin.spec.ts b/LondonDataServices.IDecide.Portal.Client/tests/homepageNhsLogin.spec.ts
new file mode 100644
index 00000000..97af4853
--- /dev/null
+++ b/LondonDataServices.IDecide.Portal.Client/tests/homepageNhsLogin.spec.ts
@@ -0,0 +1,99 @@
+import { test, expect } from '@playwright/test';
+import {
+ clickStartButton,
+ fillNhsLoginCredentials,
+ clickContinueButton,
+ handleOtpIfPrompted
+} from './helpers/helper';
+
+test.describe('Home Page Nhs Login', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('https://localhost:5173/home');
+ });
+
+ test('should display the Start button', async ({ page }) => {
+ await expect(page.getByTestId('start-login-button')).toBeVisible();
+ });
+
+ test('should navigate to NHS login and complete authentication', async ({ page }) => {
+ await clickStartButton(page);
+ await expect(page).toHaveURL('https://access.sandpit.signin.nhs.uk/login');
+
+ await fillNhsLoginCredentials(page);
+ await clickContinueButton(page);
+
+ await handleOtpIfPrompted(page);
+
+ // Verify we're back on the app after authentication
+ await expect(page).toHaveURL(/https:\/\/localhost:5173/);
+ });
+
+ test('should display Confirm Details page with user information', async ({ page }) => {
+ await clickStartButton(page);
+ await expect(page).toHaveURL('https://access.sandpit.signin.nhs.uk/login');
+
+ await fillNhsLoginCredentials(page);
+ await clickContinueButton(page);
+
+ await handleOtpIfPrompted(page);
+
+ // Wait for redirect back to app
+ await page.waitForURL('https://localhost:5173/nhs-optOut');
+
+ // Verify the Next button is visible on the Confirm Details page
+ await expect(page.getByRole('button', { name: /next/i })).toBeVisible();
+
+ // Verify the summary list contains the expected user details
+ const summaryList = page.locator('dl.nhsuk-summary-list');
+ await expect(summaryList).toBeVisible();
+
+ // Verify Name field
+ await expect(
+ summaryList
+ .locator('.nhsuk-summary-list__row', { has: page.locator('dt:has-text("Name")') })
+ .locator('dd.nhsuk-summary-list__value')
+ ).toHaveText('Mona, MILLAR');
+
+ // Verify Email field
+ await expect(
+ summaryList
+ .locator('.nhsuk-summary-list__row', { has: page.locator('dt:has-text("Email")') })
+ .locator('dd.nhsuk-summary-list__value')
+ ).toHaveText('testuserlive@demo.signin.nhs.uk');
+
+ // Verify Mobile Number field
+ await expect(
+ summaryList
+ .locator(
+ '.nhsuk-summary-list__row',
+ { has: page.locator('dt:has-text("Mobile Number")') }
+ )
+ .locator('dd.nhsuk-summary-list__value')
+ ).toHaveText('+447887510886');
+ });
+
+ test('should navigate to Make your Choice page after clicking Next button', async ({ page }) => {
+ await clickStartButton(page);
+ await expect(page).toHaveURL('https://access.sandpit.signin.nhs.uk/login');
+
+ await fillNhsLoginCredentials(page);
+ await clickContinueButton(page);
+
+ await handleOtpIfPrompted(page);
+
+ // Wait for redirect back to app
+ await page.waitForURL('https://localhost:5173/nhs-optOut');
+
+ // Click the Next button on the Confirm Details page
+ await page.getByRole('button', { name: /next/i }).click();
+
+ // Wait for navigation to complete
+ await page.waitForLoadState('networkidle');
+
+ // Verify that we're on the Make your Choice page (still same route)
+ await expect(page).toHaveURL('https://localhost:5173/nhs-optOut');
+
+ // Verify page content has changed to Make your Choice
+ await expect(page.locator('h4:has-text("Make your Choice")')).toBeVisible();
+ });
+});
\ No newline at end of file
diff --git a/LondonDataServices.IDecide.Portal.Client/tests/setup/auth.setup.tffs b/LondonDataServices.IDecide.Portal.Client/tests/setup/auth.setup.tffs
new file mode 100644
index 00000000..b7681d8c
--- /dev/null
+++ b/LondonDataServices.IDecide.Portal.Client/tests/setup/auth.setup.tffs
@@ -0,0 +1,32 @@
+import { test as setup, expect } from '@playwright/test';
+import {
+ clickStartButton,
+ fillNhsLoginCredentials,
+ clickContinueButton,
+ handleOtpIfPrompted
+} from '../helpers/helper';
+
+const authFile = 'playwright/.auth/user.json';
+
+setup('authenticate with NHS Login', async ({ page }) => {
+ // Navigate to home page
+ await page.goto('https://localhost:5173/home');
+
+ // Perform NHS Login authentication
+ await clickStartButton(page);
+ await expect(page).toHaveURL('https://access.sandpit.signin.nhs.uk/login');
+
+ await fillNhsLoginCredentials(page);
+ await clickContinueButton(page);
+
+ // Handle OTP if prompted (will remember device)
+ await handleOtpIfPrompted(page);
+
+ // Wait for redirect back to app
+ await page.waitForURL(/https:\/\/localhost:5173/);
+
+ // Save authenticated state including cookies and localStorage
+ await page.context().storageState({ path: authFile });
+
+ console.log('? Authentication completed and saved to', authFile);
+});
\ No newline at end of file