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..7fd1f007ba --- /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', + gitHubOpenPrsWeightedKpi: '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', + gitHubOpenPrsWeightedKpi: 'Scorecard: GitHub open PRs (weighted health)', +} as const; + +export const AGGREGATED_CARDS_METADATA = { + jiraDeprecatedMetricId: { + id: AGGREGATED_CARDS_METRIC_IDS.jiraMetricId, + title: 'Scorecard: With deprecated metricId property (Jira)', + metricId: 'jira.open_issues', + }, + githubDefaultAggregation: { + 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', + }, + githubOpenPrsWeightedKpi: { + id: AGGREGATED_CARDS_METRIC_IDS.gitHubOpenPrsWeightedKpi, + 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..5bf3e51bf3 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 { @@ -131,10 +129,13 @@ export class HomePage { } /** - * Clicks the homepage KPI drill-down link (healthy/total subheader). + * Clicks the homepage KPI drill-down link (healthy/total subheader) within a card. * Defaults to 10/10 (plain “10 entities” link). Pass overrides when mock `result.total` differs. */ - async clickDrillDownLink(options?: { healthy?: string; total?: string }) { + async clickDrillDownLink( + card: Locator, + options?: { healthy?: string; total?: string }, + ) { const healthy = options?.healthy ?? '10'; const total = options?.total ?? '10'; const name = getHomepageEntityCalculationHealthText( @@ -142,7 +143,6 @@ export class HomePage { healthy, total, ); - // Multiple homepage scorecards can share the same health string; target the first match. - await this.page.getByRole('link', { name }).first().click(); + await card.getByRole('link', { name }).click(); } } 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..0539acdd02 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,9 +35,7 @@ import { invalidThresholdResponse, githubAggregatedResponse, jiraAggregatedResponse, - emptyGithubAggregatedResponse, emptyJiraAggregatedResponse, - openPrsKpiMetadataResponse, openPrsWeightedAggregatedResponse, emptyOpenPrsWeightedAggregatedResponse, openPrsWeightedKpiMetadataResponse, @@ -50,6 +48,9 @@ import { sonarqubeScorecardResponse, sonarqubeFailedQualityGateResponse, fileCheckScorecardResponse, + githubCustomAggregatedResponse, + gitHubPartiallyAggregatedResponse, + gitHubWeightedPartiallyAggregatedResponse, } from './utils/scorecardResponseUtils'; import { ScorecardMessages, @@ -57,7 +58,8 @@ import { formatLastUpdatedDate, getTranslations, getEntityCount, - getThresholdsSnapshot, + getStatusGroupedCardSnapshot, + getAverageCardSnapshot, getTableFooterSnapshot, getEntitiesTableFooterRowsLabel, } from './utils/translationUtils'; @@ -65,6 +67,11 @@ import { mockAllDefaultHomepageAggregationsSuccess, mockHomepageAggregationsPermissionDenied, } from './utils/mockHomepageAggregations'; +import { + addAggregatedScorecardWidgets, + setupHomepageAggregationCard, + setupHomepageAllCardsNoData, +} from './utils/homepageWidgetUtils'; import { expectAverageCardCenterPercent, verifyAverageDonutCenterTooltip, @@ -73,33 +80,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 +114,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( @@ -447,7 +427,6 @@ test.describe('Scorecard Plugin Tests', () => { test('Verify missing permission on all default homepage scorecard widgets', async () => { await mockHomepageAggregationsPermissionDenied(page); await addAggregatedScorecardWidgets(homePage); - await page.reload(); const entityCount = getEntityCount(translations, currentLocale, '0'); @@ -462,384 +441,411 @@ 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 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.jiraDeprecatedMetricId; + + 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, + getStatusGroupedCardSnapshot(translations, { + 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(card); - 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 jiraOpenIssuesTitle = 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', + jiraOpenIssuesTitle, ); - 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.githubDefaultAggregation; + + 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, + getStatusGroupedCardSnapshot(translations, { + 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(card); - 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; - await addWidgets( - homePage, - AGGREGATED_CARDS_WIDGET_TITLES.withGithubOpenPrs, - ); - await page.reload(); + const aggregationMetadata = AGGREGATED_CARDS_METADATA.githubOpenPrsKpi; + const aggregatedResponse = githubCustomAggregatedResponse; - const card = homePage.getCard( - AGGREGATED_CARDS_METRIC_IDS.withGithubOpenPrs, - ); + test.beforeAll(async () => { + await setupHomepageAggregationCard(page, homePage, { + aggregationMetadata, + route: ScorecardRoutes.OPEN_PRS_KPI_AGGREGATION_ROUTE, + response: aggregatedResponse, + }); + 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(aggregatedResponse.metadata.title); await expect(card).toContainText( - openPrsKpiMetadataResponse.description, + aggregatedResponse.metadata.description, { timeout: 15000 }, ); }); - 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, - cardTitle: githubAggregatedResponse.metadata.title, - cardDescription: githubAggregatedResponse.metadata.description, + getStatusGroupedCardSnapshot(translations, { + drillDownMetricId: aggregationMetadata.metricId, + drillDownAggregationId: aggregationMetadata.id, + cardTitle: aggregatedResponse.metadata.title, + cardDescription: aggregatedResponse.metadata.description, + homepageCalculationHealth: { + healthy: aggregatedResponse.result.entitiesConsidered.toString(), + total: aggregatedResponse.result.total.toString(), + }, }), ); 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( + aggregatedResponse.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', '2', '25%'); + await homePage.verifyThresholdTooltip(card, 'warning', '1', '13%'); + await homePage.verifyThresholdTooltip(card, 'error', '5', '63%'); + }); + + test('Verify status grouped drill-down link', async () => { + await expect(card).toBeVisible(); + await homePage.clickDrillDownLink(card, { healthy: '8', total: '8' }); - await homePage.expectCardHasNoDataFound( - AGGREGATED_CARDS_METRIC_IDS.withGithubOpenPrs, + await scorecardDrillDownPage.expectOnPage('github.open_prs', { + aggregationId: aggregationMetadata.id, + }); + await scorecardDrillDownPage.expectPageTitle( + 'github.open_prs', + aggregatedResponse.metadata.title, ); }); - test('Verify threshold and last updated tooltips', async () => { - const githubLastUpdated = formatLastUpdatedDate( - '2026-01-24T14:10:32.858Z', - currentLocale, - ); + test('Verify card shows healthy/total entity ratio when calculation errors exist', async ({}, testInfo) => { + const partialResponse = gitHubPartiallyAggregatedResponse; + const { entitiesConsidered, calculationErrorCount } = + partialResponse.result; - await mockApiResponse( - page, - ScorecardRoutes.OPEN_PRS_KPI_AGGREGATION_ROUTE, - githubAggregatedResponse, - ); + await setupHomepageAggregationCard(page, homePage, { + aggregationMetadata, + route: ScorecardRoutes.OPEN_PRS_KPI_AGGREGATION_ROUTE, + response: partialResponse, + }); - await addWidgets( - homePage, - AGGREGATED_CARDS_WIDGET_TITLES.withGithubOpenPrs, - ); - await page.reload(); + card = homePage.getCard(aggregationMetadata.id); - const githubCard = homePage.getCard( - AGGREGATED_CARDS_METRIC_IDS.withGithubOpenPrs, - ); - await homePage.verifyThresholdTooltip( - githubCard, - 'success', - '3', - '30%', + await expect(card).toBeVisible(); + await expect(card).toMatchAriaSnapshot( + getStatusGroupedCardSnapshot(translations, { + drillDownMetricId: aggregationMetadata.metricId, + drillDownAggregationId: aggregationMetadata.id, + cardTitle: partialResponse.metadata.title, + cardDescription: partialResponse.metadata.description, + homepageCalculationHealth: { + healthy: String(entitiesConsidered - calculationErrorCount), + total: String(entitiesConsidered), + }, + }), ); - await homePage.verifyThresholdTooltip( - githubCard, - 'warning', - '5', - '50%', + }); + }); + + test.describe('Configured aggregation KPI - "average" type', () => { + const aggregationMetadata = + AGGREGATED_CARDS_METADATA.githubOpenPrsWeightedKpi; + + test.beforeAll(async () => { + await setupHomepageAggregationCard(page, homePage, { + aggregationMetadata, + route: ScorecardRoutes.OPEN_PRS_WEIGHTED_KPI_AGGREGATION_ROUTE, + response: openPrsWeightedAggregatedResponse, + }); + }); + + test.describe('Validate "average" type card content', () => { + let card: Locator; + + 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(card); + + 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('Verify card shows healthy/total entity ratio when calculation errors exist', async ({}, testInfo) => { + const partialResponse = gitHubWeightedPartiallyAggregatedResponse; + const { entitiesConsidered, calculationErrorCount } = + partialResponse.result; + + await setupHomepageAggregationCard(page, homePage, { + aggregationMetadata, + route: ScorecardRoutes.OPEN_PRS_WEIGHTED_KPI_AGGREGATION_ROUTE, + response: partialResponse, + }); + + const card = homePage.getCard(aggregationMetadata.id); + + await expect(card).toBeVisible(); + await expect(card).toMatchAriaSnapshot( + getAverageCardSnapshot(translations, { + drillDownMetricId: aggregationMetadata.metricId, + drillDownAggregationId: aggregationMetadata.id, + cardTitle: partialResponse.metadata.title, + cardDescription: partialResponse.metadata.description, + averageScoreLabel: `${partialResponse.result.averageScore}%`, + homepageCalculationHealth: { + healthy: String(entitiesConsidered - calculationErrorCount), + total: String(entitiesConsidered), + }, + }), ); - await homePage.verifyThresholdTooltip(githubCard, 'error', '2', '20%'); - await homePage.verifyLastUpdatedTooltip(githubCard, githubLastUpdated); }); + }); + 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.githubDefaultAggregation; + const metricId = aggregationMetadata.metricId; + await mockScorecardEntitiesDrillDownWithSort( page, githubEntitiesDrillDownResponse, - 'github.open_prs', + metricId, ); - 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.GITHUB_OPEN_PRS_METRIC_AGGREGATION_ROUTE, + response: githubAggregatedResponse, + }); const lastUpdatedFormatted = formatLastUpdatedDate( '2026-01-24T14:10:32.858Z', @@ -847,9 +853,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', @@ -875,22 +879,27 @@ 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, - }); - await scorecardDrillDownPage.expectPageTitle( - 'github.open_prs', - githubAggregatedResponse.metadata.title, + const title = evaluateMessage( + translations.metric[metricId].title, + metricId, ); - await scorecardDrillDownPage.expectDrillDownCardSnapshot( - 'github.open_prs', - { - aggregationId: AGGREGATED_CARDS_METRIC_IDS.withGithubOpenPrs, - cardTitle: githubAggregatedResponse.metadata.title, - cardDescription: githubAggregatedResponse.metadata.description, - }, + const description = evaluateMessage( + translations.metric[metricId].description, + metricId, + ); + + await homePage.clickDrillDownLink( + homePage.getCard(aggregationMetadata.id), ); + await scorecardDrillDownPage.expectOnPage(metricId, { + aggregationId: aggregationMetadata.id, + }); + await scorecardDrillDownPage.expectPageTitle(metricId, title); + await scorecardDrillDownPage.expectDrillDownCardSnapshot(metricId, { + aggregationId: aggregationMetadata.id, + cardTitle: title, + cardDescription: description, + }); await scorecardDrillDownPage.expectNoDrillDownCalculationErrorWarningIcon(); await scorecardDrillDownPage.expectTableHeadersVisible(); const rows5Label = getEntitiesTableFooterRowsLabel(translations, 5); @@ -945,23 +954,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 +974,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', @@ -992,9 +995,11 @@ test.describe('Scorecard Plugin Tests', () => { }); await test.step('Entity drill-down', async () => { - await homePage.clickDrillDownLink(); + await homePage.clickDrillDownLink( + homePage.getCard(aggregationMetadata.id), + ); await scorecardDrillDownPage.expectOnPage('jira.open_issues', { - aggregationId: AGGREGATED_CARDS_METRIC_IDS.withJiraOpenIssuesKpi, + aggregationId: aggregationMetadata.id, }); await scorecardDrillDownPage.expectPageTitle( 'jira.open_issues', @@ -1003,7 +1008,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 +1074,20 @@ 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(); - - const weightedEntityTotal = String( - openPrsWeightedAggregatedResponse.result.total, - ); - await homePage.clickDrillDownLink({ - healthy: weightedEntityTotal, - total: weightedEntityTotal, - }); - await scorecardDrillDownPage.expectOnPage('github.open_prs', { - aggregationId: AGGREGATED_CARDS_METRIC_IDS.withOpenPrsWeightedKpi, + test.describe('Unsupported aggregation type', () => { + const aggregationMetadata = + AGGREGATED_CARDS_METADATA.githubOpenPrsWeightedKpi; + + test.beforeAll(async () => { + await setupHomepageAggregationCard(page, homePage, { + aggregationMetadata, + route: ScorecardRoutes.OPEN_PRS_WEIGHTED_KPI_AGGREGATION_ROUTE, + response: openPrsWeightedUnsupportedAggregationResponse, }); - 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..78fc1e1edf 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'; @@ -37,6 +38,47 @@ 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, +): Promise { + const status = 200; + const timeout = 60_000; + + return page.waitForResponse( + async res => { + const isStatusValid = res.status() === status; + + if (!isAggregationDataUrl(res.url(), aggregationId) || !isStatusValid) { + return false; + } + + try { + const json = await res.json(); + const result = json?.result; + + return ( + result?.averageScore !== undefined || result?.total !== undefined + ); + } catch { + return false; + } + }, + { timeout }, + ); +} + export async function mockApiResponse( page: Page, route: string, diff --git a/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/averageCardAssertions.ts b/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/averageCardAssertions.ts index 920f71aadd..4782f77d80 100644 --- a/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/averageCardAssertions.ts +++ b/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/averageCardAssertions.ts @@ -15,15 +15,10 @@ */ import { expect, Locator, Page } from '@playwright/test'; -import type { ScorecardMessages } from './translationUtils'; - -function metricCopy(translations: ScorecardMessages, key: string): string { - const metric = translations.metric as unknown as Record< - string, - string | undefined - >; - return metric[key] ?? key; -} +import { + getMetricTranslation, + type ScorecardMessages, +} from './translationUtils'; /** Interpolate `{{key}}` placeholders in a translation template string. */ function interpolate(template: string, vars: Record): string { @@ -57,7 +52,7 @@ function expectedAverageCenterTooltipBreakdownLine( ): string { const n = Number.parseInt(count, 10); const templateKey = averageCenterTooltipBreakdownTemplateKey(locale, n); - const template = metricCopy(translations, templateKey); + const template = getMetricTranslation(translations, templateKey); const status = statusKey in translations.thresholds ? translations.thresholds[statusKey] @@ -83,14 +78,16 @@ export async function verifyAverageDonutCenterTooltip( ): Promise { await card.getByTestId('average-card-center-percent-hit-area').hover(); await expect( - page.getByText(metricCopy(translations, 'averageCenterTooltipTotalLabel'), { - exact: true, - }), + page.getByText( + getMetricTranslation(translations, 'averageCenterTooltipTotalLabel'), + { exact: true }, + ), ).toBeVisible(); await expect( - page.getByText(metricCopy(translations, 'averageCenterTooltipMaxLabel'), { - exact: true, - }), + page.getByText( + getMetricTranslation(translations, 'averageCenterTooltipMaxLabel'), + { exact: true }, + ), ).toBeVisible(); await expect( page.getByText(String(weightedSum), { exact: true }), 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..ab7a6eb051 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')) { @@ -69,6 +73,73 @@ export async function mockHomepageAggregationsPermissionDenied( body: JSON.stringify(notAllowedAggregationErrorBody), }); }); + await page.reload(); +} + +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.gitHubOpenPrsWeightedKpi)) { + 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), + }); + }); } /** @@ -103,4 +174,6 @@ export async function mockAllDefaultHomepageAggregationsSuccess( ScorecardRoutes.OPEN_PRS_WEIGHTED_KPI_AGGREGATION_ROUTE, openPrsWeightedAggregatedResponse, ); + + await page.reload(); } diff --git a/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/scorecardResponseUtils.ts b/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/scorecardResponseUtils.ts index 28a43f7f5c..8dac563eca 100644 --- a/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/scorecardResponseUtils.ts +++ b/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/scorecardResponseUtils.ts @@ -203,7 +203,7 @@ export const openIssuesKpiMetadataResponse = { aggregationType: 'statusGrouped', }; -/** Matches `scorecard.aggregationKPIs.openPrsWeightedKpi` in app-config.yaml */ +/** Matches `scorecard.aggregationKPIs.gitHubOpenPrsWeightedKpi` in app-config.yaml */ export const openPrsWeightedKpiMetadataResponse = { title: 'GitHub Open PRs (weighted health)', description: @@ -232,6 +232,8 @@ export const openPrsWeightedAggregatedResponse = { { count: 1, name: 'critical', score: 0 }, ], total: 10, + entitiesConsidered: 10, + calculationErrorCount: 0, timestamp: '2026-01-24T14:10:32.858Z', thresholds: DEFAULT_NUMBER_THRESHOLDS, averageScore: 51.5, @@ -241,6 +243,27 @@ export const openPrsWeightedAggregatedResponse = { }, }; +export const gitHubWeightedPartiallyAggregatedResponse = { + ...openPrsWeightedAggregatedResponse, + metadata: { + ...openPrsWeightedKpiMetadataResponse, + }, + result: { + ...openPrsWeightedAggregatedResponse.result, + values: [ + { count: 1, name: 'success', score: 100 }, + { count: 4, name: 'warning', score: 40 }, + { count: 0, name: 'error', score: 15 }, + { count: 1, name: 'critical', score: 0 }, + ], + total: 8, + entitiesConsidered: 6, + calculationErrorCount: 2, + averageScore: 46.7, + averageWeightedSum: 466.67, + }, +}; + export const emptyOpenPrsWeightedAggregatedResponse = { id: 'github.open_prs', status: 'success' as const, @@ -512,6 +535,42 @@ export const githubAggregatedResponse = { }, }; +export const githubCustomAggregatedResponse = { + ...githubAggregatedResponse, + metadata: { + ...githubAggregatedResponse.metadata, + title: 'GitHub Open PRs KPI', + description: + 'This KPI provides information about GitHub Open PRs grouped by status', + }, + result: { + ...githubAggregatedResponse.result, + values: [ + { count: 2, name: 'success' }, + { count: 1, name: 'warning' }, + { count: 5, name: 'error' }, + ], + total: 8, + timestamp: '2026-01-24T14:10:32.858Z', + thresholds: DEFAULT_NUMBER_THRESHOLDS, + entitiesConsidered: 8, + calculationErrorCount: 0, + }, +}; + +export const gitHubPartiallyAggregatedResponse = { + ...githubCustomAggregatedResponse, + result: { + ...githubCustomAggregatedResponse.result, + values: [ + { count: 1, name: 'success' }, + { count: 3, name: 'warning' }, + { count: 1, name: 'error' }, + ], + calculationErrorCount: 3, + }, +}; + export const jiraAggregatedResponse = { id: 'jira.open_issues', status: 'success', diff --git a/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/translationUtils.ts b/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/translationUtils.ts index 10d05598a6..fb994bcc37 100644 --- a/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/translationUtils.ts +++ b/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/translationUtils.ts @@ -193,6 +193,22 @@ export function getSomeEntitiesNotReportingTooltip( ); } +/** Flat metric-namespace string by key (e.g. averageCenterTooltipTotalLabel). */ +export function getMetricTranslation( + translations: ScorecardMessages, + key: string, +): string { + const metric = translations.metric as unknown as Record< + string, + string | undefined + >; + const refMetric = scorecardMessages.metric as unknown as Record< + string, + string | undefined + >; + return metric[key] ?? refMetric[key] ?? key; +} + /** Rows-per-page label (e.g. "5 rows", "5 lignes"). Used for dropdown and listbox options. */ export function getEntitiesTableFooterRowsLabel( translations: ScorecardMessages, @@ -369,7 +385,31 @@ export function getDrillDownNoDataFoundSnapshot( `; } -export function getThresholdsSnapshot( +function getHomepageDrillDownLinkSnapshot( + translations: ScorecardMessages, + options: { + drillDownMetricId: string; + aggregationSegment: string; + healthy: string; + total: string; + }, +): string { + const drillDownLinkText = getHomepageEntityCalculationHealthText( + translations, + options.healthy, + options.total, + ); + const drillDownUrl = `/scorecard/aggregations/${options.aggregationSegment}/metrics/${options.drillDownMetricId}`; + return options.healthy === options.total + ? `- link "${drillDownLinkText}": + - /url: ${drillDownUrl}` + : `- link "${getSomeEntitiesNotReportingTooltip(translations)}": + - /url: ${drillDownUrl} + - text: ${drillDownLinkText}`; +} + +/** Snapshot for statusGrouped-type homepage KPI cards (pie chart + threshold legend). */ +export function getStatusGroupedCardSnapshot( translations: ScorecardMessages, options: { drillDownMetricId: 'jira.open_issues' | 'github.open_prs'; @@ -391,16 +431,16 @@ export function getThresholdsSnapshot( healthy: '10', total: '10', }; - const drillDownLinkText = getHomepageEntityCalculationHealthText( - translations, + const drillDownLinkSnapshot = getHomepageDrillDownLinkSnapshot(translations, { + drillDownMetricId, + aggregationSegment, healthy, total, - ); + }); return ` - article: - text: ${cardTitle} - - link "${drillDownLinkText}": - - /url: /scorecard/aggregations/${aggregationSegment}/metrics/${drillDownMetricId} + ${drillDownLinkSnapshot} - button - separator - paragraph: ${cardDescription} @@ -411,7 +451,48 @@ export function getThresholdsSnapshot( `; } -/** Snapshot for the scorecard card on the drill-down page (same as thresholds but without the entities link). */ +/** Snapshot for average-type homepage KPI cards (donut gauge, no threshold legend). */ +export function getAverageCardSnapshot( + translations: ScorecardMessages, + options: { + drillDownMetricId: 'jira.open_issues' | 'github.open_prs'; + drillDownAggregationId?: string; + homepageCalculationHealth?: { healthy: string; total: string }; + cardTitle: string; + cardDescription: string; + averageScoreLabel: string; + }, +): string { + const { + drillDownMetricId, + drillDownAggregationId, + cardTitle, + cardDescription, + averageScoreLabel, + } = options; + const aggregationSegment = drillDownAggregationId ?? drillDownMetricId; + const { healthy, total } = options.homepageCalculationHealth ?? { + healthy: '10', + total: '10', + }; + const drillDownLinkSnapshot = getHomepageDrillDownLinkSnapshot(translations, { + drillDownMetricId, + aggregationSegment, + healthy, + total, + }); + return ` + - article: + - text: ${cardTitle} + ${drillDownLinkSnapshot} + - button + - separator + - paragraph: ${cardDescription} + - application: ${averageScoreLabel} + `; +} + +/** Snapshot for the scorecard card on the drill-down page (same as statusGrouped but without the entities link). */ export function getDrillDownCardSnapshot( translations: ScorecardMessages, metricId: 'jira.open_issues' | 'github.open_prs',