From 1e48d9329cdc0b5cda3fcdf237e720587a5e3c29 Mon Sep 17 00:00:00 2001 From: Steve Churchill Date: Thu, 26 Feb 2026 13:45:16 +0000 Subject: [PATCH] Add events checks and utilities to e2e tests Enhance E2E test suite to validate events logging and improve navigation/utilities. Changes include: - Pages: add navigateToCloudAccountsPage (CloudAccountsPage), event filtering and multiple text/pattern locators (EventsPage), clickEvents helper (MainMenu), and getSubPoolName helper (PoolsPage). Minor locator/format fixes. - Tests: update cloud-accounts and mocked cloud-accounts suites to use centralized navigation and switch suites to serial mode; add several events-related tests (cloud account actions, invitation logging, pool/sub-pool limit updates). Adjusted tags (e.g. '@cloud-accounts') and removed redundant per-test login steps in some suites. - Utils/setup: add getCurrentUTCTimestamp in date-range-utils and import debugLog where useful; add debug logging in global teardown for orphaned sub-pools cleanup. These changes enable assertions against event log entries (including timestamp checks) and consolidate common navigation/wait patterns for more reliable tests. --- e2etests/pages/cloud-accounts-page.ts | 6 + e2etests/pages/events-page.ts | 57 +++++++++- e2etests/pages/main-menu.ts | 9 ++ e2etests/pages/pools-page.ts | 29 ++++- e2etests/setup/global-teardown.ts | 2 + e2etests/tests/cloud-accounts-tests.spec.ts | 113 +++++++++++++++---- e2etests/tests/homepage-tests.spec.ts | 1 - e2etests/tests/invitation-flow-tests.spec.ts | 33 ++++++ e2etests/tests/pools-tests.spec.ts | 41 +++++++ e2etests/utils/date-range-utils.ts | 30 +++++ 10 files changed, 291 insertions(+), 30 deletions(-) diff --git a/e2etests/pages/cloud-accounts-page.ts b/e2etests/pages/cloud-accounts-page.ts index e166ee1ff..f35e7ee1a 100644 --- a/e2etests/pages/cloud-accounts-page.ts +++ b/e2etests/pages/cloud-accounts-page.ts @@ -66,6 +66,12 @@ export class CloudAccountsPage extends BasePage { } + async navigateToCloudAccountsPage(): Promise { + await this.navigateToURL(); + await this.waitForAllProgressBarsToDisappear(); + await this.allCloudAccountLinks.last().waitFor(); + } + /** * Clicks the Add button on the Cloud Accounts page. * @returns {Promise} diff --git a/e2etests/pages/events-page.ts b/e2etests/pages/events-page.ts index 0da503fff..b36f1575b 100644 --- a/e2etests/pages/events-page.ts +++ b/e2etests/pages/events-page.ts @@ -7,6 +7,12 @@ import {BasePage} from "./base-page"; */ export class EventsPage extends BasePage { readonly heading: Locator; + readonly allBtn: Locator; + readonly infoBtn: Locator; + readonly warningBtn: Locator; + readonly errorBtn: Locator; + readonly noEventsMessage: Locator; + /** * Initializes a new instance of the EventsPage class. @@ -14,6 +20,55 @@ export class EventsPage extends BasePage { */ constructor(page: Page) { super(page, '/events'); - this.heading = this.page.getByTestId('lbl_events'); + this.heading = this.main.getByTestId('lbl_events'); + this.allBtn = this.main.getByTestId('event_lvl_all'); + this.infoBtn = this.main.getByTestId('event_lvl_info'); + this.warningBtn = this.main.getByTestId('event_lvl_warning'); + this.errorBtn = this.main.getByTestId('event_lvl_error'); + this.noEventsMessage = this.main.getByText('There are no events for selected criteria. Please try to change the filters.'); + } + + async filterByEventLevel(level: 'All' | 'Info' | 'Warning' | 'Error'): Promise { + switch (level) { + case 'All': + await this.clickButtonIfNotActive(this.allBtn); + break; + case 'Info': + await this.clickButtonIfNotActive(this.infoBtn); + break; + case 'Warning': + await this.clickButtonIfNotActive(this.warningBtn); + break; + case 'Error': + await this.clickButtonIfNotActive(this.errorBtn); + break; + } + await this.waitForAllProgressBarsToDisappear(); + } + + async getEventByText(text: string): Promise { + return this.main.locator(`//p[contains(text(), "${text}")]`) + } + + /** + * Gets an event by matching text using a RegExp pattern. + * @param {RegExp} pattern - The regular expression pattern to match. + * @returns {Locator} A locator for the event matching the pattern. + */ + getEventByPattern(pattern: RegExp): Locator { + return this.main.getByText(pattern); + } + + /** + * Gets an event by matching multiple text conditions. + * @returns {Locator} A locator for the event matching both text conditions. + * @param text + */ + getEventByMultipleTexts(text: string[]): Locator { + let xpath = '//p'; + for (const t of text) { + xpath += `[contains(text(), "${t}")]`; + } + return this.main.locator(xpath).first(); } } diff --git a/e2etests/pages/main-menu.ts b/e2etests/pages/main-menu.ts index abf12a617..9e858d6c5 100644 --- a/e2etests/pages/main-menu.ts +++ b/e2etests/pages/main-menu.ts @@ -143,4 +143,13 @@ export class MainMenu extends BasePage { async clickSettings(): Promise { await this.settingsBtn.click(); } + + /** + * Clicks the Events button. + * This method is used to navigate to the Events page. + * @returns {Promise} A promise that resolves when the Events button is clicked. + */ + async clickEvents(): Promise { + await this.eventsBtn.click(); + } } diff --git a/e2etests/pages/pools-page.ts b/e2etests/pages/pools-page.ts index 33a806a3d..1cf67fcb9 100644 --- a/e2etests/pages/pools-page.ts +++ b/e2etests/pages/pools-page.ts @@ -34,7 +34,7 @@ export class PoolsPage extends BasePage { readonly expensesThisMonthToggle: Locator; readonly forecastToggle: Locator; readonly ownerToggle: Locator; - readonly firstSubItem: Locator + readonly firstSubItem: Locator; readonly nameTableHeading: Locator; readonly monthlyLimitTableHeading: Locator; @@ -136,8 +136,7 @@ export class PoolsPage extends BasePage { this.firstSubItem = this.table.locator('//tr[@data-test-id="row_1"]'); this.domainOutlinedIcon = this.getByAnyTestId('DomainOutlinedIcon', this.table); - this.poolExpandMoreIcon = this.domainOutlinedIcon.locator( - 'xpath=/ancestor::td//*[@data-testid="ExpandMoreIcon"]'); + this.poolExpandMoreIcon = this.domainOutlinedIcon.locator('xpath=/ancestor::td//*[@data-testid="ExpandMoreIcon"]'); this.navigateNextIcon = this.getByAnyTestId('NavigateNextIcon', this.main); //Edit pools side modal @@ -334,6 +333,30 @@ export class PoolsPage extends BasePage { } } + /** + * Retrieves the name of a specific sub-pool from the table. + * + * This method adjusts the provided 1-based index to zero-based indexing, + * locates the sub-pool name element, and returns the trimmed text content. + * + * @param {number} [index=1] - The 1-based index of the sub-pool to retrieve the name for (default is 1). + * @returns {Promise} The trimmed name of the sub-pool. + * + * @example + * // Get the first sub-pool name + * const firstName = await poolsPage.getSubPoolName(); + * + * @example + * // Get the third sub-pool name + * const thirdName = await poolsPage.getSubPoolName(3); + */ + async getSubPoolName(index: number = 1): Promise { + index = index - 1; // Adjust index to be zero-based + const locator = this.subPoolNameColumn.nth(index); + const text = await locator.textContent(); + return text.trim(); + } + /** * Retrieves the monthly limit for a specific sub-pool from the table. * Adjusts the index to be zero-based, retrieves the sub-pool name for logging, diff --git a/e2etests/setup/global-teardown.ts b/e2etests/setup/global-teardown.ts index 3fef951a7..98bf2032c 100644 --- a/e2etests/setup/global-teardown.ts +++ b/e2etests/setup/global-teardown.ts @@ -12,6 +12,7 @@ import { getDatasourceIdByNameViaOpsAPI, getSubPoolIdsContainingName, } from '../utils/teardown-utils'; +import { debugLog } from '../utils/debug-logging'; async function globalTeardown() { @@ -37,6 +38,7 @@ async function globalTeardown() { if (subPoolIds.length > 1) { const marketplaceDevId = await getDatasourceIdByNameViaOpsAPI(restAPIRequest, dataSourceName); + debugLog('Orphaned Sub-pools found for Marketplace (Dev), proceeding to disconnect data source, delete sub-pools and reconnect data source to clean them up'); await disconnectDataSource(restAPIRequest, token, marketplaceDevId); await deleteSubPoolsByName(restAPIRequest, token, dataSourceName); await connectDataSource(restAPIRequest, token, dataSourceName); diff --git a/e2etests/tests/cloud-accounts-tests.spec.ts b/e2etests/tests/cloud-accounts-tests.spec.ts index f4f069aa8..4f60e6243 100644 --- a/e2etests/tests/cloud-accounts-tests.spec.ts +++ b/e2etests/tests/cloud-accounts-tests.spec.ts @@ -16,19 +16,12 @@ import { OrganizationsResponse, OrganizationThemeSettingsResponse, } from '../mocks/cloud-accounts-page.mocks'; +import { getCurrentUTCTimestamp } from '../utils/date-range-utils'; -test.describe('Cloud Accounts Tests', { tag: ['@ui', '@cloudaccounts'] }, () => { - test.describe.configure({ mode: 'default' }); +test.describe('Cloud Accounts Tests', { tag: ['@ui', '@cloud-accounts'] }, () => { + test.describe.configure({ mode: 'serial' }); test.use({ restoreSession: true }); - test.beforeEach('Login admin user', async ({ cloudAccountsPage }) => { - await test.step('Login admin user', async () => { - await cloudAccountsPage.navigateToURL(); - await cloudAccountsPage.waitForAllProgressBarsToDisappear(); - await cloudAccountsPage.allCloudAccountLinks.last().waitFor(); - }); - }); - //TODO: The first datasource has not been configured correctly in the environment. The test will need to be changed to use Marketplace (Dev) which is // the test datasource that we can configure without external dependencies. test.fixme( @@ -38,6 +31,7 @@ test.describe('Cloud Accounts Tests', { tag: ['@ui', '@cloudaccounts'] }, () => let dataSourceResponse: DataSourceBillingResponse; const now = Math.floor(Date.now() / 1000); const secondsIn24Hours = 86400; + await cloudAccountsPage.navigateToCloudAccountsPage(); await test.step('Fetch Data Source Response for first account', async () => { const [response] = await Promise.all([fetchDataSourceResponse(page), cloudAccountsPage.clickCloudAccountLink(1)]); @@ -64,7 +58,9 @@ test.describe('Cloud Accounts Tests', { tag: ['@ui', '@cloudaccounts'] }, () => test('[231861] Verify adding a new AWS Assumed role - Management', async ({ cloudAccountsPage, cloudAccountsConnectPage }) => { test.fixme(); //'Skipping due to these tests possibly corrupting data due to orphaned sub-pools when disconnecting accounts' + await cloudAccountsPage.navigateToCloudAccountsPage(); const awsAccountName = 'Marketplace (Dev)'; + await test.step(`Disconnect ${awsAccountName} if connected`, async () => { await cloudAccountsPage.disconnectIfConnectedCloudAccountByName(awsAccountName); }); @@ -86,7 +82,9 @@ test.describe('Cloud Accounts Tests', { tag: ['@ui', '@cloudaccounts'] }, () => { tag: '@p1' }, async ({ cloudAccountsPage, cloudAccountsConnectPage }) => { test.fixme(); //'Skipping due to these tests possibly corrupting data due to orphaned sub-pools when disconnecting accounts' + await cloudAccountsPage.navigateToCloudAccountsPage(); const awsAccountName = 'Marketplace (Dev)'; + await test.step(`Disconnect ${awsAccountName} if connected`, async () => { await cloudAccountsPage.disconnectIfConnectedCloudAccountByName(awsAccountName); }); @@ -106,7 +104,9 @@ test.describe('Cloud Accounts Tests', { tag: ['@ui', '@cloudaccounts'] }, () => test('[231863] Verify adding a new AWS Assumed role - Standalone', async ({ cloudAccountsPage, cloudAccountsConnectPage }) => { test.fixme(); //'Skipping due to these tests possibly corrupting data due to orphaned sub-pools when disconnecting accounts' + await cloudAccountsPage.navigateToCloudAccountsPage(); const awsAccountName = 'Marketplace (Dev)'; + await test.step(`Disconnect ${awsAccountName} if connected`, async () => { await cloudAccountsPage.disconnectIfConnectedCloudAccountByName(awsAccountName); }); @@ -129,7 +129,10 @@ test.describe('Cloud Accounts Tests', { tag: ['@ui', '@cloudaccounts'] }, () => }) => { const expectedMessage = 'We recommend using the Assume Role method to provide access to your AWS account. For more information, please see the documentation.'; + + await test.step('Navigate to add cloud account page and select AWS Access key method', async () => { + await cloudAccountsPage.navigateToCloudAccountsPage(); await cloudAccountsPage.clickAddBtn(); await cloudAccountsConnectPage.clickDataSourceTileIfNotActive(cloudAccountsConnectPage.awsRootBtn); await cloudAccountsConnectPage.clickAccessKey(); @@ -142,9 +145,11 @@ test.describe('Cloud Accounts Tests', { tag: ['@ui', '@cloudaccounts'] }, () => }); test('[232862] Verify that the user can schedule a billing reimport, and see warning alert', async ({ cloudAccountsPage }) => { - const expectedAlertMessage = 'Reimporting billing starting from the selected import date will overwrite existing billing data. This action may cause discrepancies or breaks in the current billing records and can take some time to complete. The new billing data will be imported during the next billing import report processing. Please proceed with caution, as this process cannot be undone. Ensure that this action is necessary and that you are prepared for any potential data loss and inaccuracies in billing tracking.'; + const expectedAlertMessage = + 'Reimporting billing starting from the selected import date will overwrite existing billing data. This action may cause discrepancies or breaks in the current billing records and can take some time to complete. The new billing data will be imported during the next billing import report processing. Please proceed with caution, as this process cannot be undone. Ensure that this action is necessary and that you are prepared for any potential data loss and inaccuracies in billing tracking.'; await test.step('Navigate to the Billing reimport side modal', async () => { + await cloudAccountsPage.navigateToCloudAccountsPage(); await cloudAccountsPage.clickCloudAccountLinkByName('Marketplace (Dev)'); await cloudAccountsPage.clickBillingReimportBtn(); await cloudAccountsPage.billingReimportSideModal.waitFor(); @@ -162,17 +167,16 @@ test.describe('Cloud Accounts Tests', { tag: ['@ui', '@cloudaccounts'] }, () => resp => resp.request().postData().includes('operationName":"UpdateDataSource') && resp.request().method() === 'POST' ), cloudAccountsPage.scheduleImportWithDefaultDate(), - ]) - responseStatus = response.status(); - debugLog(`API Response status: ${responseStatus}`); - expect(responseStatus).toBe(200); + ]); + responseStatus = response.status(); + debugLog(`API Response status: ${responseStatus}`); + expect(responseStatus).toBe(200); }); }); - }); -test.describe('Mocked Cloud Accounts Tests', { tag: ['@ui', '@cloudaccounts'] }, () => { - test.describe.configure({ mode: 'default' }); +test.describe('Mocked Cloud Accounts Tests', { tag: ['@ui', '@cloud-accounts'] }, () => { + test.describe.configure({ mode: 'serial' }); const apiInterceptions: InterceptionEntry[] = [ { @@ -224,13 +228,8 @@ test.describe('Mocked Cloud Accounts Tests', { tag: ['@ui', '@cloudaccounts'] }, const assumeRoleMessage = 'Switching from an access key to an assumed role is permanent. After you make this change, you can’t switch back to using an access key for this data source.If you later want to use an access key again, you’ll need to delete this data source and recreate it with access key credentials. This will delete all existing data for this data source and require a full reimport of the resource and billing data.'; - await test.step('Login admin user', async () => { - await cloudAccountsPage.navigateToURL(); - await cloudAccountsPage.waitForAllProgressBarsToDisappear(); - await cloudAccountsPage.allCloudAccountLinks.last().waitFor(); - }); - await test.step('Navigate to update credentials side modal for AWS Access Key account', async () => { + await cloudAccountsPage.navigateToCloudAccountsPage(); await cloudAccountsPage.clickCloudAccountLinkByName('Marketplace (Production)'); await cloudAccountsPage.clickUpdateCredentialsBtn(); }); @@ -251,4 +250,68 @@ test.describe('Mocked Cloud Accounts Tests', { tag: ['@ui', '@cloudaccounts'] }, await expect.soft(cloudAccountsPage.sideModalSecondaryAlert).toHaveText(permissionsMessage); }); }); -}); \ No newline at end of file +}); + + +test.describe( + '[MPT-18378] Verify Cloud Account actions are recorded correctly in the events log', + { tag: ['@ui', '@cloud-accounts', '@events'] }, + () => { + test.describe.configure({ mode: 'serial' }); + test.use({ restoreSession: true }); + + test('[232954] Verify that disconnecting and creating a cloud account is recorded in the events log', async ({ + cloudAccountsPage, + cloudAccountsConnectPage, + eventsPage, + }) => { + const awsAccountName = 'Marketplace (Dev)'; + let timestamp: string; + + await test.step('Login admin user and disconnect cloud account', async () => { + await cloudAccountsPage.navigateToCloudAccountsPage(); + timestamp = getCurrentUTCTimestamp(); + await cloudAccountsPage.disconnectCloudAccountByName(awsAccountName); + + debugLog(`Timestamp: ${timestamp}`); + }); + + await test.step('Navigate to events page and verify disconnect event is recorded with correct time', async () => { + await eventsPage.navigateToURL(); + await eventsPage.waitForAllProgressBarsToDisappear(); + + const disconnectEvent = eventsPage.getEventByMultipleTexts([`Cloud account ${awsAccountName}`, 'deleted']); + await expect.soft(disconnectEvent).toBeVisible(); + + const eventText = await disconnectEvent.textContent(); + debugLog(`Disconnect event text: ${eventText}`); + expect.soft(eventText).toContain(`${timestamp} UTC`); + }); + + await test.step('Add new cloud account and ensure that the events log includes account and pool creation', async () => { + await cloudAccountsPage.navigateToURL(); + await cloudAccountsPage.clickAddBtn(); + timestamp = getCurrentUTCTimestamp(); + debugLog(`Timestamp: ${timestamp}`); + await cloudAccountsConnectPage.addAWSAssumedRoleAccount(awsAccountName, EAWSAccountType.management); + + await eventsPage.navigateToURL(); + await eventsPage.waitForAllProgressBarsToDisappear(); + + const creationEvent = eventsPage.getEventByMultipleTexts([`Cloud account ${awsAccountName}`, 'created']); + await expect.soft(creationEvent).toBeVisible(); + + const eventText = await creationEvent.textContent(); + debugLog(`Creation event text: ${eventText}`); + expect.soft(eventText).toContain(`${timestamp} UTC`); + + const poolCreationEvent = eventsPage.getEventByMultipleTexts([`Rule for ${awsAccountName}`, `created for pool ${awsAccountName}`]); + await expect.soft(poolCreationEvent).toBeVisible(); + const poolEventText = await poolCreationEvent.textContent(); + debugLog(`Pool Creation event text: ${poolEventText}`); + expect.soft(poolEventText).toContain(`${timestamp} UTC`); + expect.soft(poolEventText).toContain(process.env.DEFAULT_USER_EMAIL); + }); + }); + } +); diff --git a/e2etests/tests/homepage-tests.spec.ts b/e2etests/tests/homepage-tests.spec.ts index b3db7c6bd..3a70c328e 100644 --- a/e2etests/tests/homepage-tests.spec.ts +++ b/e2etests/tests/homepage-tests.spec.ts @@ -135,7 +135,6 @@ test.describe('[MPT-12743] Home Page test for Pools requiring attention block', homePage, poolsPage, }) => { - test.describe.configure({ mode: 'serial' }); // The test is state dependent, so it should not run in parallel with other resource tests. await test.step('Navigate to home page', async () => { await homePage.navigateToURL(); await homePage.waitForAllProgressBarsToDisappear(); diff --git a/e2etests/tests/invitation-flow-tests.spec.ts b/e2etests/tests/invitation-flow-tests.spec.ts index 65466a938..1218cedab 100644 --- a/e2etests/tests/invitation-flow-tests.spec.ts +++ b/e2etests/tests/invitation-flow-tests.spec.ts @@ -3,6 +3,7 @@ import { test } from '../fixtures/page.fixture'; import { expect } from '@playwright/test'; import { generateRandomEmail } from '../utils/random-data-generator'; +import { debugLog } from '../utils/debug-logging'; const verificationCode = '123456'; let invitationEmail: string; @@ -389,3 +390,35 @@ test.describe('MPT-8231 Invitation Flow Tests for an existing user', { tag: ['@i }); }); }); + +test.describe( + '[MPT-18378] Verify that an invitation is recorded in the events log', + { tag: ['@invitation-flow', '@ui', '@events'] }, + () => { + test.use({ restoreSession: true }); + + test('[232868] Invite a new user and verify the event is logged', async ({ mainMenu, usersPage, usersInvitePage, eventsPage }) => { + invitationEmail = generateRandomEmail(); + + await test.step('Invite a new user to the organisation', async () => { + await usersPage.navigateToURL(); + await usersPage.clickInviteBtn(); + await usersInvitePage.inviteUser(invitationEmail); + await usersInvitePage.userInvitedAlert.waitFor(); + await usersInvitePage.userInvitedAlertCloseButton.click(); + }); + + await test.step('Navigate to Events page and verify invitation event is logged', async () => { + const date = new Date().toLocaleDateString('en-US'); // Get current date in US format (M/D/YYYY) + debugLog(`Current date: ${date}`); + await mainMenu.clickEvents(); + await eventsPage.filterByEventLevel('Info'); + const invitationEventText = await (await eventsPage.getEventByText(invitationEmail)).textContent(); + debugLog(`Invitation event text: ${invitationEventText}`); + expect(invitationEventText).toContain(date); + expect(invitationEventText.toLowerCase()).toContain(`employee ${invitationEmail} invited by`); + expect(invitationEventText).toContain(process.env.DEFAULT_USER_EMAIL); + }); + }); + } +); diff --git a/e2etests/tests/pools-tests.spec.ts b/e2etests/tests/pools-tests.spec.ts index 495f4a37c..619fc03c9 100644 --- a/e2etests/tests/pools-tests.spec.ts +++ b/e2etests/tests/pools-tests.spec.ts @@ -3,6 +3,8 @@ import { test } from '../fixtures/page.fixture'; import { expect } from '@playwright/test'; import { isWithinRoundingDrift } from '../utils/custom-assertions'; import { debugLog } from '../utils/debug-logging'; +import { getCurrentUTCTimestamp } from '../utils/date-range-utils'; +import { getEnvironmentTestOrgName } from '../utils/environment-util'; function extractMultiplier(input: string): number | null { const match = input.match(/\(x([\d.]+)\)/); @@ -361,4 +363,43 @@ test.describe('[MPT-12743] Pools Tests', { tag: ['@ui', '@pools'] }, () => { await expect.soft(poolsPage.subPoolNameColumn.first()).toBeVisible(); }); }); + + test('[232865] Verify that updating limits of pools and sub-pools is recorded in the logs', async ({ poolsPage, eventsPage }) => { + test.setTimeout(60000); + let timestamp: string; + const randomNumber = Math.floor(Math.random() * 1_000); + + await test.step('Modify a pool limit and verify event log', async () => { + timestamp = getCurrentUTCTimestamp(); + await poolsPage.editPoolMonthlyLimit(randomNumber); + await eventsPage.navigateToURL(); + const poolEvent = eventsPage.getEventByMultipleTexts([ + `Pool ${getEnvironmentTestOrgName()}`, + `updated with parameters: limit: ${randomNumber}`, + ]); + const poolEventText = await poolEvent.textContent(); + debugLog(`Pool event log text: ${poolEventText}`); + expect.soft(poolEventText).toContain(`${timestamp} UTC`); + expect.soft(poolEventText).toContain(`limit: ${randomNumber},`); + expect.soft(poolEventText).toContain(`(${process.env.DEFAULT_USER_EMAIL})`); + }); + + await test.step('Add a sub-pool limit and verify event log', async () => { + await poolsPage.navigateToURL(); + timestamp = getCurrentUTCTimestamp(); + await poolsPage.toggleExpandPool(); + const subPoolName = await poolsPage.getSubPoolName(1); + await poolsPage.editSubPoolMonthlyLimit(randomNumber, true, 1, true); + await eventsPage.navigateToURL(); + const subPoolEvent = eventsPage.getEventByMultipleTexts([ + `Pool ${subPoolName}`, + `updated with parameters: name:`, + `limit: ${randomNumber},`, + ]); + const subPoolEventText = await subPoolEvent.textContent(); + debugLog(`Sub-pool event log text: ${subPoolEventText}`); + expect.soft(subPoolEventText).toContain(`${timestamp} UTC`); + expect.soft(subPoolEventText).toContain(`(${process.env.DEFAULT_USER_EMAIL})`); + }); + }); }); diff --git a/e2etests/utils/date-range-utils.ts b/e2etests/utils/date-range-utils.ts index b89e06c54..53ba00774 100644 --- a/e2etests/utils/date-range-utils.ts +++ b/e2etests/utils/date-range-utils.ts @@ -182,3 +182,33 @@ export function getLastMonthUnixDateRange(): { startDate: number; endDate: numbe export function unixToDateString(unix: number): string { return new Date(unix * 1000).toISOString().split('T')[0]; // yyyy-mm-dd } + +/** + * Gets the current UTC timestamp formatted as a string. + * + * This function returns the current date and time in UTC with zero-padded values + * for month, day, hour, and minute in 12-hour format without seconds. + * + * @returns {string} A formatted timestamp string in the format "MM/DD/YYYY HH:MM AM/PM". + * + * @example + * // Returns something like: "02/26/2026 09:23 AM" + * const timestamp = getCurrentUTCTimestamp(); + * + * @remarks + * The format is specifically designed to match event log timestamps in the OptScale UI. + * The comma between date and time is removed to produce the final format. + */ +export function getCurrentUTCTimestamp(): string { + return new Date() + .toLocaleString('en-US', { + timeZone: 'UTC', + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + hour12: true, + }) + .replace(',', ''); +}