diff --git a/e2etests/mocks/home-page.mocks.ts b/e2etests/mocks/home-page.mocks.ts new file mode 100644 index 000000000..f1105e54e --- /dev/null +++ b/e2etests/mocks/home-page.mocks.ts @@ -0,0 +1,277 @@ +export const OrganisationConstraintsMock = { + organization_constraints: [ + { + deleted_at: 0, + id: '0d1f8fd4-16d9-4e6d-9d67-f0474fd2b850', + created_at: 1771926579, + name: 'Tagging required policy', + organization_id: '4eae08f8-9b40-4094-a11c-f9ee2dc76a12', + type: 'tagging_policy', + definition: { + start_date: 1771926434, + conditions: { + without_tag: 'AccountId', + }, + }, + filters: {}, + last_run: 1771926602, + last_run_result: { + value: 3119, + }, + limit_hits: [ + { + deleted_at: 0, + id: 'cc7fcf83-0db7-4295-920c-fe2f7dca24c5', + organization_id: '4eae08f8-9b40-4094-a11c-f9ee2dc76a12', + constraint_id: '0d1f8fd4-16d9-4e6d-9d67-f0474fd2b850', + constraint_limit: 0.0, + value: 3119.0, + created_at: 1771926602, + run_result: { + value: 3119, + }, + }, + ], + }, + { + deleted_at: 0, + id: '33b09374-c7b3-43ed-8909-a404bb8098b5', + created_at: 1771926729, + name: 'Prohibited tagging policy', + organization_id: '4eae08f8-9b40-4094-a11c-f9ee2dc76a12', + type: 'tagging_policy', + definition: { + start_date: 1771926629, + conditions: { + tag: '__department', + }, + }, + filters: { + active: [true], + }, + last_run: 1771926902, + last_run_result: { + value: 2, + }, + limit_hits: [ + { + deleted_at: 0, + id: '3c122ecd-5227-421b-9eb2-93a2cce24a92', + organization_id: '4eae08f8-9b40-4094-a11c-f9ee2dc76a12', + constraint_id: '33b09374-c7b3-43ed-8909-a404bb8098b5', + constraint_limit: 0.0, + value: 2.0, + created_at: 1771926902, + run_result: { + value: 2, + }, + }, + ], + }, + { + deleted_at: 0, + id: '7b419326-06f5-4bfa-bb38-78e612c78d24', + created_at: 1771926369, + name: 'Recurring budget', + organization_id: '4eae08f8-9b40-4094-a11c-f9ee2dc76a12', + type: 'recurring_budget', + definition: { + monthly_budget: 100, + }, + filters: {}, + last_run: 1771926602, + last_run_result: { + limit: 100, + current: 234445.8851078514, + }, + limit_hits: [ + { + deleted_at: 0, + id: 'c927d737-e472-428c-b621-8884c529b129', + organization_id: '4eae08f8-9b40-4094-a11c-f9ee2dc76a12', + constraint_id: '7b419326-06f5-4bfa-bb38-78e612c78d24', + constraint_limit: 100.0, + value: 234446.0, + created_at: 1771926602, + run_result: { + limit: 100, + current: 234445.8851078514, + }, + }, + ], + }, + { + deleted_at: 0, + id: '89bd9421-6dc7-44bb-93c9-ff2e763be6fe', + created_at: 1771926308, + name: 'Resource quota', + organization_id: '4eae08f8-9b40-4094-a11c-f9ee2dc76a12', + type: 'resource_quota', + definition: { + max_value: 100, + }, + filters: { + active: [true], + }, + last_run: 1771926602, + last_run_result: { + limit: 100, + current: 478, + }, + limit_hits: [ + { + deleted_at: 0, + id: '374b794a-f7b4-4d6a-9e6e-015ba022f8ab', + organization_id: '4eae08f8-9b40-4094-a11c-f9ee2dc76a12', + constraint_id: '89bd9421-6dc7-44bb-93c9-ff2e763be6fe', + constraint_limit: 100.0, + value: 478.0, + created_at: 1771926602, + run_result: { + limit: 100, + current: 478, + }, + }, + ], + }, + { + deleted_at: 0, + id: '8c3f83ca-c772-41ac-a3fa-cce8d54a1f16', + created_at: 1771926622, + name: 'Correlated tagging policy', + organization_id: '4eae08f8-9b40-4094-a11c-f9ee2dc76a12', + type: 'tagging_policy', + definition: { + start_date: 1771926584, + conditions: { + tag: 'CostCenter', + without_tag: 'Environment', + }, + }, + filters: {}, + last_run: 1771926902, + last_run_result: { + value: 1, + }, + limit_hits: [ + { + deleted_at: 0, + id: '34a26573-33ca-4055-a21f-52403f1d68f5', + organization_id: '4eae08f8-9b40-4094-a11c-f9ee2dc76a12', + constraint_id: '8c3f83ca-c772-41ac-a3fa-cce8d54a1f16', + constraint_limit: 0.0, + value: 1.0, + created_at: 1771926902, + run_result: { + value: 1, + }, + }, + ], + }, + { + deleted_at: 0, + id: '9f16e4d0-16d6-4ec8-adb8-fde69fdebafd', + created_at: 1744898002, + name: 'Default - resource count anomaly', + organization_id: '4eae08f8-9b40-4094-a11c-f9ee2dc76a12', + type: 'resource_count_anomaly', + definition: { + threshold_days: 7, + threshold: 30, + }, + filters: {}, + last_run: 1771923003, + last_run_result: { + average: 3385.714285714286, + today: 3202, + breakdown: { + '1767657600': 3399, + '1767744000': 3392, + '1767830400': 3407, + '1767916800': 3408, + '1768003200': 3376, + '1768089600': 3352, + '1768176000': 3366, + }, + }, + limit_hits: [], + }, + { + deleted_at: 0, + id: 'ac9073f8-712d-46ee-9632-9694e999b777', + created_at: 1744898002, + name: 'Default - expense anomaly', + organization_id: '4eae08f8-9b40-4094-a11c-f9ee2dc76a12', + type: 'expense_anomaly', + definition: { + threshold_days: 7, + threshold: 30, + }, + filters: {}, + last_run: 1771923902, + last_run_result: { + average: 7621.639588907393, + today: 63376.15244631838, + breakdown: { + '1770595200': 7566.475845989279, + '1770249600': 7581.231650058954, + '1770681600': 7670.8199591838975, + '1770336000': 7546.220103338065, + '1770422400': 7674.9010295840135, + '1770508800': 7406.122633114845, + '1770768000': 7905.705901082702, + }, + }, + limit_hits: [], + }, + { + deleted_at: 0, + id: 'e43668ee-d1ff-4a94-b772-b85186728165', + created_at: 1771926430, + name: 'Expiring budget', + organization_id: '4eae08f8-9b40-4094-a11c-f9ee2dc76a12', + type: 'expiring_budget', + definition: { + total_budget: 10, + start_date: 1771926378, + }, + filters: {}, + last_run: 1771926602, + last_run_result: { + limit: 10, + current: 1175.8189694743335, + }, + limit_hits: [ + { + deleted_at: 0, + id: '4e52b0d2-05e9-49e8-aa06-6f987b212e68', + organization_id: '4eae08f8-9b40-4094-a11c-f9ee2dc76a12', + constraint_id: 'e43668ee-d1ff-4a94-b772-b85186728165', + constraint_limit: 10.0, + value: 1175.82, + created_at: 1771926602, + run_result: { + limit: 10, + current: 1175.8189694743335, + }, + }, + ], + }, + { + deleted_at: 0, + id: 'fb5d60b4-a196-493a-8ad0-b21071ad55a7', + created_at: 1771926104, + name: 'Resource Count', + organization_id: '4eae08f8-9b40-4094-a11c-f9ee2dc76a12', + type: 'resource_count_anomaly', + definition: { + threshold_days: 7, + threshold: 10, + }, + filters: {}, + last_run: 1771926303, + last_run_result: {}, + limit_hits: [], + }, + ], +}; diff --git a/e2etests/pages/home-page.ts b/e2etests/pages/home-page.ts index 259775b85..e6f81840f 100644 --- a/e2etests/pages/home-page.ts +++ b/e2etests/pages/home-page.ts @@ -35,6 +35,16 @@ export class HomePage extends BasePage { //Policy violations block readonly policyViolationsBlock: Locator; + readonly policyViolationsTable: Locator; + readonly policyViolationsNavigateNextBtn: Locator; + readonly correlatedTaggingRow: Locator; + readonly defaultExpenseAnomalyRow: Locator; + readonly expiringBudgetRow: Locator; + readonly prohibitedTaggingRow: Locator; + readonly recurringBudgetRow: Locator; + readonly resourceQuotaRow: Locator; + readonly taggingRequiredRow: Locator; + readonly defaultResourceCountAnomalyRow: Locator; //Pools requiring attention block readonly poolsRequiringAttentionBlock: Locator; @@ -86,6 +96,16 @@ export class HomePage extends BasePage { //Policy violations block this.policyViolationsBlock = this.page.getByTestId('block_policies_violations'); + this.policyViolationsTable = this.policyViolationsBlock.locator('table'); + this.policyViolationsNavigateNextBtn = this.getByAnyTestId('NavigateNextIcon', this.policyViolationsBlock); + this.correlatedTaggingRow = this.policyViolationsTable.locator('tbody tr').filter({ hasText: 'Correlated tagging policy' }); + this.expiringBudgetRow = this.policyViolationsTable.locator('tbody tr').filter({ hasText: 'Expiring budget' }); + this.prohibitedTaggingRow = this.policyViolationsTable.locator('tbody tr').filter({ hasText: 'Prohibited tagging policy' }); + this.recurringBudgetRow = this.policyViolationsTable.locator('tbody tr').filter({ hasText: 'Recurring budget' }); + this.resourceQuotaRow = this.policyViolationsTable.locator('tbody tr').filter({ hasText: 'Resource quota' }); + this.taggingRequiredRow = this.policyViolationsTable.locator('tbody tr').filter({ hasText: 'Tagging required policy' }); + this.defaultExpenseAnomalyRow = this.policyViolationsTable.locator('tbody tr').filter({ hasText: 'Default - expense anomaly' }); + this.defaultResourceCountAnomalyRow = this.policyViolationsTable.locator('tbody tr').filter({ hasText: 'Default - resource count anomaly' }); //Pools requiring attention block this.poolsRequiringAttentionBlock = this.main.getByTestId('block_pools'); @@ -285,4 +305,10 @@ export class HomePage extends BasePage { await this.poolsReqAttnExceededForecastedOverspendBtn.click(); await this.waitForPageLoad(); } + + async getPolicyViolationsTableRowByText(text: string): Promise { + const row = this.policyViolationsTable.locator('tbody tr').filter({ hasText: text }); + await row.waitFor(); + return row; + } } diff --git a/e2etests/setup/auth.setup.ts b/e2etests/setup/auth.setup.ts index 38b340cea..b203f12c6 100644 --- a/e2etests/setup/auth.setup.ts +++ b/e2etests/setup/auth.setup.ts @@ -40,11 +40,11 @@ setup.describe('Auth Setup', () => { await page.getByTestId('input_pass').fill(password); await page.getByTestId('btn_login').click(); const initializingMessage = page.getByTestId('p_initializing') - await initializingMessage.waitFor({ timeout: 10000 }); + await initializingMessage.waitFor({ timeout: 20000 }); await initializingMessage.waitFor({ state: 'detached', timeout: 20000 }); const loadingImage = page.getByRole('img', { name: 'Loading page' }); await loadingImage.waitFor(); - await loadingImage.waitFor({ state: 'detached', timeout: 10000 }); + await loadingImage.waitFor({ state: 'detached', timeout: 20000 }); const authValue = await getLocalforageRoot(page); const storageState = await page.context().storageState(); diff --git a/e2etests/tests/anomalies-tests.spec.ts b/e2etests/tests/anomalies-tests.spec.ts index 057779e2d..4d70d6c3a 100644 --- a/e2etests/tests/anomalies-tests.spec.ts +++ b/e2etests/tests/anomalies-tests.spec.ts @@ -390,6 +390,7 @@ test.describe('[MPT-14737] Mocked Anomalies Tests', { tag: ['@ui', '@anomalies'] match = await comparePngImages(expectedPath, actualPath, diffPath); expect.soft(match).toBe(true); }); + test('[231439] Verify detected anomalies are displayed in the table correctly', async ({ anomaliesPage }) => { await anomaliesPage.page.clock.setFixedTime(new Date('2025-11-11T14:11:00Z')); await anomaliesPage.navigateToURL(); diff --git a/e2etests/tests/homepage-tests.spec.ts b/e2etests/tests/homepage-tests.spec.ts index 7c46c8e2e..b3db7c6bd 100644 --- a/e2etests/tests/homepage-tests.spec.ts +++ b/e2etests/tests/homepage-tests.spec.ts @@ -1,6 +1,8 @@ import { test } from '../fixtures/page.fixture'; import { expect } from '@playwright/test'; import { isWithinRoundingDrift } from '../utils/custom-assertions'; +import { InterceptionEntry } from '../types/interceptor.types'; +import { OrganisationConstraintsMock } from '../mocks/home-page.mocks'; test.describe('[MPT-11464] Home Page Recommendations block tests', { tag: ['@ui', '@recommendations', '@homepage'] }, () => { test.describe.configure({ mode: 'default' }); @@ -15,12 +17,16 @@ test.describe('[MPT-11464] Home Page Recommendations block tests', { tag: ['@ui' }); }); - test('[230550] Compare possible savings on home page with those on recommendations page', {tag: '@p1'}, async ({ homePage, recommendationsPage }) => { - const homePageValue = await homePage.getRecommendationsPossibleSavingsValue(); - await homePage.recommendationsBtn.click(); - const recommendationsPageValue = await recommendationsPage.getPossibleMonthlySavingsValue(); - expect.soft(homePageValue).toBe(recommendationsPageValue); - }); + test( + '[230550] Compare possible savings on home page with those on recommendations page', + { tag: '@p1' }, + async ({ homePage, recommendationsPage }) => { + const homePageValue = await homePage.getRecommendationsPossibleSavingsValue(); + await homePage.recommendationsBtn.click(); + const recommendationsPageValue = await recommendationsPage.getPossibleMonthlySavingsValue(); + expect.soft(homePageValue).toBe(recommendationsPageValue); + } + ); test('[230551] Verify Cost items displayed in the recommendations block match the sum total of items displayed on cards with savings', async ({ homePage, @@ -74,38 +80,38 @@ test.describe('[MPT-11958] Home Page Resource block tests', { tag: ['@ui', '@res }); }); - test('[230839] Verify top Resource link navigates to the correct resource details page and last 30 days value match', {tag: '@p1'}, async ({ - homePage, - resourceDetailsPage, - datePicker, - }) => { - let homepageResourceTitle: string; - let homePageExpenseValue: number; - await test.step("Get first resource's homepage values", async () => { - await homePage.topResourcesAllLinks.last().waitFor(); - homepageResourceTitle = await homePage.getFirstResourceTitle(); - homePageExpenseValue = await homePage.getFirstResourceValue(); - expect.soft(homepageResourceTitle).toBeTruthy(); - }); + test( + '[230839] Verify top Resource link navigates to the correct resource details page and last 30 days value match', + { tag: '@p1' }, + async ({ homePage, resourceDetailsPage, datePicker }) => { + let homepageResourceTitle: string; + let homePageExpenseValue: number; + await test.step("Get first resource's homepage values", async () => { + await homePage.topResourcesAllLinks.last().waitFor(); + homepageResourceTitle = await homePage.getFirstResourceTitle(); + homePageExpenseValue = await homePage.getFirstResourceValue(); + expect.soft(homepageResourceTitle).toBeTruthy(); + }); - await test.step('Click on the first resource link and verify navigation', async () => { - await homePage.clickFirstTopResourceLink(); - await expect.soft(resourceDetailsPage.heading).toContainText(homepageResourceTitle); - }); + await test.step('Click on the first resource link and verify navigation', async () => { + await homePage.clickFirstTopResourceLink(); + await expect.soft(resourceDetailsPage.heading).toContainText(homepageResourceTitle); + }); - await test.step('Click expenses tab and set date range to last 30 days', async () => { - await resourceDetailsPage.clickExpensesTab(); - await datePicker.selectLast30DaysDateRange(); - }); + await test.step('Click expenses tab and set date range to last 30 days', async () => { + await resourceDetailsPage.clickExpensesTab(); + await datePicker.selectLast30DaysDateRange(); + }); - await test.step('Verify that the expenses column total matches the home page last 30 days expenses value', async () => { - const expenseTotal = await resourceDetailsPage.sumCurrencyColumn( - resourceDetailsPage.tableColumn2, - resourceDetailsPage.navigateNextIcon - ); - expect(isWithinRoundingDrift(homePageExpenseValue, expenseTotal, 0.0001)).toBe(true); //0.01% drift is acceptable for the test - }); - }); + await test.step('Verify that the expenses column total matches the home page last 30 days expenses value', async () => { + const expenseTotal = await resourceDetailsPage.sumCurrencyColumn( + resourceDetailsPage.tableColumn2, + resourceDetailsPage.navigateNextIcon + ); + expect(isWithinRoundingDrift(homePageExpenseValue, expenseTotal, 0.0001)).toBe(true); //0.01% drift is acceptable for the test + }); + } + ); test('[230842] Verify Top Resource Block displayed correctly', async ({ homePage }) => { await test.step('Verify that the Top Resources section is displayed with 6 or fewer resources and include names for each', async () => { @@ -129,6 +135,7 @@ 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(); @@ -240,3 +247,71 @@ test.describe('[MPT-12743] Home Page test for Pools requiring attention block', }); }); }); + +test.describe('[MPT-18353] Home Page test for Policy Violation block', { tag: ['@ui', '@anomalies', '@homepage'] }, () => { + const apiInterceptions: InterceptionEntry[] = [ + { + url: '/v2/organizations/[^/]+/organization_constraints\\?hit_days=3&type=resource_count_anomaly&type=expense_anomaly&type=resource_quota&type=recurring_budget&type=expiring_budget&type=tagging_policy', + mock: OrganisationConstraintsMock, + }, + ]; + + test.use({ + restoreSession: true, + interceptAPI: { entries: apiInterceptions, failOnInterceptionMissing: true }, + }); + + test('[232876] Verify that Policy Violation block displays policy violations correctly', async ({ homePage }) => { + await test.step('Navigate to home page', async () => { + await homePage.page.clock.setFixedTime(new Date('2026-02-24T11:00:00Z')); + await homePage.navigateToURL(); + await homePage.waitForAllCanvases(); + }); + + const typeColumn = '//td[2]'; + const statusColumn = '//td[3]'; + + await test.step('Verify that the Policy Violation block first page is displayed with correct data', async () => { + const NBSP = '\u00A0'; + + await expect.soft(homePage.correlatedTaggingRow.locator(typeColumn)).toHaveText('Tagging'); + await expect.soft(homePage.correlatedTaggingRow.locator(statusColumn)).toHaveText('1 violation right now'); + + await expect.soft(homePage.defaultExpenseAnomalyRow.locator(typeColumn)).toHaveText('Anomaly'); + await homePage.defaultExpenseAnomalyRow.locator(statusColumn).hover(); + await expect.soft(homePage.tooltip).toHaveText(`Average:${NBSP}$7,621.64Today:${NBSP}$63,376.15`); + + await expect.soft(homePage.expiringBudgetRow.locator(`${typeColumn}`)).toHaveText('Quota/Budget'); + await expect.soft(homePage.expiringBudgetRow.locator(`${statusColumn}/div/div`)).toHaveText('$1,175.82'); + expect + .soft(await homePage.getColorFromElement(homePage.expiringBudgetRow.locator(`${statusColumn}/div/div`))) + .toBe(homePage.errorColor); + + await expect.soft(homePage.prohibitedTaggingRow.locator(typeColumn)).toHaveText('Tagging'); + await expect.soft(homePage.prohibitedTaggingRow.locator(statusColumn)).toHaveText('2 violations right now'); + + await expect.soft(homePage.recurringBudgetRow.locator(`${typeColumn}`)).toHaveText('Quota/Budget'); + await expect.soft(homePage.recurringBudgetRow.locator(`${statusColumn}/div/div`)).toHaveText('$234,445.89'); + expect + .soft(await homePage.getColorFromElement(homePage.recurringBudgetRow.locator(`${statusColumn}/div/div`))) + .toBe(homePage.errorColor); + + await expect.soft(homePage.defaultResourceCountAnomalyRow).toBeHidden(); + }); + + await test.step('Verify second page of the Policy Violation block is displayed with correct data when clicking on the pagination button', async () => { + await homePage.policyViolationsNavigateNextBtn.click(); + + await expect.soft(homePage.resourceQuotaRow.locator(`${typeColumn}`)).toHaveText('Quota/Budget'); + await expect.soft(homePage.resourceQuotaRow.locator(`${statusColumn}/div/div`)).toHaveText('478'); + expect + .soft(await homePage.getColorFromElement(homePage.resourceQuotaRow.locator(`${statusColumn}/div/div`))) + .toBe(homePage.errorColor); + + await expect.soft(homePage.taggingRequiredRow.locator(`${typeColumn}`)).toHaveText('Tagging'); + await expect.soft(homePage.taggingRequiredRow.locator(`${statusColumn}`)).toHaveText('3119 violations right now'); + + await expect.soft(homePage.defaultResourceCountAnomalyRow).toBeHidden(); + }); + }); +});