Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@red-hat-developer-hub/backstage-plugin-scorecard-common': patch
'@red-hat-developer-hub/backstage-plugin-scorecard-backend': patch
'@red-hat-developer-hub/backstage-plugin-scorecard': patch
---

Expose scorecard entity calculation health on drill-down and aggregation APIs, and align the drill-down warning plus homepage subheader with those counts.
3 changes: 2 additions & 1 deletion workspaces/scorecard/app-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,8 @@ auth:
# see https://backstage.io/docs/auth/ to learn about auth providers
providers:
# See https://backstage.io/docs/auth/guest/provider
guest: {}
guest:
userEntityRef: user:development/guest

scaffolder:
# see https://backstage.io/docs/features/software-templates/configuration for software template options
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,14 @@ export class CatalogPage {
async loginAndSetLocale(locale: string) {
await this.page.goto('/');
const enterButton = this.page.getByRole('button', { name: 'Enter' });
await expect(enterButton).toBeVisible();
await expect(enterButton).toBeVisible({ timeout: 30000 });
await enterButton.click();
await expect(this.page.getByText('Welcome back!')).toBeVisible();
// Guest flow copy varies by Backstage / branding; wait for shell instead of "Welcome back!".
await expect(
this.page.getByRole('link', { name: 'Home' }).first(),
).toBeVisible({
timeout: 30000,
});
await this.switchToLocale(locale);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import { Locator, Page, expect } from '@playwright/test';
import { AGGREGATED_CARDS_WIDGET_TITLES } from '../constants/homepageWidgetTitles';
import {
ScorecardMessages,
getEntitiesLabel,
getEntityCount,
getHomepageEntityCalculationHealthText,
getLastUpdatedLabel,
} from '../utils/translationUtils';

Expand Down Expand Up @@ -130,22 +130,18 @@ export class HomePage {
await expect(this.page.getByText(label)).toBeVisible();
}

async clickDrillDownLink() {
// CardSubheader renders the count as a Link (e.g. "10 entities"). The card
// description can also contain the word "entities" (see API metadata), so
// getByText(entitiesLabel) is ambiguous. MUI Tooltip also sets the link’s
// accessible name to the long tooltip, so getByRole('link', { name }) is
// locale‑fragile. Match only links whose *visible* text is "{{count}} <label>".
const entitiesLabel = getEntitiesLabel(this.translations);
await this.page
.getByRole('link')
.filter({
hasText: new RegExp(
String.raw`^\d+\s*${escapeRegex(entitiesLabel)}$`,
'i',
),
})
.first()
.click();
/**
* Clicks the homepage KPI drill-down link (healthy/total subheader). Mock data uses 10/10.
*/
async clickDrillDownLink(options?: { healthy?: string; total?: string }) {
const healthy = options?.healthy ?? '10';
const total = options?.total ?? '10';
const name = getHomepageEntityCalculationHealthText(
this.translations,
healthy,
total,
);
// Multiple homepage scorecards can share the same health string; target the first match.
await this.page.getByRole('link', { name }).first().click();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,18 @@ export class ScorecardDrillDownPage {
await expect(this.page.locator('tbody')).toContainText(noDataText);
}

/**
* When mocks report no calculation failures, the drill-down must not show the
* calculation-warning icon next to the Entities heading.
*/
async expectNoDrillDownCalculationErrorWarningIcon() {
const heading = this.page.getByRole('heading', {
level: 3,
name: this.translations.entitiesPage.entitiesTable.title,
});
await expect(heading.locator('svg.MuiSvgIcon-colorWarning')).toHaveCount(0);
}

/** Verifies the "some entities not reporting" icon tooltip on the drill-down card. */
async verifySomeEntitiesNotReportingTooltip() {
const icon = this.page.getByTestId('ReportProblemOutlinedIcon');
Expand Down Expand Up @@ -190,9 +202,21 @@ export class ScorecardDrillDownPage {
}
}

/**
* Asserts each entity row is present. Uses the catalog entity link `href`
* (…/component/&lt;slug&gt;) so it works when the UI shows `metadata.title`
* (e.g. "Red Hat Developer Hub") instead of `metadata.name` (slug).
*/
async expectEntityNamesVisible(entityNames: string[]) {
const entitiesTable = this.getEntitiesTable();
for (const name of entityNames) {
await expect(this.page.getByText(name, { exact: true })).toBeVisible();
const slug = encodeURIComponent(name);
await expect(
entitiesTable
.locator('tbody')
.locator(`a[href*="/catalog/default/component/${slug}"]`)
.first(),
).toBeVisible({ timeout: 15_000 });
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ test.describe('Scorecard Plugin Tests', () => {
notAllowedAggregationErrorBody,
403,
);

await catalogPage.openCatalog();
await catalogPage.openComponent('Red Hat Developer Hub');
await page.getByText('Scorecard', { exact: true }).click();
Expand Down Expand Up @@ -417,11 +418,6 @@ test.describe('Scorecard Plugin Tests', () => {
await addAggregatedScorecardWidgets(homePage);
await page.reload();

const jiraEntityCount = getEntityCount(
translations,
currentLocale,
'10',
);
const card = homePage.getCard(
AGGREGATED_CARDS_METRIC_IDS.withDeprecatedMetricId,
);
Expand All @@ -435,7 +431,6 @@ test.describe('Scorecard Plugin Tests', () => {
getThresholdsSnapshot(translations, {
drillDownMetricId:
AGGREGATED_CARDS_METRIC_IDS.withDeprecatedMetricId,
entityCount: jiraEntityCount,
cardTitle: metadata.title,
cardDescription: metadata.description,
}),
Expand Down Expand Up @@ -525,11 +520,6 @@ test.describe('Scorecard Plugin Tests', () => {
await addAggregatedScorecardWidgets(homePage);
await page.reload();

const githubEntityCount = getEntityCount(
translations,
currentLocale,
'10',
);
const metadata =
translations.metric[
AGGREGATED_CARDS_METRIC_IDS.withDefaultAggregation
Expand All @@ -543,7 +533,6 @@ test.describe('Scorecard Plugin Tests', () => {
getThresholdsSnapshot(translations, {
drillDownMetricId:
AGGREGATED_CARDS_METRIC_IDS.withDefaultAggregation,
entityCount: githubEntityCount,
cardTitle: metadata.title,
cardDescription: metadata.description,
}),
Expand Down Expand Up @@ -647,11 +636,6 @@ test.describe('Scorecard Plugin Tests', () => {
);
await page.reload();

const githubEntityCount = getEntityCount(
translations,
currentLocale,
'10',
);
const card = homePage.getCard(
AGGREGATED_CARDS_METRIC_IDS.withGithubOpenPrs,
);
Expand All @@ -663,7 +647,6 @@ test.describe('Scorecard Plugin Tests', () => {
AGGREGATED_CARDS_METRIC_IDS.withDefaultAggregation,
drillDownAggregationId:
AGGREGATED_CARDS_METRIC_IDS.withGithubOpenPrs,
entityCount: githubEntityCount,
cardTitle: githubAggregatedResponse.metadata.title,
cardDescription: githubAggregatedResponse.metadata.description,
}),
Expand Down Expand Up @@ -798,7 +781,7 @@ test.describe('Scorecard Plugin Tests', () => {
cardDescription: githubAggregatedResponse.metadata.description,
},
);
await scorecardDrillDownPage.verifySomeEntitiesNotReportingTooltip();
await scorecardDrillDownPage.expectNoDrillDownCalculationErrorWarningIcon();
await scorecardDrillDownPage.expectTableHeadersVisible();
const rows5Label = getEntitiesTableFooterRowsLabel(translations, 5);
await scorecardDrillDownPage.expectTableFooterSnapshot(
Expand All @@ -820,7 +803,7 @@ test.describe('Scorecard Plugin Tests', () => {
// First page: only 5 entities (pageSize=5)
await scorecardDrillDownPage.expectEntityNamesVisible([
'all-scorecards-service',
'Red Hat Developer Hub',
'red-hat-developer-hub',
'github-scorecard-only-service',
'all-scorecards-service-different-owner',
'backend-api',
Expand Down Expand Up @@ -915,7 +898,7 @@ test.describe('Scorecard Plugin Tests', () => {
cardDescription: jiraAggregatedResponse.metadata.description,
},
);
await scorecardDrillDownPage.verifySomeEntitiesNotReportingTooltip();
await scorecardDrillDownPage.expectNoDrillDownCalculationErrorWarningIcon();
await scorecardDrillDownPage.expectTableHeadersVisible();
await scorecardDrillDownPage.expectEntityNamesVisible([
'platform-api',
Expand Down Expand Up @@ -1111,11 +1094,10 @@ test.describe('Scorecard Plugin Tests', () => {
},
);
await expectAverageCardCenterPercent(drillCard, '50%');
await scorecardDrillDownPage.verifySomeEntitiesNotReportingTooltip();
await scorecardDrillDownPage.expectTableHeadersVisible();
await scorecardDrillDownPage.expectEntityNamesVisible([
'all-scorecards-service',
'Red Hat Developer Hub',
'red-hat-developer-hub',
'github-scorecard-only-service',
'all-scorecards-service-different-owner',
'backend-api',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,8 @@ export const githubAggregatedResponse = {
total: 10,
timestamp: '2026-01-24T14:10:32.858Z',
thresholds: DEFAULT_NUMBER_THRESHOLDS,
entitiesConsidered: 10,
calculationErrorCount: 0,
},
};

Expand All @@ -330,6 +332,8 @@ export const jiraAggregatedResponse = {
total: 10,
timestamp: '2026-01-24T14:10:32.776Z',
thresholds: DEFAULT_NUMBER_THRESHOLDS,
entitiesConsidered: 10,
calculationErrorCount: 0,
},
};

Expand All @@ -353,6 +357,8 @@ export const emptyJiraAggregatedResponse = {
],
timestamp: '2026-01-24T14:10:32.858Z',
thresholds: DEFAULT_NUMBER_THRESHOLDS,
entitiesConsidered: 0,
calculationErrorCount: 0,
},
};

Expand All @@ -376,6 +382,8 @@ export const emptyGithubAggregatedResponse = {
],
timestamp: '2026-01-24T14:10:32.858Z',
thresholds: DEFAULT_NUMBER_THRESHOLDS,
entitiesConsidered: 0,
calculationErrorCount: 0,
},
};

Expand Down Expand Up @@ -497,6 +505,11 @@ export const githubEntitiesDrillDownResponse = {
totalPages: 1,
isCapped: false,
},
entityHealth: {
totalEntities: 10,
calculationErrorCount: 0,
countsArePartial: false,
},
};

/** Mock response for GET .../api/scorecard/metrics/jira.open_issues/catalog/aggregations/entities (in sync with jiraAggregatedResponse) */
Expand Down Expand Up @@ -557,6 +570,11 @@ export const jiraEntitiesDrillDownResponse = {
totalPages: 1,
isCapped: false,
},
entityHealth: {
totalEntities: 4,
calculationErrorCount: 0,
countsArePartial: false,
},
};

/** Mock response for Jira entities drill-down when aggregation has no data (empty list). */
Expand All @@ -576,6 +594,11 @@ export const jiraEntitiesDrillDownNoDataResponse = {
totalPages: 0,
isCapped: false,
},
entityHealth: {
totalEntities: 0,
calculationErrorCount: 0,
countsArePartial: false,
},
};

export const fileCheckScorecardResponse = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,20 +306,37 @@ export function getLastUpdatedLabel(
}

/**
* Homepage KPI cards use aggregationIds (e.g. openPrsKpi); labels fall back to API/config
* metadata in English, not `metric.github.open_prs` locale keys. Use ref copy for title
* / description; keep localized errors, thresholds, and entity-count strings.
* Homepage KPI drill-down link text:
* - with calculation errors: healthy/total ratio
* - without calculation errors: plain entity count
*/
function getSomeEntitiesNotReportingLabel(
export function getHomepageEntityCalculationHealthText(
translations: ScorecardMessages,
healthy: string,
total: string,
): string {
const metric = translations.metric as {
someEntitiesNotReportingValues?: string;
};
return (
metric.someEntitiesNotReportingValues ??
scorecardMessages.metric.someEntitiesNotReportingValues
);
if (healthy === total) {
const count = Number(total);
const entitiesTemplate =
count === 1
? translations.thresholds.entities_one
: translations.thresholds.entities_other;
return entitiesTemplate.replaceAll('{{count}}', String(count));
}

const template =
(
translations.metric as {
homepageEntityHealthRatio?: string;
homepageEntityCalculationHealth?: string;
}
).homepageEntityHealthRatio ??
scorecardMessages.metric.homepageEntityHealthRatio ??
scorecardMessages.metric.homepageEntityCalculationHealth;

return template
.replaceAll('{{healthy}}', healthy)
.replaceAll('{{total}}', total);
}

/** Snapshot for the scorecard card on the drill-down page when permission is missing (no entity count in UI). */
Expand Down Expand Up @@ -357,26 +374,33 @@ export function getThresholdsSnapshot(
options: {
drillDownMetricId: 'jira.open_issues' | 'github.open_prs';
drillDownAggregationId?: string;
entityCount: string;
/** Interpolation for homepage subheader (mock data uses 10/10). */
homepageCalculationHealth?: { healthy: string; total: string };
cardTitle: string;
cardDescription: string;
},
): string {
const {
drillDownMetricId,
drillDownAggregationId,
entityCount,
cardTitle,
cardDescription,
} = options;
const aggregationSegment = drillDownAggregationId ?? drillDownMetricId;
const drillDownLinkName = getSomeEntitiesNotReportingLabel(translations);
const { healthy, total } = options.homepageCalculationHealth ?? {
healthy: '10',
total: '10',
};
const drillDownLinkText = getHomepageEntityCalculationHealthText(
translations,
healthy,
total,
);
return `
- article:
- text: ${cardTitle}
- link "${drillDownLinkName}":
- link "${drillDownLinkText}":
- /url: /scorecard/aggregations/${aggregationSegment}/metrics/${drillDownMetricId}
- text: ${entityCount}
- button
- separator
- paragraph: ${cardDescription}
Expand Down
Loading
Loading