Repository: HealthResearchAuthority/rsp-iras-qa
Pull request #22: RSP-1860 IRAS ID duplicate and other validations
Author: login: vijay-kumar-hra
Created: 2025-04-23T16:13:17Z | Merged: 2025-04-29T09:34:40Z

## What was the purpose of this ticket?

As a researcher (front-stage user)
I want to create a project record with a unique and valid  IRAS ID
So that I know the project is valid and has not already been created in the system (eg by another user) 

## Jira Ref

[RSP-1860](https://nihr.atlassian.net/browse/RSP-1860)

New scenario added to validate the duplicate IRAS ID and updated existing scenario to validate invalid IRAS ID errors 

Put [X] inside the [] to make the box ticked

- [X] New Tests
- [X] Updating Tests
- [] Fixing Tests
- [] Framework Improvements
- [] Documentation

## Solution

What work was completed to cover off the story?

- Updated the existing error validation method to check the order and viewport 
- Added new scenario 
- Added new function to generate unique IRAS ID data

## Checklist

Put [X] inside the [] to make the box ticked

- [X] Your code builds clean without any warnings, i.e. no ESLint or SonarCube errors
- [X] You have run the Pipeline jobs against this branch successfully
- [X] Any new or updated tests add value, i.e. provide correct assurances, fail when expected and log errors appropriately
- [X] Tests have been tagged appropriately
- [X] No duplicate tests, steps or functions have been added
- [X] You are using the correct naming conventions
- [X] Added files have been placed correctly within the projects folder structure
- [X] There is no dead code e.g. no "TODO" comments left, no unused imports etc
- [] Any Framework updates have been explained and demonstrated to the team



Diff:
diff --git a/src/pages/Common/CommonItemsPage.ts b/src/pages/Common/CommonItemsPage.ts
index ed5a46c1a..5eb4cab58 100644
--- a/src/pages/Common/CommonItemsPage.ts
+++ b/src/pages/Common/CommonItemsPage.ts
@@ -2,6 +2,7 @@ import { expect, Locator, Page } from '@playwright/test';
 import * as buttonTextData from '../../resources/test_data/common/button_text_data.json';
 import * as linkTextData from '../../resources/test_data/common/link_text_data.json';
 import * as questionSetData from '../../resources/test_data/common/question_set_data.json';
+import * as commonTestData from '../../resources/test_data/common/common_data.json';
 import fs from 'fs';
 import path from 'path';
 import ProjectFilterPage from '../IRAS/questionSet/ProjectFilterPage';
@@ -21,6 +22,7 @@ export default class CommonItemsPage {
   readonly buttonTextData: typeof buttonTextData;
   readonly linkTextData: typeof linkTextData;
   readonly questionSetData: typeof questionSetData;
+  readonly commonTestData: typeof commonTestData;
   readonly showAllSectionsAccordion: Locator;
   readonly genericButton: Locator;
   readonly govUkButton: Locator;
@@ -48,6 +50,8 @@ export default class CommonItemsPage {
   readonly alert_box_list_items: Locator;
   readonly errorMessageFieldLabel: Locator;
   readonly errorMessageSummaryLabel: Locator;
+  readonly summaryErrorLinks: Locator;
+  readonly fieldGroup: Locator;
 
   //Initialize Page Objects
   constructor(page: Page) {
@@ -55,6 +59,7 @@ export default class CommonItemsPage {
     this.buttonTextData = buttonTextData;
     this.linkTextData = linkTextData;
     this.questionSetData = questionSetData;
+    this.commonTestData = commonTestData;
 
     //Locators
     this.showAllSectionsAccordion = page.locator('.govuk-accordion__show-all"');
@@ -63,6 +68,7 @@ export default class CommonItemsPage {
     this.govUkCheckboxes = this.page.locator('.govuk-checkboxes');
     this.govUkCheckboxItem = this.govUkCheckboxes.locator('.govuk-checkboxes__item');
     this.govUkLink = this.page.getByRole('link');
+    this.fieldGroup = this.page.locator('.govuk-form-group');
     this.govUkFieldValidationError = this.page.locator('.govuk-error-message.field-validation-error');
     this.qSetProgressBar = page.locator('.progress-container');
     this.qSetProgressBarStage = this.qSetProgressBar.locator('.stage');
@@ -81,8 +87,13 @@ export default class CommonItemsPage {
     this.bannerQuestionSet = this.bannerNavBar.getByText(this.linkTextData.Banner.Question_Set, { exact: true });
     this.bannerSystemAdmin = this.bannerNavBar.getByText(this.linkTextData.Banner.System_Admin, { exact: true });
     this.bannerMyApplications = this.bannerNavBar.getByText(this.linkTextData.Banner.My_Applications, { exact: true });
-    this.errorMessageFieldLabel = page.locator('[class$="field-validation-error"]');
-    this.errorMessageSummaryLabel = page.locator('div[class="govuk-error-summary"]');
+    this.errorMessageFieldLabel = page.locator('.govuk-error-message');
+    this.errorMessageSummaryLabel = this.page
+      .getByRole('heading')
+      .getByText(this.commonTestData.error_message_summary_header, {
+        exact: true,
+      });
+    this.summaryErrorLinks = this.errorMessageSummaryLabel.locator('..').getByRole('listitem').getByRole('link');
     //Validation Alert Box
     this.alert_box = this.page.getByRole('alert');
     this.alert_box_headings = this.alert_box.getByRole('heading');
@@ -371,6 +382,7 @@ export default class CommonItemsPage {
       await expect(otherLocator).toHaveText(errorMessageFieldDataset[key]);
     }
   }
+
   async validateUIComponentValues<PageObject>(dataset: JSON, key: string, page: PageObject) {
     const locator: Locator = page[key];
     const typeAttribute = await locator.first().getAttribute('type');
@@ -442,4 +454,21 @@ export default class CommonItemsPage {
     const locator: Locator = page[key];
     return await removeUnwantedWhitespace(confirmStringNotNull(await locator.textContent()));
   }
+
+  async getSummaryErrorMessages() {
+    const summaryErrorActualValues = await this.summaryErrorLinks.allTextContents();
+    return summaryErrorActualValues;
+  }
+
+  async getFieldErrorMessages<PageObject>(key: string, page: PageObject) {
+    const element = await page[key].first();
+    const fieldErrorLocator = this.fieldGroup.filter({ has: element }).locator(this.errorMessageFieldLabel);
+    return await fieldErrorLocator.textContent();
+  }
+
+  async checkViewport<PageObject>(errorMessageFieldDataset: JSON, key: string, page: PageObject) {
+    const element = await page[key].first();
+    await this.summaryErrorLinks.filter({ hasText: errorMessageFieldDataset[key] }).click();
+    return await element;
+  }
 }
diff --git a/src/pages/IRAS/makeChanges/ProjectDetailsIRASPage.ts b/src/pages/IRAS/makeChanges/ProjectDetailsIRASPage.ts
index 2ad94fc90..e724e4eeb 100644
--- a/src/pages/IRAS/makeChanges/ProjectDetailsIRASPage.ts
+++ b/src/pages/IRAS/makeChanges/ProjectDetailsIRASPage.ts
@@ -1,6 +1,6 @@
 import { expect, Locator, Page } from '@playwright/test';
 import * as projectDetailsIRASPageTestData from '../../../resources/test_data/iras/make_changes/project_details_iras_data.json';
-
+import * as fse from 'fs-extra';
 //Declare Page Objects
 export default class ProjectDetailsIRASPage {
   readonly page: Page;
@@ -10,6 +10,7 @@ export default class ProjectDetailsIRASPage {
   readonly iras_id_text: Locator;
   readonly iras_id_text_summary_error_label: Locator;
   readonly iras_textbox_hint: Locator;
+  private _unique_iras_id: string;
 
   //Initialize Page Objects
   constructor(page: Page) {
@@ -17,11 +18,15 @@ export default class ProjectDetailsIRASPage {
     this.projectDetailsIRASPageTestData = projectDetailsIRASPageTestData;
 
     //Locators
-    this.pageHeading = this.page.getByTestId('title');
-    this.iras_textbox_label = this.page.locator('label[for="IQA0001_Text"]');
-    this.iras_textbox_hint = this.page.getByTestId('rule-hint');
-    this.iras_id_text = this.page.getByTestId('IQA0001_Text');
-    this.iras_id_text_summary_error_label = this.page.locator('a[href="#Questions[0].AnswerText"]');
+    this.pageHeading = this.page.getByRole('heading');
+    this.iras_textbox_label = this.page.getByText(this.projectDetailsIRASPageTestData.Label_Texts.iras_textbox_label, {
+      exact: true,
+    });
+    this.iras_textbox_hint = this.page.getByText(this.projectDetailsIRASPageTestData.Label_Texts.iras_textbox_hint, {
+      exact: true,
+    });
+    this.iras_id_text = this.page.getByTestId('IrasId');
+    this.iras_id_text_summary_error_label = this.page.locator('.govuk-error-summary__list [href^="#IrasId"]');
   }
 
   //Page Methods
@@ -29,4 +34,24 @@ export default class ProjectDetailsIRASPage {
     await expect(this.pageHeading).toBeVisible();
     await expect(this.pageHeading).toHaveText(this.projectDetailsIRASPageTestData.Project_Details_IRAS_Page.heading);
   }
+
+  async setUniqueIrasId(value: string): Promise<void> {
+    this._unique_iras_id = value;
+  }
+
+  async getUniqueIrasId(): Promise<string> {
+    return this._unique_iras_id;
+  }
+
+  async updateUniqueIrasIdTestDataJson(filePath: string, updateVal: string) {
+    (async () => {
+      try {
+        const data = await fse.readJson(filePath);
+        data.Invalid_IRAS_ID_Duplicate.iras_id_text = updateVal;
+        await fse.writeJson(filePath, data, { spaces: 2 });
+      } catch (error) {
+        throw new Error(`${error} Error updating unique iras id to testdata json file:`);
+      }
+    })();
+  }
 }
diff --git a/src/resources/test_data/common/common_data.json b/src/resources/test_data/common/common_data.json
new file mode 100644
index 000000000..08a0d8558
--- /dev/null
+++ b/src/resources/test_data/common/common_data.json
@@ -0,0 +1,3 @@
+{
+  "error_message_summary_header": "There is a problem"
+}
diff --git a/src/resources/test_data/iras/make_changes/project_details_iras_data.json b/src/resources/test_data/iras/make_changes/project_details_iras_data.json
index 2cae907a7..9817988f5 100644
--- a/src/resources/test_data/iras/make_changes/project_details_iras_data.json
+++ b/src/resources/test_data/iras/make_changes/project_details_iras_data.json
@@ -6,13 +6,18 @@
     "iras_textbox_label": "IRAS ID",
     "iras_textbox_hint": "This is the 4 to 7 digit ID generated when you created your initial application"
   },
-  "Summary_Error_Message": {
-    "error_message_summary_header": "There is a problem",
-    "iras_id_text": "IRAS ID must only contain numbers"
-  },
   "Field_Error_Message": {
     "iras_id_text": "IRAS ID must only contain numbers"
   },
+  "Field_Error_Message_Iras_Id_Duplicate": {
+    "iras_id_text": "A record for the project with this IRAS ID already exists"
+  },
+  "Field_Error_Message_Iras_Id_Mandatory": {
+    "iras_id_text": "Enter an IRAS ID"
+  },
+  "Field_Error_Message_Iras_Id_Length": {
+    "iras_id_text": "IRAS ID must be 4 to 7 digits"
+  },
   "Valid_IRAS_ID_Max": {
     "iras_id_text": "1234567"
   },
@@ -48,5 +53,11 @@
   },
   "Invalid_IRAS_ID_Spaces_Seperator": {
     "iras_id_text": "12 34"
+  },
+  "Invalid_IRAS_ID_Blank": {
+    "iras_id_text": ""
+  },
+  "Invalid_IRAS_ID_Duplicate": {
+    "iras_id_text": "3379"
   }
-}
\ No newline at end of file
+}
diff --git a/src/steps/Common/CommonSteps.ts b/src/steps/Common/CommonSteps.ts
index 3860fb97c..f771f8aab 100644
--- a/src/steps/Common/CommonSteps.ts
+++ b/src/steps/Common/CommonSteps.ts
@@ -373,3 +373,30 @@ Then(
     }
   }
 );
+
+Then(
+  'I validate {string} displayed on {string}',
+  async (
+    { commonItemsPage, projectDetailsIRASPage },
+    errorMessageFieldAndSummaryDatasetName: string,
+    pageKey: string
+  ) => {
+    let errorMessageFieldDataset: any;
+    if (pageKey == 'Project_Details_IRAS_Page') {
+      errorMessageFieldDataset =
+        projectDetailsIRASPage.projectDetailsIRASPageTestData[errorMessageFieldAndSummaryDatasetName];
+    }
+    await expect(commonItemsPage.errorMessageSummaryLabel).toBeVisible();
+    const allSummaryErrorExpectedValues = Object.values(errorMessageFieldDataset);
+    const summaryErrorActualValues = await commonItemsPage.getSummaryErrorMessages();
+    expect(summaryErrorActualValues).toEqual(allSummaryErrorExpectedValues);
+    for (const key in errorMessageFieldDataset) {
+      if (Object.prototype.hasOwnProperty.call(errorMessageFieldDataset, key)) {
+        const fieldErrorMessagesActualValues = await commonItemsPage.getFieldErrorMessages(key, projectDetailsIRASPage);
+        expect(fieldErrorMessagesActualValues).toEqual(errorMessageFieldDataset[key]);
+        const element = await commonItemsPage.checkViewport(errorMessageFieldDataset, key, projectDetailsIRASPage);
+        await expect(element).toBeInViewport();
+      }
+    }
+  }
+);
diff --git a/src/steps/IRAS/makeChanges/ProjectDetailsIRASSteps.ts b/src/steps/IRAS/makeChanges/ProjectDetailsIRASSteps.ts
index e9724f346..a1802c063 100644
--- a/src/steps/IRAS/makeChanges/ProjectDetailsIRASSteps.ts
+++ b/src/steps/IRAS/makeChanges/ProjectDetailsIRASSteps.ts
@@ -1,5 +1,8 @@
 import { createBdd } from 'playwright-bdd';
 import { expect, test } from '../../../hooks/CustomFixtures';
+import { generateRandomNumber } from '../.././../utils/GenerateTestData';
+import path from 'path';
+const pathToTestDataJson = './src/resources/test_data/iras/make_changes/project_details_iras_data.json';
 
 const { Then } = createBdd(test);
 
@@ -32,6 +35,19 @@ Then(
   }
 );
 
+Then('I fill the unique iras id in project details iras page', async ({ projectDetailsIRASPage }) => {
+  const uniqueIrasId = generateRandomNumber();
+  await projectDetailsIRASPage.setUniqueIrasId(uniqueIrasId);
+  const filePath = path.resolve(pathToTestDataJson);
+  await projectDetailsIRASPage.updateUniqueIrasIdTestDataJson(filePath, uniqueIrasId);
+  await projectDetailsIRASPage.iras_id_text.fill(uniqueIrasId);
+});
+
+Then('I fill the existing iras id in project details iras page', async ({ projectDetailsIRASPage }) => {
+  const irasIdRunTime = await projectDetailsIRASPage.getUniqueIrasId();
+  await projectDetailsIRASPage.iras_id_text.fill(irasIdRunTime);
+});
+
 Then(
   'I can see previously saved values for {string} displayed on the project details iras page',
   async ({ commonItemsPage, projectDetailsIRASPage }, datasetName: string) => {
@@ -43,29 +59,3 @@ Then(
     }
   }
 );
-
-Then(
-  'I validate {string} and {string} displayed on project details iras page for {string}',
-  async (
-    { commonItemsPage, projectDetailsIRASPage },
-    errorMessageFieldDatasetName: string,
-    errorMessageSummaryDatasetName: string,
-    invalidFieldsDatasetName: string
-  ) => {
-    const errorMessageFieldDataset =
-      projectDetailsIRASPage.projectDetailsIRASPageTestData[errorMessageFieldDatasetName];
-    const errorMessageSummaryDataset =
-      projectDetailsIRASPage.projectDetailsIRASPageTestData[errorMessageSummaryDatasetName];
-    const invalidFieldsDataset = projectDetailsIRASPage.projectDetailsIRASPageTestData[invalidFieldsDatasetName];
-    for (const key in invalidFieldsDataset) {
-      if (Object.prototype.hasOwnProperty.call(invalidFieldsDataset, key)) {
-        await commonItemsPage.validateErrorMessage(
-          errorMessageFieldDataset,
-          errorMessageSummaryDataset,
-          key,
-          projectDetailsIRASPage
-        );
-      }
-    }
-  }
-);
diff --git a/src/utils/GenerateTestData.ts b/src/utils/GenerateTestData.ts
index 10057c8b0..a084c859f 100644
--- a/src/utils/GenerateTestData.ts
+++ b/src/utils/GenerateTestData.ts
@@ -697,3 +697,10 @@ export async function removeGeneratedTestdatafilesinTeardown(resultsFolder: stri
     console.error(err);
   }
 }
