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
2 changes: 1 addition & 1 deletion playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default defineConfig({
/* Retry on CI only */
retries: process.env.CI ? 1 : 0,
workers: 4,
timeout: 300 * 1000,
timeout: 600 * 1000,
expect: { timeout: 10 * 1000 },
use: { actionTimeout: 10 * 1000, navigationTimeout: 30 * 1000 },
/* Report slow tests if they take longer than 5 mins */
Expand Down
6 changes: 1 addition & 5 deletions src/test/ui/config/clean-attachments.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import * as path from 'path';
const testCasesPath = path.join(__dirname, '../../../../allure-report/data/test-cases');

const files = fs.readdirSync(testCasesPath).filter(f => f.endsWith('.json'));
let _removedCount = 0;

for (const file of files) {
const filePath = path.join(testCasesPath, file);
Expand All @@ -25,10 +24,7 @@ for (const file of files) {
if (cleaned.attachments && Array.isArray(cleaned.attachments)) {
const before = cleaned.attachments.length;
cleaned.attachments = cleaned.attachments.filter((a: any) => {
const isSmallVideo = a.type === 'video/webm' && a.size < 10000;
if (isSmallVideo) {
_removedCount++;
}
const isSmallVideo = a.type === 'video/webm' && a.size < 50000;
return !isSmallVideo;
});
if (cleaned.attachments.length !== before) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const nonRentArrearsDispute = {
partOfClaimParagraph: `You should`,
toSeeIfParagraph: `to see if there’s any other parts of the claim that are incorrect or you disagree with.`,
viewTheClaimLink: `view the claim (opens in new tab)`,
mainHeaderGovServiceHiddenNewTab: `Welcome to GOV.UK`,
titleGovServiceHiddenNewTab: `GOV.UK - The best place to find government services and information`,
thisIncludesParagraph: `This includes:`,
groundsForPossessionList: `${process.env.CLAIMANT_NAME}’s grounds for possession (their reasons for making the claim)`,
anyDocumentsList: `any documents they’ve uploaded to support their claim`,
Expand Down
2 changes: 1 addition & 1 deletion src/test/ui/functional/nonRentArrearsDispute.pft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export async function nonRentArrearsDisputeErrorValidation(): Promise<void> {
await performAction(
'clickLinkAndVerifyNewTabTitle',
nonRentArrearsDispute.viewTheClaimLink,
nonRentArrearsDispute.mainHeaderGovServiceHiddenNewTab
nonRentArrearsDispute.titleGovServiceHiddenNewTab
);
await performAction(
'inputText',
Expand Down
11 changes: 8 additions & 3 deletions src/test/ui/functional/rentArrears.pft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ export async function rentArrearsErrorValidation(): Promise<void> {
header: rentArrears.thereIsAProblemErrorMessageHeader,
message: rentArrears.selectIfYouOweErrorMessage,
});
await performAction('clickRadioButton', rentArrears.noRadioOption);
await performAction('clickRadioButton', {
question: rentArrears.doYouOweThisQuestion,
option: rentArrears.noRadioOption,
});
//mandatory input field validation for 'No' radio button selection
await performAction('clickButton', rentArrears.saveAndContinueButton);
await performValidation('errorMessage', {
Expand Down Expand Up @@ -70,7 +73,9 @@ export async function rentArrearsNavigationTests(): Promise<void> {
} else if (process.env.NOTICE_SERVED === 'NO' && process.env.TENANCY_START_DATE_KNOWN === 'YES') {
await performValidation('pageNavigation', rentArrears.backLink, tenancyDateDetails.mainHeader);
}

await performAction('clickRadioButton', rentArrears.yesRadioOption);
await performAction('clickRadioButton', {
question: rentArrears.doYouOweThisQuestion,
option: rentArrears.yesRadioOption,
});
await performValidation('pageNavigation', rentArrears.saveForLaterButton, dashboard.mainHeader);
}
10 changes: 8 additions & 2 deletions src/test/ui/functional/tenancyTypeDetails.pft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ export async function tenancyTypeDetailsErrorValidation(): Promise<void> {
message: tenancyTypeDetails.selectIfTenancyDetailsErrorMessage,
});
//mandatory text field for 'No' radio button selection
await performAction('clickRadioButton', tenancyTypeDetails.noRadioOption);
await performAction('clickRadioButton', {
question: tenancyTypeDetails.isTenancyTypeCorrectQuestion,
option: tenancyTypeDetails.noRadioOption,
});
await performAction('clickButton', tenancyTypeDetails.saveAndContinueButton);
await performValidation('errorMessage', {
header: tenancyTypeDetails.thereIsAProblemErrorMessageHeader,
Expand All @@ -38,6 +41,9 @@ export async function tenancyTypeDetailsNavigationTests(): Promise<void> {
);
}
}
await performAction('clickRadioButton', tenancyTypeDetails.yesRadioOption);
await performAction('clickRadioButton', {
question: tenancyTypeDetails.isTenancyTypeCorrectQuestion,
option: tenancyTypeDetails.yesRadioOption,
});
await performValidation('pageNavigation', tenancyTypeDetails.saveForLaterButton, dashboard.mainHeader);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as fs from 'fs';
import * as path from 'path';

import { Page } from '@playwright/test';
import { Page, test } from '@playwright/test';

import {
enable_content_validation,
Expand All @@ -21,7 +21,6 @@ export class TriggerPageFunctionalTestsAction implements IAction {
private static readonly PFT_DIR = path.join(__dirname, '../../../functional');
private static readonly PAGE_DATA_DIR = path.join(__dirname, '../../../data/page-data');

// Temporary in-memory storage for pages tested in current test run
private static pagesTestedInCurrentRun = new Set<string>();

static resetTestedPages(): void {
Expand All @@ -38,15 +37,18 @@ export class TriggerPageFunctionalTestsAction implements IAction {
return;
}

// Prevent duplicate runs within the same test run (in‑memory)
// Check lock file before running tests
const lockPath = path.join(TriggerPageFunctionalTestsAction.LOCK_DIR, `${pageName}.lock`);
if (fs.existsSync(lockPath)) {
return; // Skip if lock file exists (page already passed all tests)
}

if (TriggerPageFunctionalTestsAction.pagesTestedInCurrentRun.has(pageName)) {
return;
}

// Always run the tests (ignore lock file for execution decision)
TriggerPageFunctionalTestsAction.pagesTestedInCurrentRun.add(pageName);

// Run all enabled tests, tracking failures
const pageDataFilePath = path.join(TriggerPageFunctionalTestsAction.PAGE_DATA_DIR, `${pageName}.page.data.ts`);
const pageDataFileExists = fs.existsSync(pageDataFilePath);

Expand All @@ -69,32 +71,39 @@ export class TriggerPageFunctionalTestsAction implements IAction {
if (enable_navigation_tests === 'true') {
PageNavigationValidation.trackMissingNavigationFile(pageName);
}
// No PFT file means no functional tests to run – nothing to lock
return;
}

let errorValidationFailed = false;
let navigationTestsFailed = false;

if (enable_error_message_validation === 'true') {
try {
await this.runErrorMessageValidation(page, pageName, pftFilePath);
} catch {
errorValidationFailed = true;
// Parent step that groups all functional tests for this page
await test.step(`PFT triggered for page - ${pageName}`, async () => {
if (enable_error_message_validation === 'true') {
await test.step(`EMV triggered for page - ${pageName}`, async () => {
try {
await this.runErrorMessageValidation(page, pageName, pftFilePath);
} catch (error) {
ErrorMessageValidation.trackValidationError(pageName, error);
errorValidationFailed = true;
}
});
}
}

if (enable_navigation_tests === 'true') {
try {
await this.runNavigationTests(page, pageName, pftFilePath);
} catch {
navigationTestsFailed = true;
if (enable_navigation_tests === 'true') {
await test.step(`Navigation tests triggered for page - ${pageName}`, async () => {
try {
await this.runNavigationTests(page, pageName, pftFilePath);
} catch (error) {
PageNavigationValidation.trackNavigationFailure(pageName, error);
navigationTestsFailed = true;
}
});
}
}

const anyTestFailed = errorValidationFailed || navigationTestsFailed;
});

// Update the permanent lock file based on the test outcome
const anyTestFailed = errorValidationFailed || navigationTestsFailed;
if (anyTestFailed) {
// Failure: ensure lock file is removed (if it existed)
this.deleteLockFile(pageName);
Expand Down
2 changes: 1 addition & 1 deletion src/test/ui/utils/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export async function performAction(
? ` - ${typeof displayFieldName === 'object' ? readValuesFromInputObjects(displayFieldName) : displayFieldName}`
: ''
}${
displayValue !== undefined
displayValue !== undefined && value !== undefined
? ` with value '${typeof displayValue === 'object' ? readValuesFromInputObjects(displayValue) : displayValue}'`
: ''
}`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export class ErrorMessageValidation implements IValidation {
private static pagesPassed = new Set<string>();
private static missingEMVFiles = new Set<string>();
private static shouldThrowError = true;
private static emvFailed = false;
private static readonly MAPPING_PATH = path.join(__dirname, '../../../config/urlToFileMapping.config.ts');

async validate(
Expand Down Expand Up @@ -114,6 +115,10 @@ export class ErrorMessageValidation implements IValidation {
error: passed ? undefined : `Expected "${expected}" but found "${actualText || 'nothing'}"`,
});

if (!passed) {
ErrorMessageValidation.emvFailed = true;
}

if (passed) {
ErrorMessageValidation.pagesPassed.add(pageName);
}
Expand All @@ -131,8 +136,18 @@ export class ErrorMessageValidation implements IValidation {
ErrorMessageValidation.missingEMVFiles.add(pageName);
}

static trackValidationError(pageName: string, _error?: unknown): void {
ErrorMessageValidation.missingEMVFiles.add(pageName);
static trackValidationError(pageName: string, error?: unknown): void {
const errorMessage = error instanceof Error ? error.message : String(error);
ErrorMessageValidation.results.push({
pageUrl: '',
pageName,
scenario: 'Error message validation',
passed: false,
expected: 'Error message validation should execute successfully',
actual: errorMessage,
error: errorMessage,
});
ErrorMessageValidation.emvFailed = true;
}

private static async getPageNameFromUrl(url: string, page?: Page): Promise<string> {
Expand Down Expand Up @@ -215,7 +230,7 @@ export class ErrorMessageValidation implements IValidation {
const totalPages = ErrorMessageValidation.pagesWithEMV.size + ErrorMessageValidation.missingEMVFiles.size;

if (totalPages === 0) {
console.log(`\n📊 ERROR MESSAGE VALIDATION (Test #${ErrorMessageValidation.testCounter}):`);
console.log(`\n📊 ERROR MESSAGE VALIDATION SUMMARY (Test #${ErrorMessageValidation.testCounter}):`);
console.log(' No pages checked for error message validation');
return;
}
Expand Down Expand Up @@ -270,26 +285,47 @@ export class ErrorMessageValidation implements IValidation {
const details = failureDetails.get(pageName);
console.log(` Page: ${pageName}`);
if (details) {
console.log(` Expected: ${details.expected}`);
console.log(` Actual: ${details.actual}`);
let errorMessage = details.actual;
if (errorMessage.includes('Header') && errorMessage.includes('not found')) {
errorMessage = `"${details.expected.split(':')[0]}" header not found`;
} else if (errorMessage.includes('Message') && errorMessage.includes('not found')) {
errorMessage = `"${details.expected.split(':')[1]?.trim() || details.expected}" message not found`;
} else if (errorMessage === 'No error message found') {
errorMessage = `"${details.expected}" error message not found`;
}
console.log(` Error: ${errorMessage}`);
}
console.log('');
}
}

if (failedPages.size > 0) {
const hasFailures = failedPages.size > 0 || ErrorMessageValidation.emvFailed;

if (hasFailures) {
console.log('❌ ERROR MESSAGE VALIDATIONS FAILED\n');
} else if (passedPages.size > 0) {
console.log('\n✅ ALL ERROR MESSAGE VALIDATIONS PASSED\n');
} else if (ErrorMessageValidation.pagesWithEMV.size > 0) {
console.log('\n⚠️ EMV methods found but no validations performed\n');
}

// Collect errors to throw with full error message
const errors: string[] = [];

for (const [pageName, details] of failureDetails) {
// Use the actual error message (which contains the full stack trace)
const errorMessage = details.actual;
// Don't format the error message - keep it as is to show the full error
errors.push(`${pageName}: ${errorMessage}`);
}

const shouldThrow =
(failedPages.size > 0 || ErrorMessageValidation.emvFailed) && ErrorMessageValidation.shouldThrowError;

ErrorMessageValidation.clearResults();

// Throw after clearing so failures don't cascade into subsequent tests
if (failedPages.size > 0 && ErrorMessageValidation.shouldThrowError) {
throw new Error(`Error message validation failed: ${failedPages.size} page(s) have failures`);
if (shouldThrow && errors.length > 0) {
throw new Error(`Error message validations failed:\n\n${errors.join('\n\n')}`);
}
}

Expand All @@ -298,5 +334,6 @@ export class ErrorMessageValidation implements IValidation {
ErrorMessageValidation.pagesWithEMV.clear();
ErrorMessageValidation.pagesPassed.clear();
ErrorMessageValidation.missingEMVFiles.clear();
ErrorMessageValidation.emvFailed = false;
}
}
Loading
Loading