Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<ItemGroup>
<Folder Include="src\components\accessControls\" />
<Folder Include="dependencies\guid-typescript\node_modules\%40types\" />
<Folder Include="tests\setup\NewFolder\" />
</ItemGroup>
<Target Name="NpmBuild" AfterTargets="ComputeFilesToPublish">
<Exec Command="npm run build:$(Configuration)" WorkingDirectory="$(SpaRoot)" />
Expand Down
110 changes: 109 additions & 1 deletion LondonDataServices.IDecide.Portal.Client/tests/helpers/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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.'
);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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();
});
});
Original file line number Diff line number Diff line change
@@ -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);
});
Loading