+
+export function generateRandomNumber(minLength: number = 4, maxLength: number = 7): string {
+  const min = Math.pow(10, minLength - 1);
+  const max = Math.pow(10, maxLength) - 1;
+  const randomNumber = Math.floor(Math.random() * (max - min + 1)) + min;
+  return randomNumber.toString();
+}
diff --git a/tests/features/cross-compatible/stories/systemTests/makeChanges/CreateProject.feature b/tests/features/cross-compatible/stories/systemTests/makeChanges/CreateProject.feature
index 0b13532a8..d699962c9 100644
--- a/tests/features/cross-compatible/stories/systemTests/makeChanges/CreateProject.feature
+++ b/tests/features/cross-compatible/stories/systemTests/makeChanges/CreateProject.feature
@@ -14,37 +14,37 @@ Feature: Create Amendment - Create Project
     When I click the '<Navigation_Button_Second>' button on the 'Create_Project_Record_Page'
     Then I can see the project details iras page
     And I can see the '<Validation_Text>' ui labels on the project details iras page
-    And I fill the project details iras page with '<Project_Details_IRAS>'
+    And I fill the unique iras id in project details iras page
     When I click the '<Navigation_Button_Third>' button on the 'Project_Details_IRAS_Page'
     Then I can see the project details title page
     When I click the '<Navigation_Link>' link on the 'Project_Details_Title_Page'
     Then I can see the project details iras page
