diff --git a/workspaces/scorecard/packages/app-legacy/e2e-tests/constants/aggregations.ts b/workspaces/scorecard/packages/app-legacy/e2e-tests/constants/aggregations.ts new file mode 100644 index 0000000000..4fa01b08fc --- /dev/null +++ b/workspaces/scorecard/packages/app-legacy/e2e-tests/constants/aggregations.ts @@ -0,0 +1,60 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const AGGREGATED_CARDS_METRIC_IDS = { + jiraMetricId: 'jira.open_issues', + githubMetricId: 'github.open_prs', + githubOpenPrsKpi: 'openPrsKpi', + jiraOpenIssuesKpi: 'openIssuesKpi', + openPrsWeightedKpi: 'openPrsWeightedKpi', +} as const; + +/** Must match `title` in App.tsx homepage widget config (Add widget picker). */ +export const AGGREGATED_CARDS_WIDGET_TITLES = { + jiraMetricId: 'Scorecard: With deprecated metricId property (Jira)', + githubMetricId: 'Scorecard: With default aggregation config (GitHub)', + githubOpenPrsKpi: 'Scorecard: GitHub open PRs', + jiraOpenIssuesKpi: 'Scorecard: Jira open blocking tickets', + openPrsWeightedKpi: 'Scorecard: GitHub open PRs (weighted health)', +} as const; + +export const AGGREGATED_CARDS_METADATA = { + deprecatedMetricId: { + id: AGGREGATED_CARDS_METRIC_IDS.jiraMetricId, + title: 'Scorecard: With deprecated metricId property (Jira)', + metricId: 'jira.open_issues', + }, + defaultAggregation: { + id: AGGREGATED_CARDS_METRIC_IDS.githubMetricId, + title: 'Scorecard: With default aggregation config (GitHub)', + metricId: 'github.open_prs', + }, + jiraOpenIssuesKpi: { + id: AGGREGATED_CARDS_METRIC_IDS.jiraOpenIssuesKpi, + title: 'Scorecard: Jira open blocking tickets', + metricId: 'jira.open_issues', + }, + githubOpenPrsKpi: { + id: AGGREGATED_CARDS_METRIC_IDS.githubOpenPrsKpi, + title: 'Scorecard: GitHub open PRs', + metricId: 'github.open_prs', + }, + openPrsWeightedKpi: { + id: AGGREGATED_CARDS_METRIC_IDS.openPrsWeightedKpi, + title: 'Scorecard: GitHub open PRs (weighted health)', + metricId: 'github.open_prs', + }, +} as const; diff --git a/workspaces/scorecard/packages/app-legacy/e2e-tests/constants/homepageWidgetTitles.ts b/workspaces/scorecard/packages/app-legacy/e2e-tests/constants/homepageWidgetTitles.ts deleted file mode 100644 index fd4cf72a53..0000000000 --- a/workspaces/scorecard/packages/app-legacy/e2e-tests/constants/homepageWidgetTitles.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Red Hat, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export const AGGREGATED_CARDS_METRIC_IDS = { - withDeprecatedMetricId: 'jira.open_issues', - withDefaultAggregation: 'github.open_prs', - withGithubOpenPrs: 'openPrsKpi', - withJiraOpenIssuesKpi: 'openIssuesKpi', - withOpenPrsWeightedKpi: 'openPrsWeightedKpi', -} as const; - -export const AGGREGATED_CARDS_WIDGET_TITLES = { - /** Must match `title` in App.tsx homepage widget config (Add widget picker). */ - withDeprecatedMetricId: 'Scorecard: With deprecated metricId property (Jira)', - withDefaultAggregation: 'Scorecard: With default aggregation config (GitHub)', - withGithubOpenPrs: 'Scorecard: GitHub open PRs', - withJiraOpenIssuesKpi: 'Scorecard: Jira open blocking tickets', - withOpenPrsWeightedKpi: 'Scorecard: GitHub open PRs (weighted health)', -} as const; diff --git a/workspaces/scorecard/packages/app-legacy/e2e-tests/pages/HomePage.ts b/workspaces/scorecard/packages/app-legacy/e2e-tests/pages/HomePage.ts index b8045e001c..591917caa6 100644 --- a/workspaces/scorecard/packages/app-legacy/e2e-tests/pages/HomePage.ts +++ b/workspaces/scorecard/packages/app-legacy/e2e-tests/pages/HomePage.ts @@ -15,7 +15,7 @@ */ import { Locator, Page, expect } from '@playwright/test'; -import { AGGREGATED_CARDS_WIDGET_TITLES } from '../constants/homepageWidgetTitles'; +import { AGGREGATED_CARDS_WIDGET_TITLES } from '../constants/aggregations'; import { ScorecardMessages, getEntityCount, @@ -63,9 +63,7 @@ export class HomePage { cardPattern = /Scorecard:\s*GitHub open PRs|ScorecardGithubHomepage/i; } else if (cardName === 'Scorecard: Jira open blocking') { cardPattern = /Scorecard:\s*Jira open blocking|ScorecardJiraHomepage/i; - } else if ( - cardName === AGGREGATED_CARDS_WIDGET_TITLES.withOpenPrsWeightedKpi - ) { + } else if (cardName === AGGREGATED_CARDS_WIDGET_TITLES.openPrsWeightedKpi) { cardPattern = /Scorecard:\s*GitHub open PRs \(weighted health\)|ScorecardOpenPrsWeightedKpi/i; } else { diff --git a/workspaces/scorecard/packages/app-legacy/e2e-tests/scorecard.test.ts b/workspaces/scorecard/packages/app-legacy/e2e-tests/scorecard.test.ts index eefc4742f4..e1da9907b7 100644 --- a/workspaces/scorecard/packages/app-legacy/e2e-tests/scorecard.test.ts +++ b/workspaces/scorecard/packages/app-legacy/e2e-tests/scorecard.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { test, expect, Page } from '@playwright/test'; +import { test, expect, Page, Locator } from '@playwright/test'; import { mockJiraAggregationResponse, mockScorecardEntitiesDrillDown, @@ -35,7 +35,6 @@ import { invalidThresholdResponse, githubAggregatedResponse, jiraAggregatedResponse, - emptyGithubAggregatedResponse, emptyJiraAggregatedResponse, openPrsKpiMetadataResponse, openPrsWeightedAggregatedResponse, @@ -65,6 +64,11 @@ import { mockAllDefaultHomepageAggregationsSuccess, mockHomepageAggregationsPermissionDenied, } from './utils/mockHomepageAggregations'; +import { + addAggregatedScorecardWidgets, + setupHomepageAggregationCard, + setupHomepageAllCardsNoData, +} from './utils/homepageWidgetUtils'; import { expectAverageCardCenterPercent, verifyAverageDonutCenterTooltip, @@ -73,33 +77,11 @@ import { import { runAccessibilityTests } from './utils/accessibility'; import { ScorecardRoutes } from './constants/routes'; import { + AGGREGATED_CARDS_METADATA, AGGREGATED_CARDS_METRIC_IDS, - AGGREGATED_CARDS_WIDGET_TITLES, -} from './constants/homepageWidgetTitles'; +} from './constants/aggregations'; import { installWebpackDevOverlayGuards } from './utils/devOverlays'; -async function addWidgets(homePage: HomePage, widgetTitle: string) { - await homePage.navigateToHome(); - await homePage.enterEditMode(); - await homePage.clearAllCards(); - await homePage.addCard(widgetTitle); - await homePage.saveChanges(); -} - -async function addAggregatedScorecardWidgets(homePage: HomePage) { - await homePage.navigateToHome(); - await homePage.enterEditMode(); - await homePage.clearAllCards(); - - await homePage.addCard(AGGREGATED_CARDS_WIDGET_TITLES.withDeprecatedMetricId); - await homePage.addCard(AGGREGATED_CARDS_WIDGET_TITLES.withDefaultAggregation); - await homePage.addCard(AGGREGATED_CARDS_WIDGET_TITLES.withGithubOpenPrs); - await homePage.addCard(AGGREGATED_CARDS_WIDGET_TITLES.withJiraOpenIssuesKpi); - await homePage.addCard(AGGREGATED_CARDS_WIDGET_TITLES.withOpenPrsWeightedKpi); - - await homePage.saveChanges(); -} - test.describe('Scorecard Plugin Tests', () => { let page: Page; let catalogPage: CatalogPage; @@ -129,11 +111,6 @@ test.describe('Scorecard Plugin Tests', () => { await page?.context()?.close(); }); - test.afterEach(async () => { - await page.unroute('**/api/scorecard/metrics/**'); - await page.unroute('**/api/scorecard/aggregations/**'); - }); - test.describe('Entity Scorecards', () => { test('Verify permission required state', async ({ browser }, testInfo) => { await mockApiResponse( @@ -462,272 +439,193 @@ test.describe('Scorecard Plugin Tests', () => { } }); + test('Verify empty aggregated response shows no data on all default homepage scorecard widgets', async () => { + await setupHomepageAllCardsNoData(page, homePage); + + for (const instanceId of Object.values(AGGREGATED_CARDS_METRIC_IDS)) { + await homePage.expectCardHasNoDataFound(instanceId); + } + }); + test('Manage scorecards on Home page', async () => { - await homePage.navigateToHome(); + await mockAllDefaultHomepageAggregationsSuccess(page); + await page.reload(); + await homePage.navigateToHome(); await homePage.enterEditMode(); await homePage.clearAllCards(); await homePage.addCard('Onboarding section'); await homePage.saveChanges(); - await homePage.expectCardNotVisible( - AGGREGATED_CARDS_METRIC_IDS.withDefaultAggregation, - ); - await homePage.expectCardNotVisible( - AGGREGATED_CARDS_METRIC_IDS.withGithubOpenPrs, - ); + for (const instanceId of Object.values(AGGREGATED_CARDS_METRIC_IDS)) { + await homePage.expectCardNotVisible(instanceId); + } - await homePage.enterEditMode(); - await homePage.addCard( - AGGREGATED_CARDS_WIDGET_TITLES.withDefaultAggregation, - ); - await homePage.addCard(AGGREGATED_CARDS_WIDGET_TITLES.withGithubOpenPrs); - await homePage.saveChanges(); + await addAggregatedScorecardWidgets(homePage); - await homePage.expectCardVisible( - AGGREGATED_CARDS_METRIC_IDS.withDefaultAggregation, - ); - await homePage.expectCardVisible( - AGGREGATED_CARDS_METRIC_IDS.withGithubOpenPrs, - ); + for (const instanceId of Object.values(AGGREGATED_CARDS_METRIC_IDS)) { + await homePage.expectCardVisible(instanceId); + } }); test.describe('Deprecated homepage card (metricId only)', () => { - test('Verify translated title and description', async () => { - await mockApiResponse( - page, - ScorecardRoutes.JIRA_OPEN_ISSUES_METRIC_AGGREGATION_ROUTE, - jiraAggregatedResponse, - ); - - await homePage.navigateToHome(); - await homePage.enterEditMode(); - await homePage.clearAllCards(); - await homePage.addCard( - AGGREGATED_CARDS_WIDGET_TITLES.withDeprecatedMetricId, - ); - await homePage.saveChanges(); + let card: Locator; + const aggregationMetadata = AGGREGATED_CARDS_METADATA.deprecatedMetricId; + + test.beforeAll(async () => { + await setupHomepageAggregationCard(page, homePage, { + aggregationMetadata, + route: ScorecardRoutes.JIRA_OPEN_ISSUES_METRIC_AGGREGATION_ROUTE, + response: jiraAggregatedResponse, + }); + card = homePage.getCard(aggregationMetadata.id); + }); - const card = homePage.getCard( - AGGREGATED_CARDS_METRIC_IDS.withDeprecatedMetricId, - ); - const metadata = - translations.metric[ - AGGREGATED_CARDS_METRIC_IDS.withDeprecatedMetricId - ]; + test('Verify translated title and description', async () => { + const translationMetadata = translations.metric[aggregationMetadata.id]; await expect(card).toBeVisible(); - await expect(card).toContainText(metadata.title); - await expect(card).toContainText(metadata.description); + await expect(card).toContainText(translationMetadata.title); + await expect(card).toContainText(translationMetadata.description); }); - test('Verify entity counts with mocked API response', async ({ - browser, - }, testInfo) => { - await mockAllDefaultHomepageAggregationsSuccess(page); - await addAggregatedScorecardWidgets(homePage); - await page.reload(); - - const card = homePage.getCard( - AGGREGATED_CARDS_METRIC_IDS.withDeprecatedMetricId, - ); - const metadata = - translations.metric[ - AGGREGATED_CARDS_METRIC_IDS.withDeprecatedMetricId - ]; + test('Verify entity counts with mocked API response', async ({}, testInfo) => { + const translationMetadata = translations.metric[aggregationMetadata.id]; await expect(card).toBeVisible(); await expect(card).toMatchAriaSnapshot( getThresholdsSnapshot(translations, { - drillDownMetricId: - AGGREGATED_CARDS_METRIC_IDS.withDeprecatedMetricId, - cardTitle: metadata.title, - cardDescription: metadata.description, + drillDownMetricId: aggregationMetadata.metricId, + cardTitle: translationMetadata.title, + cardDescription: translationMetadata.description, }), ); await runAccessibilityTests(page, testInfo); }); - test('Verify empty aggregated response shows no data', async () => { - await mockApiResponse( - page, - ScorecardRoutes.JIRA_OPEN_ISSUES_METRIC_AGGREGATION_ROUTE, - emptyJiraAggregatedResponse, + test('Verify last updated date', async () => { + const lastUpdatedFormatted = formatLastUpdatedDate( + jiraAggregatedResponse.result.timestamp, + currentLocale, ); - await addWidgets( - homePage, - AGGREGATED_CARDS_WIDGET_TITLES.withDeprecatedMetricId, - ); - await page.reload(); + await expect(card).toBeVisible(); + await homePage.verifyLastUpdatedTooltip(card, lastUpdatedFormatted); + }); - await homePage.expectCardHasNoDataFound( - AGGREGATED_CARDS_METRIC_IDS.withDeprecatedMetricId, - ); + test('Verify threshold', async () => { + await homePage.verifyThresholdTooltip(card, 'success', '6', '60%'); + await homePage.verifyThresholdTooltip(card, 'warning', '3', '30%'); + await homePage.verifyThresholdTooltip(card, 'error', '1', '10%'); }); - test('Verify threshold and last updated tooltips', async () => { - const lastUpdatedFormatted = formatLastUpdatedDate( - '2026-01-24T14:10:32.776Z', - currentLocale, - ); + test('Verify status grouped drill-down link', async () => { + await expect(card).toBeVisible(); + await homePage.clickDrillDownLink(); - await mockApiResponse( - page, - ScorecardRoutes.JIRA_OPEN_ISSUES_METRIC_AGGREGATION_ROUTE, - jiraAggregatedResponse, - ); + await scorecardDrillDownPage.expectOnPage('jira.open_issues', { + aggregationId: aggregationMetadata.id, + }); - await addWidgets( - homePage, - AGGREGATED_CARDS_WIDGET_TITLES.withDeprecatedMetricId, + const githubOpenPrsTitle = evaluateMessage( + translations.metric['jira.open_issues'].title, + 'jira.open_issues', ); - await page.reload(); - - const jiraCard = homePage.getCard( - AGGREGATED_CARDS_METRIC_IDS.withDeprecatedMetricId, + await scorecardDrillDownPage.expectPageTitle( + 'jira.open_issues', + githubOpenPrsTitle, ); - await homePage.verifyThresholdTooltip(jiraCard, 'success', '6', '60%'); - await homePage.verifyThresholdTooltip(jiraCard, 'warning', '3', '30%'); - await homePage.verifyThresholdTooltip(jiraCard, 'error', '1', '10%'); - await homePage.verifyLastUpdatedTooltip(jiraCard, lastUpdatedFormatted); }); }); test.describe('Default aggregation (aggregationId equals metric id)', () => { + let card: Locator; + const aggregationMetadata = AGGREGATED_CARDS_METADATA.defaultAggregation; + + test.beforeAll(async () => { + await setupHomepageAggregationCard(page, homePage, { + aggregationMetadata, + route: ScorecardRoutes.GITHUB_OPEN_PRS_METRIC_AGGREGATION_ROUTE, + response: githubAggregatedResponse, + }); + card = homePage.getCard(aggregationMetadata.id); + }); + // Backend: no KPI entry → aggregationId is treated as metric id (aggregation.md). test('Verify translated title and description', async () => { - await mockApiResponse( - page, - ScorecardRoutes.GITHUB_OPEN_PRS_METRIC_AGGREGATION_ROUTE, - githubAggregatedResponse, - ); - - await addWidgets( - homePage, - AGGREGATED_CARDS_WIDGET_TITLES.withDefaultAggregation, - ); - await page.reload(); - - const card = homePage.getCard( - AGGREGATED_CARDS_METRIC_IDS.withDefaultAggregation, - ); - const metadata = - translations.metric[ - AGGREGATED_CARDS_METRIC_IDS.withDefaultAggregation - ]; + const translationMetadata = translations.metric[aggregationMetadata.id]; await expect(card).toBeVisible(); - await expect(card).toContainText(metadata.title); - await expect(card).toContainText(metadata.description); + await expect(card).toContainText(translationMetadata.title); + await expect(card).toContainText(translationMetadata.description); }); - test('Verify entity counts with mocked API response', async ({ - browser, - }, testInfo) => { - await mockAllDefaultHomepageAggregationsSuccess(page); - await addAggregatedScorecardWidgets(homePage); - await page.reload(); - - const metadata = - translations.metric[ - AGGREGATED_CARDS_METRIC_IDS.withDefaultAggregation - ]; - const card = homePage.getCard( - AGGREGATED_CARDS_METRIC_IDS.withDefaultAggregation, - ); + test('Verify entity counts with mocked API response', async ({}, testInfo) => { + const translationMetadata = translations.metric[aggregationMetadata.id]; await expect(card).toBeVisible(); await expect(card).toMatchAriaSnapshot( getThresholdsSnapshot(translations, { - drillDownMetricId: - AGGREGATED_CARDS_METRIC_IDS.withDefaultAggregation, - cardTitle: metadata.title, - cardDescription: metadata.description, + drillDownMetricId: aggregationMetadata.metricId, + cardTitle: translationMetadata.title, + cardDescription: translationMetadata.description, }), ); await runAccessibilityTests(page, testInfo); }); - test('Verify empty aggregated response shows no data', async () => { - await mockApiResponse( - page, - ScorecardRoutes.GITHUB_OPEN_PRS_METRIC_AGGREGATION_ROUTE, - emptyGithubAggregatedResponse, - ); - - await addWidgets( - homePage, - AGGREGATED_CARDS_WIDGET_TITLES.withDefaultAggregation, + test('Verify last updated date', async () => { + const lastUpdatedFormatted = formatLastUpdatedDate( + githubAggregatedResponse.result.timestamp, + currentLocale, ); - await page.reload(); - await homePage.expectCardHasNoDataFound( - AGGREGATED_CARDS_METRIC_IDS.withDefaultAggregation, - ); + await expect(card).toBeVisible(); + await homePage.verifyLastUpdatedTooltip(card, lastUpdatedFormatted); }); - test('Verify threshold and last updated tooltips', async () => { - const lastUpdatedFormatted = formatLastUpdatedDate( - '2026-01-24T14:10:32.858Z', - currentLocale, - ); + test('Verify threshold', async () => { + await homePage.verifyThresholdTooltip(card, 'success', '3', '30%'); + await homePage.verifyThresholdTooltip(card, 'warning', '5', '50%'); + await homePage.verifyThresholdTooltip(card, 'error', '2', '20%'); + }); - await mockApiResponse( - page, - ScorecardRoutes.GITHUB_OPEN_PRS_METRIC_AGGREGATION_ROUTE, - githubAggregatedResponse, - ); + test('Verify open drill-down link', async () => { + await expect(card).toBeVisible(); + await homePage.clickDrillDownLink(); - await addWidgets( - homePage, - AGGREGATED_CARDS_WIDGET_TITLES.withDefaultAggregation, - ); - await page.reload(); + await scorecardDrillDownPage.expectOnPage('github.open_prs', { + aggregationId: aggregationMetadata.id, + }); - const githubCard = homePage.getCard( - AGGREGATED_CARDS_METRIC_IDS.withDefaultAggregation, - ); - await homePage.verifyThresholdTooltip( - githubCard, - 'success', - '3', - '30%', - ); - await homePage.verifyThresholdTooltip( - githubCard, - 'warning', - '5', - '50%', + const githubOpenPrsTitle = evaluateMessage( + translations.metric['github.open_prs'].title, + 'github.open_prs', ); - await homePage.verifyThresholdTooltip(githubCard, 'error', '2', '20%'); - await homePage.verifyLastUpdatedTooltip( - githubCard, - lastUpdatedFormatted, + await scorecardDrillDownPage.expectPageTitle( + 'github.open_prs', + githubOpenPrsTitle, ); }); }); - test.describe('Configured aggregation KPI (metadata labels, no metric id translation keys)', () => { - test('Verify provided title and description from API metadata', async () => { - await mockApiResponse( - page, - ScorecardRoutes.OPEN_PRS_KPI_AGGREGATION_ROUTE, - githubAggregatedResponse, - ); + test.describe('Configured aggregation KPI - "statusGrouped" type', () => { + let card: Locator; + const aggregationMetadata = AGGREGATED_CARDS_METADATA.githubOpenPrsKpi; - await addWidgets( - homePage, - AGGREGATED_CARDS_WIDGET_TITLES.withGithubOpenPrs, - ); - await page.reload(); + test.beforeAll(async () => { + await setupHomepageAggregationCard(page, homePage, { + aggregationMetadata, + route: ScorecardRoutes.OPEN_PRS_KPI_AGGREGATION_ROUTE, + response: githubAggregatedResponse, + }); - const card = homePage.getCard( - AGGREGATED_CARDS_METRIC_IDS.withGithubOpenPrs, - ); + card = homePage.getCard(aggregationMetadata.id); + }); + test('Verify title and description', async () => { await expect(card).toBeVisible(); await expect(card).toContainText(openPrsKpiMetadataResponse.title); await expect(card).toContainText( @@ -736,27 +634,12 @@ test.describe('Scorecard Plugin Tests', () => { ); }); - test('Verify entity counts with mocked API response', async ({ - browser, - }, testInfo) => { - await mockAllDefaultHomepageAggregationsSuccess(page); - await addWidgets( - homePage, - AGGREGATED_CARDS_WIDGET_TITLES.withGithubOpenPrs, - ); - await page.reload(); - - const card = homePage.getCard( - AGGREGATED_CARDS_METRIC_IDS.withGithubOpenPrs, - ); - + test('Verify entity counts with mocked API response', async ({}, testInfo) => { await expect(card).toBeVisible(); await expect(card).toMatchAriaSnapshot( getThresholdsSnapshot(translations, { - drillDownMetricId: - AGGREGATED_CARDS_METRIC_IDS.withDefaultAggregation, - drillDownAggregationId: - AGGREGATED_CARDS_METRIC_IDS.withGithubOpenPrs, + drillDownMetricId: aggregationMetadata.metricId, + drillDownAggregationId: aggregationMetadata.id, cardTitle: githubAggregatedResponse.metadata.title, cardDescription: githubAggregatedResponse.metadata.description, }), @@ -765,81 +648,135 @@ test.describe('Scorecard Plugin Tests', () => { await runAccessibilityTests(page, testInfo); }); - test('Verify empty aggregated response shows no data', async () => { - await mockApiResponse( - page, - ScorecardRoutes.OPEN_PRS_KPI_AGGREGATION_ROUTE, - emptyGithubAggregatedResponse, + test('Verify last updated date', async () => { + const lastUpdatedFormatted = formatLastUpdatedDate( + githubAggregatedResponse.result.timestamp, + currentLocale, ); + await expect(card).toBeVisible(); + await homePage.verifyLastUpdatedTooltip(card, lastUpdatedFormatted); + }); - await addWidgets( - homePage, - AGGREGATED_CARDS_WIDGET_TITLES.withGithubOpenPrs, - ); - await page.reload(); + test('Verify threshold', async () => { + await homePage.verifyThresholdTooltip(card, 'success', '3', '30%'); + await homePage.verifyThresholdTooltip(card, 'warning', '5', '50%'); + await homePage.verifyThresholdTooltip(card, 'error', '2', '20%'); + }); - await homePage.expectCardHasNoDataFound( - AGGREGATED_CARDS_METRIC_IDS.withGithubOpenPrs, + test('Verify status grouped drill-down link', async () => { + await expect(card).toBeVisible(); + await homePage.clickDrillDownLink(); + + await scorecardDrillDownPage.expectOnPage('github.open_prs', { + aggregationId: aggregationMetadata.id, + }); + await scorecardDrillDownPage.expectPageTitle( + 'github.open_prs', + githubAggregatedResponse.metadata.title, ); }); + }); - test('Verify threshold and last updated tooltips', async () => { - const githubLastUpdated = formatLastUpdatedDate( - '2026-01-24T14:10:32.858Z', - currentLocale, - ); + test.describe('Configured aggregation KPI - "average" type', () => { + const aggregationMetadata = AGGREGATED_CARDS_METADATA.openPrsWeightedKpi; - await mockApiResponse( - page, - ScorecardRoutes.OPEN_PRS_KPI_AGGREGATION_ROUTE, - githubAggregatedResponse, - ); + test.beforeAll(async () => { + await setupHomepageAggregationCard(page, homePage, { + aggregationMetadata, + route: ScorecardRoutes.OPEN_PRS_WEIGHTED_KPI_AGGREGATION_ROUTE, + response: openPrsWeightedAggregatedResponse, + }); + }); - await addWidgets( - homePage, - AGGREGATED_CARDS_WIDGET_TITLES.withGithubOpenPrs, - ); - await page.reload(); + test.describe('Validate "average" type card content', () => { + let card: Locator; - const githubCard = homePage.getCard( - AGGREGATED_CARDS_METRIC_IDS.withGithubOpenPrs, - ); - await homePage.verifyThresholdTooltip( - githubCard, - 'success', - '3', - '30%', - ); - await homePage.verifyThresholdTooltip( - githubCard, - 'warning', - '5', - '50%', - ); - await homePage.verifyThresholdTooltip(githubCard, 'error', '2', '20%'); - await homePage.verifyLastUpdatedTooltip(githubCard, githubLastUpdated); + test.beforeAll(async () => { + await homePage.navigateToHome(); + card = homePage.getCard(aggregationMetadata.id); + }); + + test('Verify title and description', async () => { + await expect(card).toBeVisible(); + await expect(card).toContainText( + openPrsWeightedKpiMetadataResponse.title, + ); + await expect(card).toContainText( + openPrsWeightedKpiMetadataResponse.description, + ); + }); + + test('Verify last updated date', async () => { + const lastUpdatedFormatted = formatLastUpdatedDate( + openPrsWeightedAggregatedResponse.result.timestamp, + currentLocale, + ); + await expect(card).toBeVisible(); + await homePage.verifyLastUpdatedTooltip(card, lastUpdatedFormatted); + }); + + test('Verify center score percentage', async () => { + await expect(card).toBeVisible(); + await expectAverageCardCenterPercent(card, '51.5%'); + }); + + test('Verify center tooltip', async () => { + await expect(card).toBeVisible(); + await verifyAverageDonutCenterTooltip( + page, + card, + translations, + openPrsWeightedAggregatedResponse.result.averageWeightedSum, + openPrsWeightedAggregatedResponse.result.averageMaxPossible, + ); + await verifyAverageCenterTooltipBreakdownRows( + page, + card, + translations, + currentLocale, + ); + }); + + test('Verify open drill-down link', async () => { + await expect(card).toBeVisible(); + await homePage.clickDrillDownLink(); + + await scorecardDrillDownPage.expectOnPage('github.open_prs', { + aggregationId: aggregationMetadata.id, + }); + await scorecardDrillDownPage.expectPageTitle( + 'github.open_prs', + openPrsWeightedAggregatedResponse.metadata.title, + ); + }); }); + test('Verify empty aggregated response shows no data', async () => { + await setupHomepageAggregationCard(page, homePage, { + aggregationMetadata, + route: ScorecardRoutes.OPEN_PRS_WEIGHTED_KPI_AGGREGATION_ROUTE, + response: emptyOpenPrsWeightedAggregatedResponse, + }); + + await homePage.expectCardHasNoDataFound(aggregationMetadata.id); + }); + }); + + test.describe('Drill down logic', () => { test('GitHub scorecard: tooltips, entity drill-down, and metric sort', async () => { - await mockApiResponse( - page, - ScorecardRoutes.OPEN_PRS_KPI_AGGREGATION_ROUTE, - githubAggregatedResponse, - ); + const aggregationMetadata = AGGREGATED_CARDS_METADATA.githubOpenPrsKpi; + await mockScorecardEntitiesDrillDownWithSort( page, githubEntitiesDrillDownResponse, 'github.open_prs', ); - await homePage.navigateToHome(); - await page.reload(); - await homePage.enterEditMode(); - await homePage.clearAllCards(); - await homePage.addCard( - AGGREGATED_CARDS_WIDGET_TITLES.withGithubOpenPrs, - ); - await homePage.saveChanges(); + await setupHomepageAggregationCard(page, homePage, { + aggregationMetadata, + route: ScorecardRoutes.OPEN_PRS_KPI_AGGREGATION_ROUTE, + response: githubAggregatedResponse, + }); const lastUpdatedFormatted = formatLastUpdatedDate( '2026-01-24T14:10:32.858Z', @@ -847,9 +784,7 @@ test.describe('Scorecard Plugin Tests', () => { ); await test.step('Verify threshold and last updated tooltips', async () => { - const githubCard = homePage.getCard( - AGGREGATED_CARDS_METRIC_IDS.withGithubOpenPrs, - ); + const githubCard = homePage.getCard(aggregationMetadata.id); await homePage.verifyThresholdTooltip( githubCard, 'success', @@ -877,7 +812,7 @@ test.describe('Scorecard Plugin Tests', () => { await test.step('Entity drill-down', async () => { await homePage.clickDrillDownLink(); await scorecardDrillDownPage.expectOnPage('github.open_prs', { - aggregationId: AGGREGATED_CARDS_METRIC_IDS.withGithubOpenPrs, + aggregationId: aggregationMetadata.id, }); await scorecardDrillDownPage.expectPageTitle( 'github.open_prs', @@ -886,7 +821,7 @@ test.describe('Scorecard Plugin Tests', () => { await scorecardDrillDownPage.expectDrillDownCardSnapshot( 'github.open_prs', { - aggregationId: AGGREGATED_CARDS_METRIC_IDS.withGithubOpenPrs, + aggregationId: aggregationMetadata.id, cardTitle: githubAggregatedResponse.metadata.title, cardDescription: githubAggregatedResponse.metadata.description, }, @@ -945,23 +880,19 @@ test.describe('Scorecard Plugin Tests', () => { }); test('Jira scorecard: tooltips, entity drill-down, and metric sort', async () => { - await mockApiResponse( - page, - ScorecardRoutes.OPEN_ISSUES_KPI_AGGREGATION_ROUTE, - jiraAggregatedResponse, - ); + const aggregationMetadata = AGGREGATED_CARDS_METADATA.jiraOpenIssuesKpi; + await mockScorecardEntitiesDrillDownWithSort( page, jiraEntitiesDrillDownResponse, 'jira.open_issues', ); - await homePage.navigateToHome(); - await page.reload(); - await homePage.enterEditMode(); - await homePage.clearAllCards(); - await homePage.addCard('Scorecard: Jira open blocking'); - await homePage.saveChanges(); + await setupHomepageAggregationCard(page, homePage, { + aggregationMetadata, + route: ScorecardRoutes.OPEN_ISSUES_KPI_AGGREGATION_ROUTE, + response: jiraAggregatedResponse, + }); const lastUpdatedFormatted = formatLastUpdatedDate( '2026-01-24T14:10:32.776Z', @@ -969,9 +900,7 @@ test.describe('Scorecard Plugin Tests', () => { ); await test.step('Verify threshold and last updated tooltips', async () => { - const jiraCard = homePage.getCard( - AGGREGATED_CARDS_METRIC_IDS.withJiraOpenIssuesKpi, - ); + const jiraCard = homePage.getCard(aggregationMetadata.id); await homePage.verifyThresholdTooltip( jiraCard, 'success', @@ -994,7 +923,7 @@ test.describe('Scorecard Plugin Tests', () => { await test.step('Entity drill-down', async () => { await homePage.clickDrillDownLink(); await scorecardDrillDownPage.expectOnPage('jira.open_issues', { - aggregationId: AGGREGATED_CARDS_METRIC_IDS.withJiraOpenIssuesKpi, + aggregationId: aggregationMetadata.id, }); await scorecardDrillDownPage.expectPageTitle( 'jira.open_issues', @@ -1003,7 +932,7 @@ test.describe('Scorecard Plugin Tests', () => { await scorecardDrillDownPage.expectDrillDownCardSnapshot( 'jira.open_issues', { - aggregationId: AGGREGATED_CARDS_METRIC_IDS.withJiraOpenIssuesKpi, + aggregationId: aggregationMetadata.id, cardTitle: jiraAggregatedResponse.metadata.title, cardDescription: jiraAggregatedResponse.metadata.description, }, @@ -1069,174 +998,19 @@ test.describe('Scorecard Plugin Tests', () => { }); }); - test.describe('Average aggregation KPI (openPrsWeightedKpi)', () => { - test('Verify title and description from API metadata', async () => { - await mockApiResponse( - page, - ScorecardRoutes.OPEN_PRS_WEIGHTED_KPI_AGGREGATION_ROUTE, - openPrsWeightedAggregatedResponse, - ); - - await addWidgets( - homePage, - AGGREGATED_CARDS_WIDGET_TITLES.withOpenPrsWeightedKpi, - ); - await page.reload(); - - const card = homePage.getCard( - AGGREGATED_CARDS_METRIC_IDS.withOpenPrsWeightedKpi, - ); - await expect(card).toBeVisible(); - await expect(card).toContainText( - openPrsWeightedKpiMetadataResponse.title, - ); - await expect(card).toContainText( - openPrsWeightedKpiMetadataResponse.description, - ); - }); - - test('Verify center score and average tooltips', async () => { - await mockApiResponse( - page, - ScorecardRoutes.OPEN_PRS_WEIGHTED_KPI_AGGREGATION_ROUTE, - openPrsWeightedAggregatedResponse, - ); - - await addWidgets( - homePage, - AGGREGATED_CARDS_WIDGET_TITLES.withOpenPrsWeightedKpi, - ); - await page.reload(); - - const card = homePage.getCard( - AGGREGATED_CARDS_METRIC_IDS.withOpenPrsWeightedKpi, - ); - await expectAverageCardCenterPercent(card, '51.5%'); - await verifyAverageDonutCenterTooltip( - page, - card, - translations, - 515, - 1000, - ); - await verifyAverageCenterTooltipBreakdownRows( - page, - card, - translations, - currentLocale, - ); - }); - - test('Verify empty aggregated response shows no data', async () => { - await mockApiResponse( - page, - ScorecardRoutes.OPEN_PRS_WEIGHTED_KPI_AGGREGATION_ROUTE, - emptyOpenPrsWeightedAggregatedResponse, - ); - - await addWidgets( - homePage, - AGGREGATED_CARDS_WIDGET_TITLES.withOpenPrsWeightedKpi, - ); - await page.reload(); - - await homePage.expectCardHasNoDataFound( - AGGREGATED_CARDS_METRIC_IDS.withOpenPrsWeightedKpi, - ); - }); - - test('Accessibility on weighted average card', async ({ - browser: _browser, - }, testInfo) => { - await mockApiResponse( - page, - ScorecardRoutes.OPEN_PRS_WEIGHTED_KPI_AGGREGATION_ROUTE, - openPrsWeightedAggregatedResponse, - ); - - await homePage.navigateToHome(); - await homePage.enterEditMode(); - await homePage.clearAllCards(); - await homePage.addCard( - AGGREGATED_CARDS_WIDGET_TITLES.withOpenPrsWeightedKpi, - ); - await homePage.saveChanges(); - await page.reload(); - - await runAccessibilityTests(page, testInfo); - }); - - test('GitHub weighted KPI: drill-down, average card, and table', async () => { - await mockApiResponse( - page, - ScorecardRoutes.OPEN_PRS_WEIGHTED_KPI_AGGREGATION_ROUTE, - openPrsWeightedAggregatedResponse, - ); - await mockScorecardEntitiesDrillDownWithSort( - page, - githubEntitiesDrillDownResponse, - 'github.open_prs', - ); - - await homePage.navigateToHome(); - await page.reload(); - await homePage.enterEditMode(); - await homePage.clearAllCards(); - await homePage.addCard( - AGGREGATED_CARDS_WIDGET_TITLES.withOpenPrsWeightedKpi, - ); - await homePage.saveChanges(); + test.describe('Unsupported aggregation type', () => { + const aggregationMetadata = AGGREGATED_CARDS_METADATA.openPrsWeightedKpi; - const weightedEntityTotal = String( - openPrsWeightedAggregatedResponse.result.total, - ); - await homePage.clickDrillDownLink({ - healthy: weightedEntityTotal, - total: weightedEntityTotal, + test.beforeAll(async () => { + await setupHomepageAggregationCard(page, homePage, { + aggregationMetadata, + route: ScorecardRoutes.OPEN_PRS_WEIGHTED_KPI_AGGREGATION_ROUTE, + response: openPrsWeightedUnsupportedAggregationResponse, }); - await scorecardDrillDownPage.expectOnPage('github.open_prs', { - aggregationId: AGGREGATED_CARDS_METRIC_IDS.withOpenPrsWeightedKpi, - }); - await scorecardDrillDownPage.expectPageTitle( - 'github.open_prs', - openPrsWeightedKpiMetadataResponse.title, - ); - - const drillCard = scorecardDrillDownPage.getDrillDownCard( - 'github.open_prs', - { - aggregationId: AGGREGATED_CARDS_METRIC_IDS.withOpenPrsWeightedKpi, - }, - ); - await expectAverageCardCenterPercent(drillCard, '51.5%'); - await scorecardDrillDownPage.expectTableHeadersVisible(); - await scorecardDrillDownPage.expectEntityNamesVisible([ - 'all-scorecards-service', - 'red-hat-developer-hub', - 'github-scorecard-only-service', - 'all-scorecards-service-different-owner', - 'backend-api', - ]); }); - }); - test.describe('Unsupported aggregation type', () => { test('Shows unsupported message when aggregationType is unknown', async () => { - await mockApiResponse( - page, - ScorecardRoutes.OPEN_PRS_WEIGHTED_KPI_AGGREGATION_ROUTE, - openPrsWeightedUnsupportedAggregationResponse, - ); - - await addWidgets( - homePage, - AGGREGATED_CARDS_WIDGET_TITLES.withOpenPrsWeightedKpi, - ); - await page.reload(); - - const card = homePage.getCard( - AGGREGATED_CARDS_METRIC_IDS.withOpenPrsWeightedKpi, - ); + const card = homePage.getCard(aggregationMetadata.id); await expect(card).toContainText( translations.errors.unsupportedAggregationType, ); diff --git a/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/apiUtils.ts b/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/apiUtils.ts index 17335d133d..94915d7f11 100644 --- a/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/apiUtils.ts +++ b/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/apiUtils.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import type { Response } from '@playwright/test'; import { Page, expect } from '@playwright/test'; import { ScorecardRoutes } from '../constants/routes'; @@ -21,6 +22,15 @@ const GITHUB_AGGREGATION_ROUTE = const JIRA_AGGREGATION_ROUTE = ScorecardRoutes.JIRA_OPEN_ISSUES_METRIC_AGGREGATION_ROUTE; +export type WaitForAggregationResponseOptions = { + status?: number; + timeout?: number; + expectedResult?: { + averageScore?: number; + total?: number; + }; +}; + export async function waitUntilApiCallSucceeds( page: Page, urlPart: string = '/api/scorecard/metrics/catalog/Component/default/red-hat-developer-hub', @@ -37,6 +47,57 @@ export async function waitUntilApiCallSucceeds( expect(response.status()).toBe(200); } +function isAggregationDataUrl(url: string, aggregationId: string): boolean { + return ( + url.includes(`/api/scorecard/aggregations/${aggregationId}`) && + !url.includes('/metadata') + ); +} + +/** + * Waits for GET /api/scorecard/aggregations/{aggregationId} (not /metadata). + * Start the returned promise before the action that triggers the fetch (e.g. page.reload). + */ +export function waitForAggregationResponse( + page: Page, + aggregationId: string, + options?: WaitForAggregationResponseOptions, +): Promise { + const status = options?.status ?? 200; + const timeout = options?.timeout ?? 60_000; + + return page.waitForResponse( + async res => { + const isStatusValid = res.status() === status; + + if (!isAggregationDataUrl(res.url(), aggregationId) || !isStatusValid) { + return false; + } + + const expected = options?.expectedResult; + if (!expected) { + return true; + } + + try { + const json = await res.json(); + const result = json?.result; + + const isAverageOk = + expected.averageScore === undefined || + result?.averageScore === expected.averageScore; + const isTotalOk = + expected.total === undefined || result?.total === expected.total; + + return isAverageOk && isTotalOk; + } catch { + return false; + } + }, + { timeout }, + ); +} + export async function mockApiResponse( page: Page, route: string, diff --git a/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/homepageWidgetUtils.ts b/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/homepageWidgetUtils.ts new file mode 100644 index 0000000000..5f73d07089 --- /dev/null +++ b/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/homepageWidgetUtils.ts @@ -0,0 +1,81 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Page } from '@playwright/test'; +import type { HomePage } from '../pages/HomePage'; +import { + AGGREGATED_CARDS_WIDGET_TITLES, + AGGREGATED_CARDS_METRIC_IDS, +} from '../constants/aggregations'; +import { mockApiResponse, waitForAggregationResponse } from './apiUtils'; +import { mockAggregationNoDataFound } from './mockHomepageAggregations'; + +type SetupHomepageAggregationCardOptions = { + aggregationMetadata: { id: string; title: string }; + route: string; + response: object; + status?: number; +}; + +async function addWidget(homePage: HomePage, widgetTitle: string) { + await homePage.navigateToHome(); + await homePage.enterEditMode(); + await homePage.clearAllCards(); + await homePage.addCard(widgetTitle); + await homePage.saveChanges(); +} + +export async function addAggregatedScorecardWidgets(homePage: HomePage) { + await homePage.navigateToHome(); + await homePage.enterEditMode(); + await homePage.clearAllCards(); + + for (const instanceId of Object.keys(AGGREGATED_CARDS_METRIC_IDS)) { + await homePage.addCard(AGGREGATED_CARDS_WIDGET_TITLES[instanceId]); + } + + await homePage.saveChanges(); +} + +export async function setupHomepageAggregationCard( + page: Page, + homePage: HomePage, + options: SetupHomepageAggregationCardOptions, +): Promise { + const { aggregationMetadata, route, response, status } = options; + + await mockApiResponse(page, route, response, status ?? 200); + + await addWidget(homePage, aggregationMetadata.title); + + // Reload clears the singleton React Query cache + await page.reload(); +} + +export async function setupHomepageAllCardsNoData( + page: Page, + homePage: HomePage, +): Promise { + await mockAggregationNoDataFound(page); + + await addAggregatedScorecardWidgets(homePage); + + const responseWaits = Object.values(AGGREGATED_CARDS_METRIC_IDS).map(id => + waitForAggregationResponse(page, id), + ); + + await Promise.all([page.reload(), ...responseWaits]); +} diff --git a/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/mockHomepageAggregations.ts b/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/mockHomepageAggregations.ts index 4c041411ca..78f5928b42 100644 --- a/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/mockHomepageAggregations.ts +++ b/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/mockHomepageAggregations.ts @@ -18,6 +18,9 @@ import type { Page } from '@playwright/test'; import { mockApiResponse } from './apiUtils'; import { ScorecardRoutes } from '../constants/routes'; import { + emptyGithubAggregatedResponse, + emptyJiraAggregatedResponse, + emptyOpenPrsWeightedAggregatedResponse, githubAggregatedResponse, jiraAggregatedResponse, notAllowedAggregationErrorBody, @@ -26,6 +29,7 @@ import { openPrsWeightedAggregatedResponse, openPrsWeightedKpiMetadataResponse, } from './scorecardResponseUtils'; +import { AGGREGATED_CARDS_METRIC_IDS } from '../constants/aggregations'; function aggregationMetadataForRequestUrl(url: string): object { if (url.includes('openIssuesKpi')) { @@ -71,6 +75,72 @@ export async function mockHomepageAggregationsPermissionDenied( }); } +export async function mockAggregationNoDataFound(page: Page): Promise { + await page.route('**/api/scorecard/aggregations/**', async route => { + const url = route.request().url(); + + if (url.includes('metadata')) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(aggregationMetadataForRequestUrl(url)), + }); + return; + } + + if (url.includes(AGGREGATED_CARDS_METRIC_IDS.openPrsWeightedKpi)) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(emptyOpenPrsWeightedAggregatedResponse), + }); + return; + } + + const githubAggregations = [ + AGGREGATED_CARDS_METRIC_IDS.githubOpenPrsKpi, + AGGREGATED_CARDS_METRIC_IDS.githubMetricId, + ]; + const isGithubAggregation = githubAggregations.some(aggregation => + url.includes(aggregation), + ); + + if (isGithubAggregation) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(emptyGithubAggregatedResponse), + }); + + return; + } + + const jiraAggregations = [ + AGGREGATED_CARDS_METRIC_IDS.jiraOpenIssuesKpi, + AGGREGATED_CARDS_METRIC_IDS.jiraMetricId, + ]; + const isJiraAggregation = jiraAggregations.some(aggregation => + url.includes(aggregation), + ); + + if (isJiraAggregation) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(emptyJiraAggregatedResponse), + }); + + return; + } + + await route.fulfill({ + status: 404, + contentType: 'application/json', + body: JSON.stringify(notAllowedAggregationErrorBody), + }); + }); +} + /** * Mocks all default homepage scorecard aggregation KPI endpoints used by * `addAggregatedScorecardWidgets` so the grid can load without hitting the real backend.