-    Then I can see previously saved values for '<Project_Details_IRAS>' displayed on the project details iras page
 
     Examples:
       | Validation_Text | Navigation_Button_First | Navigation_Button_Second | Project_Details_IRAS | Navigation_Button_Third | Navigation_Link |
       | Label_Texts     | Create_Project_Record   | Start                    | Valid_IRAS_ID_Min    | Save_Continue           | Back            |
       | Label_Texts     | Create_Project_Record   | Start                    | Valid_IRAS_ID_Max    | Save_Continue           | Back            |
 
-  @rsp-1858
+  @rsp-1858 @rsp-1860 @invalidIrasIdValidations
   Scenario Outline: Validate error messages displayed when user amend the project using invalid iras id
     And I click the '<Navigation_Button_First>' button on the 'My_Research_Projects_Page'
     And I click the '<Navigation_Button_Second>' button on the 'Create_Project_Record_Page'
     And I fill the project details iras page with '<Project_Details_IRAS>'
     When I click the '<Navigation_Button_Third>' button on the 'Project_Details_IRAS_Page'
-    Then I validate '<Field_Error_Message>' and '<Summary_Error_Message>' displayed on project details iras page for '<Project_Details_IRAS>'
+    Then I validate '<Field_And_Summary_Error_Message>' displayed on 'Project_Details_IRAS_Page'
 
     Examples:
-      | Navigation_Button_First | Navigation_Button_Second | Project_Details_IRAS             | Navigation_Button_Third | Field_Error_Message | Summary_Error_Message |
-      | Create_Project_Record   | Start                    | Invalid_IRAS_ID_Letters          | Save_Continue           | Field_Error_Message | Summary_Error_Message |
-      | Create_Project_Record   | Start                    | Invalid_IRAS_ID_Symbols          | Save_Continue           | Field_Error_Message | Summary_Error_Message |
-      | Create_Project_Record   | Start                    | Invalid_IRAS_ID_Letters_Symbols  | Save_Continue           | Field_Error_Message | Summary_Error_Message |
-      | Create_Project_Record   | Start                    | Invalid_IRAS_ID_Min_Length       | Save_Continue           | Field_Error_Message | Summary_Error_Message |
-      | Create_Project_Record   | Start                    | Invalid_IRAS_ID_Max_Length       | Save_Continue           | Field_Error_Message | Summary_Error_Message |
-      | Create_Project_Record   | Start                    | Invalid_IRAS_ID_Spaces           | Save_Continue           | Field_Error_Message | Summary_Error_Message |
-      | Create_Project_Record   | Start                    | Invalid_IRAS_ID_Spaces_Suffix    | Save_Continue           | Field_Error_Message | Summary_Error_Message |
-      | Create_Project_Record   | Start                    | Invalid_IRAS_ID_Spaces_Prefix    | Save_Continue           | Field_Error_Message | Summary_Error_Message |
-      | Create_Project_Record   | Start                    | Invalid_IRAS_ID_Spaces_Seperator | Save_Continue           | Field_Error_Message | Summary_Error_Message |
+      | Navigation_Button_First | Navigation_Button_Second | Project_Details_IRAS             | Navigation_Button_Third | Field_And_Summary_Error_Message       |
+      | Create_Project_Record   | Start                    | Invalid_IRAS_ID_Letters          | Save_Continue           | Field_Error_Message                   |
+      | Create_Project_Record   | Start                    | Invalid_IRAS_ID_Symbols          | Save_Continue           | Field_Error_Message                   |
+      | Create_Project_Record   | Start                    | Invalid_IRAS_ID_Letters_Symbols  | Save_Continue           | Field_Error_Message                   |
+      | Create_Project_Record   | Start                    | Invalid_IRAS_ID_Min_Length       | Save_Continue           | Field_Error_Message_Iras_Id_Length    |
+      | Create_Project_Record   | Start                    | Invalid_IRAS_ID_Max_Length       | Save_Continue           | Field_Error_Message                   |
+      | Create_Project_Record   | Start                    | Invalid_IRAS_ID_Spaces           | Save_Continue           | Field_Error_Message_Iras_Id_Mandatory |
+      | Create_Project_Record   | Start                    | Invalid_IRAS_ID_Spaces_Suffix    | Save_Continue           | Field_Error_Message                   |
+      | Create_Project_Record   | Start                    | Invalid_IRAS_ID_Spaces_Prefix    | Save_Continue           | Field_Error_Message                   |
+      | Create_Project_Record   | Start                    | Invalid_IRAS_ID_Spaces_Seperator | Save_Continue           | Field_Error_Message                   |
+      | Create_Project_Record   | Start                    | Invalid_IRAS_ID_Blank            | Save_Continue           | Field_Error_Message_Iras_Id_Mandatory |
 
   @rsp-1858
   Scenario Outline: Validate breadcrumb navigations in project details iras id page
@@ -62,7 +62,7 @@ Feature: Create Amendment - Create Project
   Scenario Outline: Validate user is able to fill project details page for short project title and project end date
     And I click the '<Navigation_Button_First>' button on the 'My_Research_Projects_Page'
     And I click the '<Navigation_Button_Second>' button on the 'Create_Project_Record_Page'
-    And I fill the project details iras page with '<Project_Details_IRAS>'
+    And I fill the unique iras id in project details iras page
     When I click the '<Navigation_Button_Third>' button on the 'Project_Details_IRAS_Page'
     Then I can see the project details title page
     Then I can see the '<Validation_Text>' ui labels on the project details title page
@@ -91,7 +91,7 @@ Feature: Create Amendment - Create Project
   Scenario Outline: Validate error messages displayed when user fill invalid data for short project title and project end date
     And I click the '<Navigation_Button_First>' button on the 'My_Research_Projects_Page'
     And I click the '<Navigation_Button_Second>' button on the 'Create_Project_Record_Page'
-    And I fill the project details iras page with '<Project_Details_IRAS>'
+    And I fill the unique iras id in project details iras page
     And I click the '<Navigation_Button_Third>' button on the 'Project_Details_IRAS_Page'
     And I fill the project details title page with '<Project_Details_Title>'
     When I click the '<Navigation_Button_Third>' button on the 'Project_Details_Title_Page'
@@ -118,7 +118,7 @@ Feature: Create Amendment - Create Project
   Scenario Outline: Validate error messages displayed when user fill todays date for project end date
     And I click the '<Navigation_Button_First>' button on the 'My_Research_Projects_Page'
     And I click the '<Navigation_Button_Second>' button on the 'Create_Project_Record_Page'
-    And I fill the project details iras page with '<Project_Details_IRAS>'
+    And I fill the unique iras id in project details iras page
     And I click the '<Navigation_Button_Third>' button on the 'Project_Details_IRAS_Page'
     And I fill todays date for planned project end date in project details title page
     When I click the '<Navigation_Button_Third>' button on the 'Project_Details_Title_Page'
@@ -132,7 +132,7 @@ Feature: Create Amendment - Create Project
   Scenario Outline: Validate breadcrumb navigations in project details short project title page
     And I click the '<Navigation_Button_First>' button on the 'My_Research_Projects_Page'
     And I click the '<Navigation_Button_Second>' button on the 'Create_Project_Record_Page'
-    And I fill the project details iras page with '<Project_Details_IRAS>'
+    And I fill the unique iras id in project details iras page
     And I click the '<Navigation_Button_Third>' button on the 'Project_Details_IRAS_Page'
     And I can see the project details title page
     When I click the '<Navigation_Link>' link on the 'Project_Details_Title_Page'
@@ -146,7 +146,7 @@ Feature: Create Amendment - Create Project
   Scenario Outline: Validate user is able to fill key project roles page with valid data
     And I click the '<Navigation_Button_First>' button on the 'My_Research_Projects_Page'
     And I click the '<Navigation_Button_Second>' button on the 'Create_Project_Record_Page'
-    And I fill the project details iras page with '<Project_Details_IRAS>'
+    And I fill the unique iras id in project details iras page
     And I click the '<Navigation_Button_Third>' button on the 'Project_Details_IRAS_Page'
     And I fill the project details title page with '<Project_Details_Title>'
     When I click the '<Navigation_Button_Third>' button on the 'Project_Details_Title_Page'
@@ -196,7 +196,7 @@ Feature: Create Amendment - Create Project
   Scenario Outline: Validate error messages displayed when user inputs invalid data in key project roles page
     And I click the '<Navigation_Button_First>' button on the 'My_Research_Projects_Page'
     And I click the '<Navigation_Button_Second>' button on the 'Create_Project_Record_Page'
-    And I fill the project details iras page with '<Project_Details_IRAS>'
+    And I fill the unique iras id in project details iras page
     And I click the '<Navigation_Button_Third>' button on the 'Project_Details_IRAS_Page'
     And I fill the project details title page with '<Project_Details_Title>'
     And I click the '<Navigation_Button_Third>' button on the 'Project_Details_Title_Page'
@@ -239,7 +239,7 @@ Feature: Create Amendment - Create Project
   Scenario Outline: Validate breadcrumb navigations in key project roles page
     And I click the '<Navigation_Button_First>' button on the 'My_Research_Projects_Page'
     And I click the '<Navigation_Button_Second>' button on the 'Create_Project_Record_Page'
-    And I fill the project details iras page with '<Project_Details_IRAS>'
+    And I fill the unique iras id in project details iras page
     And I click the '<Navigation_Button_Third>' button on the 'Project_Details_IRAS_Page'
     And I fill the project details title page with '<Project_Details_Title>'
     And I click the '<Navigation_Button_Third>' button on the 'Project_Details_Title_Page'
@@ -257,7 +257,7 @@ Feature: Create Amendment - Create Project
   Scenario Outline: Validate user is able to fill research locations page
     And I click the '<Navigation_Button_First>' button on the 'My_Research_Projects_Page'
     And I click the '<Navigation_Button_Second>' button on the 'Create_Project_Record_Page'
-    And I fill the project details iras page with '<Project_Details_IRAS>'
+    And I fill the unique iras id in project details iras page
     And I click the '<Navigation_Button_Third>' button on the 'Project_Details_IRAS_Page'
     And I fill the project details title page with '<Project_Details_Title>'
     And I click the '<Navigation_Button_Third>' button on the 'Project_Details_Title_Page'
@@ -284,7 +284,7 @@ Feature: Create Amendment - Create Project
   Scenario Outline: Validate lead nation radio option when javascript is enabled
     And I click the '<Navigation_Button_First>' button on the 'My_Research_Projects_Page'
     And I click the '<Navigation_Button_Second>' button on the 'Create_Project_Record_Page'
-    And I fill the project details iras page with '<Project_Details_IRAS>'
+    And I fill the unique iras id in project details iras page
     And I click the '<Navigation_Button_Third>' button on the 'Project_Details_IRAS_Page'
     And I fill the project details title page with '<Project_Details_Title>'
     And I click the '<Navigation_Button_Third>' button on the 'Project_Details_Title_Page'
@@ -303,7 +303,7 @@ Feature: Create Amendment - Create Project
   Scenario Outline: Validate lead nation radio option when javascript is disabled
     And I click the '<Navigation_Button_First>' button on the 'My_Research_Projects_Page'
     And I click the '<Navigation_Button_Second>' button on the 'Create_Project_Record_Page'
-    And I fill the project details iras page with '<Project_Details_IRAS>'
+    And I fill the unique iras id in project details iras page
     And I click the '<Navigation_Button_Third>' button on the 'Project_Details_IRAS_Page'
     And I fill the project details title page with '<Project_Details_Title>'
     And I click the '<Navigation_Button_Third>' button on the 'Project_Details_Title_Page'
@@ -321,7 +321,7 @@ Feature: Create Amendment - Create Project
   Scenario Outline: Validate breadcrumb navigations in research locations page
     And I click the '<Navigation_Button_First>' button on the 'My_Research_Projects_Page'
     And I click the '<Navigation_Button_Second>' button on the 'Create_Project_Record_Page'
-    And I fill the project details iras page with '<Project_Details_IRAS>'
+    And I fill the unique iras id in project details iras page
     And I click the '<Navigation_Button_Third>' button on the 'Project_Details_IRAS_Page'
     And I fill the project details title page with '<Project_Details_Title>'
     And I click the '<Navigation_Button_Third>' button on the 'Project_Details_Title_Page'
@@ -334,3 +334,24 @@ Feature: Create Amendment - Create Project
     Examples:
       | Navigation_Button_First | Navigation_Button_Second | Project_Details_IRAS | Navigation_Button_Third | Project_Details_Title | Key_Project_Roles     | Navigation_Link |
       | Create_Project_Record   | Start                    | Valid_IRAS_ID_Min    | Save_Continue           | Valid_Data_All_Fields | Valid_Data_All_Fields | Back            |
+
+  @rsp-1860 @duplicateIrasIdValidation
+  Scenario Outline: Verify the duplicate IRAS ID error message when user enters the existing IRAS ID
+    And I click the 'Create_Project_Record' button on the 'My_Research_Projects_Page'
+    And I click the 'Start' button on the 'Create_Project_Record_Page'
+    Then I can see the project details iras page
+    And I fill the unique iras id in project details iras page
+    And I click the 'Save_Continue' button on the 'Project_Details_IRAS_Page'
+    Then I can see the project details title page
+    And  I have navigated to the my research projects page
+    And I can see the my research projects page
+    And I click the 'Create_Project_Record' button on the 'My_Research_Projects_Page'
+    And I click the 'Start' button on the 'Create_Project_Record_Page'
+    Then I can see the project details iras page
+    And I fill the existing iras id in project details iras page
+    When I click the 'Save_Continue' button on the 'Project_Details_IRAS_Page'
+    Then I validate '<Field_And_Summary_Error_Message>' displayed on 'Project_Details_IRAS_Page'
+
+    Examples:
+      | Project_Details_IRAS      | Field_And_Summary_Error_Message       |
+      | Invalid_IRAS_ID_Duplicate | Field_Error_Message_Iras_Id_Duplicate |


Reviews:
user: login: Chris-McNeill-PA-HRA
body: Address comments and previously raised points
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-28T10:31:55Z
user: login: vijay-kumar-hra
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-28T15:45:18Z
user: login: vijay-kumar-hra
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-28T15:45:28Z
user: login: vijay-kumar-hra
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-28T15:45:43Z
user: login: vijay-kumar-hra
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-28T15:48:21Z
user: login: Chris-McNeill-PA-HRA
body: Address the one remaining comment, resolve the comment and merge.
Rest of the PR is great
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-28T16:45:57Z

Review comments:
diff_hunk: @@ -371,6 +371,51 @@ export default class CommonItemsPage {
       await expect(otherLocator).toHaveText(errorMessageFieldDataset[key]);
     }
   }
+
+  async validateErrorMessageOrderAndViewport<PageObject>(
+    errorMessageFieldDataset: string,
+    errorMessageSummaryDataset: string,
+    key: string,
+    page: PageObject
+  ) {
+    const errorSummaryHeading = errorMessageSummaryDataset['error_message_summary_header'];
+    await expect(this.errorMessageSummaryLabel.getByText(errorSummaryHeading)).toBeVisible();
+    const allFieldKeys = Object.keys(errorMessageFieldDataset);
+    const summaryErrorLinks = this.errorMessageSummaryLabel.locator('ul li a');
+    const totalSummaryErrorLinks = await summaryErrorLinks.count();
+
+    for (let i = 0; i < allFieldKeys.length; i++) {
+      const fieldKey = allFieldKeys[i];
+      const element = page[fieldKey].first();
+      // Create a new locator in required pages to validate the summary error labels.
+      // The name of locator should be the same as in the test data and appended by '_summary_error_label'.
+      const summaryErrorLocatorKey = `${key}_summary_error_label`;
+      const fieldGroup = element.locator('..').locator('..');
+      const fieldErrorLocator = fieldGroup.locator(this.errorMessageFieldLabel);
+      await expect(fieldErrorLocator).toContainText(errorMessageFieldDataset[fieldKey]);
+      await expect(page[summaryErrorLocatorKey].getByText(errorMessageSummaryDataset[fieldKey])).toBeVisible();
+      if (i < totalSummaryErrorLinks) {
path: src/pages/Common/CommonItemsPage.ts
user: login: Chris-McNeill-PA-HRA
body: As mentioned on Fridays call, most of this can be removed and replaced with the `toBeInViewport` [assertion](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-be-in-viewport).

Also place that assertion and your other assertions in the step file where possible.
This method could be renamed then to `validateErrorMessageOrder`
created_at: 2025-04-28T09:36:58Z
author_association: COLLABORATOR
diff_hunk: @@ -371,6 +371,51 @@ export default class CommonItemsPage {
       await expect(otherLocator).toHaveText(errorMessageFieldDataset[key]);
     }
   }
+
+  async validateErrorMessageOrderAndViewport<PageObject>(
+    errorMessageFieldDataset: string,
+    errorMessageSummaryDataset: string,
+    key: string,
+    page: PageObject
+  ) {
+    const errorSummaryHeading = errorMessageSummaryDataset['error_message_summary_header'];
+    await expect(this.errorMessageSummaryLabel.getByText(errorSummaryHeading)).toBeVisible();
+    const allFieldKeys = Object.keys(errorMessageFieldDataset);
+    const summaryErrorLinks = this.errorMessageSummaryLabel.locator('ul li a');
+    const totalSummaryErrorLinks = await summaryErrorLinks.count();
+
+    for (let i = 0; i < allFieldKeys.length; i++) {
path: src/pages/Common/CommonItemsPage.ts
user: login: Chris-McNeill-PA-HRA
body: As mentioned on the call simplify this to an advanced for loop, which would probably be best called in the step.
Refer to steps where `fillUiComponent` method is called, e.g.:
`const dataset = keyProjectRolesPage.keyProjectRolesPageTestData[datasetName];
    for (const key in dataset) {
      if (Object.prototype.hasOwnProperty.call(dataset, key)) {
        await commonItemsPage.fillUIComponent(dataset, key, keyProjectRolesPage);
      }
    }`

In your case replacing fillUiComponent with `validateErrorMessageOrder`
created_at: 2025-04-28T09:50:53Z
author_association: COLLABORATOR
diff_hunk: @@ -1,4 +1,4 @@
-@adminUser @createProject @createAmendment @SystemTest
+@adminUser @createProject @createAmendment @SystemTest @rsp-1860
path: tests/features/cross-compatible/stories/systemTests/makeChanges/CreateProject.feature
user: login: Chris-McNeill-PA-HRA
body: Remove the ticket tag from the feature level and place it on the new scenario that you have made `@duplicateIrasIdValidation`, bcos otherwise every scenario in this feature will have that tag, and they don't relate to that user story. Only the new scenario does.
created_at: 2025-04-28T09:58:27Z
author_association: COLLABORATOR
diff_hunk: @@ -697,3 +697,14 @@ export async function removeGeneratedTestdatafilesinTeardown(resultsFolder: stri
     console.error(err);
   }
 }
+
+export function generateRandomNumber(minLength: number = 4, maxLength: number = 4): string {
path: src/utils/GenerateTestData.ts
user: login: Chris-McNeill-PA-HRA
body: As your generating an Iras Id you should set maxLength to be 7 by default.
Not sure what the purpose of the Date and getTime portions of this code are.

You can simplify the logic within using this:
```
const min = Math.pow(10, minLength - 1);
const max = Math.pow(10, maxLength) - 1;
const randomNumber = Math.floor(Math.random() * (max - min + 1)) + min;
return randomNumber.toString();
```
Which will return a random value between 1000 & 9999999
created_at: 2025-04-28T10:31:36Z
author_association: COLLABORATOR
diff_hunk: @@ -371,6 +371,51 @@ export default class CommonItemsPage {
       await expect(otherLocator).toHaveText(errorMessageFieldDataset[key]);
     }
   }
+
+  async validateErrorMessageOrderAndViewport<PageObject>(
+    errorMessageFieldDataset: string,
+    errorMessageSummaryDataset: string,
+    key: string,
+    page: PageObject
+  ) {
+    const errorSummaryHeading = errorMessageSummaryDataset['error_message_summary_header'];
+    await expect(this.errorMessageSummaryLabel.getByText(errorSummaryHeading)).toBeVisible();
+    const allFieldKeys = Object.keys(errorMessageFieldDataset);
+    const summaryErrorLinks = this.errorMessageSummaryLabel.locator('ul li a');
+    const totalSummaryErrorLinks = await summaryErrorLinks.count();
+
+    for (let i = 0; i < allFieldKeys.length; i++) {
+      const fieldKey = allFieldKeys[i];
+      const element = page[fieldKey].first();
+      // Create a new locator in required pages to validate the summary error labels.
+      // The name of locator should be the same as in the test data and appended by '_summary_error_label'.
+      const summaryErrorLocatorKey = `${key}_summary_error_label`;
+      const fieldGroup = element.locator('..').locator('..');
+      const fieldErrorLocator = fieldGroup.locator(this.errorMessageFieldLabel);
+      await expect(fieldErrorLocator).toContainText(errorMessageFieldDataset[fieldKey]);
+      await expect(page[summaryErrorLocatorKey].getByText(errorMessageSummaryDataset[fieldKey])).toBeVisible();
+      if (i < totalSummaryErrorLinks) {
path: src/pages/Common/CommonItemsPage.ts
user: login: vijay-kumar-hra
body: Updated as per the review comments 
created_at: 2025-04-28T15:45:18Z
author_association: COLLABORATOR
diff_hunk: @@ -371,6 +371,51 @@ export default class CommonItemsPage {
       await expect(otherLocator).toHaveText(errorMessageFieldDataset[key]);
     }
   }
+
+  async validateErrorMessageOrderAndViewport<PageObject>(
+    errorMessageFieldDataset: string,
+    errorMessageSummaryDataset: string,
+    key: string,
+    page: PageObject
+  ) {
+    const errorSummaryHeading = errorMessageSummaryDataset['error_message_summary_header'];
+    await expect(this.errorMessageSummaryLabel.getByText(errorSummaryHeading)).toBeVisible();
+    const allFieldKeys = Object.keys(errorMessageFieldDataset);
+    const summaryErrorLinks = this.errorMessageSummaryLabel.locator('ul li a');
+    const totalSummaryErrorLinks = await summaryErrorLinks.count();
+
+    for (let i = 0; i < allFieldKeys.length; i++) {
path: src/pages/Common/CommonItemsPage.ts
user: login: vijay-kumar-hra
body: Updated as per the review comments 
created_at: 2025-04-28T15:45:28Z
author_association: COLLABORATOR
diff_hunk: @@ -1,4 +1,4 @@
-@adminUser @createProject @createAmendment @SystemTest
+@adminUser @createProject @createAmendment @SystemTest @rsp-1860
path: tests/features/cross-compatible/stories/systemTests/makeChanges/CreateProject.feature
user: login: vijay-kumar-hra
body: Updated as per the review comments 
created_at: 2025-04-28T15:45:43Z
author_association: COLLABORATOR
diff_hunk: @@ -697,3 +697,14 @@ export async function removeGeneratedTestdatafilesinTeardown(resultsFolder: stri
     console.error(err);
   }
 }
+
+export function generateRandomNumber(minLength: number = 4, maxLength: number = 4): string {
path: src/utils/GenerateTestData.ts
user: login: vijay-kumar-hra
body: Thought of generating the IRAS ID based on time and date so that we can check when it was created , Changed it as per the review comments 
created_at: 2025-04-28T15:48:21Z
author_association: COLLABORATOR
diff_hunk: @@ -373,3 +373,30 @@ Then(
     }
   }
 );
+
+Then(
+  'I validate {string} displayed on {string}',
+  async (
+    { commonItemsPage, projectDetailsIRASPage },
+    errorMessageFieldAndSummaryDatasetName: string,
+    pageKey: string
+  ) => {
+    let errorMessageFieldDataset: any;
+    if (pageKey == 'Project_Details_IRAS_Page') {
+      errorMessageFieldDataset =
+        projectDetailsIRASPage.projectDetailsIRASPageTestData[errorMessageFieldAndSummaryDatasetName];
+    }
+    await expect(commonItemsPage.errorMessageSummaryLabel).toBeVisible();
+    const allSummaryErrorExpectedValues = Object.values(errorMessageFieldDataset);
+    const summaryErrorActualValues = await commonItemsPage.getSummaryErrorMessages();
+    expect(summaryErrorActualValues).toEqual(allSummaryErrorExpectedValues);
+    for (const key in errorMessageFieldDataset) {
+      if (Object.prototype.hasOwnProperty.call(errorMessageFieldDataset, key)) {
+        const fieldErrorMessagesActualValues = await commonItemsPage.getFieldErrorMessages(key, projectDetailsIRASPage);
+        expect(fieldErrorMessagesActualValues).toEqual(errorMessageFieldDataset[key]);
+        const element = await commonItemsPage.checkViewport(errorMessageFieldDataset, key, projectDetailsIRASPage);
+        expect(element).toBeInViewport();
path: src/steps/Common/CommonSteps.ts
user: login: Chris-McNeill-PA-HRA
body: toBeInVeiwport returns Promise<void> so you need to add an `await` here 
created_at: 2025-04-28T16:45:30Z
author_association: COLLABORATOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-04-29T08:52:22Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=HealthResearchAuthority_rsp-iras-qa&pullRequest=22) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed.svg '') [0 New issues](https://sonarcloud.io/project/issues?id=HealthResearchAuthority_rsp-iras-qa&pullRequest=22&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted.svg '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=HealthResearchAuthority_rsp-iras-qa&pullRequest=22&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed.svg '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=HealthResearchAuthority_rsp-iras-qa&pullRequest=22&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed.svg '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=HealthResearchAuthority_rsp-iras-qa&pullRequest=22&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed.svg '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=HealthResearchAuthority_rsp-iras-qa&pullRequest=22&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=HealthResearchAuthority_rsp-iras-qa&pullRequest=22)


author_association: NONE