Skip to content

Commit b6600bb

Browse files
HusneShabbirHusneShabbir
andauthored
feat(scorecard): app-legacy e2e drill-down, mocks, and i18n helpers (#2703)
Add ScorecardDrillDownPage POM, drill-down and aggregation tests, API mocks, and translation-aware helpers for multi-locale runs. Made-with: Cursor Co-authored-by: HusneShabbir <husneshabbir447@gmail.com>
1 parent 31916b7 commit b6600bb

File tree

6 files changed

+1029
-72
lines changed

6 files changed

+1029
-72
lines changed

workspaces/scorecard/packages/app-legacy/e2e-tests/pages/HomePage.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
import { Locator, Page, expect } from '@playwright/test';
1818
import {
1919
ScorecardMessages,
20+
getEntitiesLabel,
2021
getEntityCount,
2122
getLastUpdatedLabel,
23+
getMetricTitleEn,
2224
} from '../utils/translationUtils';
2325

2426
type ThresholdState = 'success' | 'warning' | 'error';
@@ -85,10 +87,13 @@ export class HomePage {
8587
}
8688

8789
getCard(metricId: 'github.open_prs' | 'jira.open_issues'): Locator {
88-
return this.page
89-
.getByText(this.translations.metric[metricId].title, { exact: true })
90-
.last()
91-
.locator('xpath=ancestor::article[1]');
90+
const translatedTitle = this.translations.metric[metricId].title;
91+
const enTitle = getMetricTitleEn(metricId);
92+
const pattern =
93+
translatedTitle === enTitle
94+
? translatedTitle
95+
: new RegExp(`${escapeRegex(translatedTitle)}|${escapeRegex(enTitle)}`);
96+
return this.page.locator('article').filter({ hasText: pattern });
9297
}
9398

9499
async verifyThresholdTooltip(
@@ -133,4 +138,8 @@ export class HomePage {
133138
await infoIcon.hover();
134139
await expect(this.page.getByText(label)).toBeVisible();
135140
}
141+
142+
async clickDrillDownLink() {
143+
await this.page.getByText(getEntitiesLabel(this.translations)).click();
144+
}
136145
}
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { Locator, Page, expect } from '@playwright/test';
18+
import {
19+
ScorecardMessages,
20+
getDrillDownCardSnapshot,
21+
getDrillDownMissingPermissionSnapshot,
22+
getDrillDownNoDataFoundSnapshot,
23+
getEntitiesPageMissingPermission,
24+
getEntitiesPageNoDataFound,
25+
getEntitiesTableHeaderLabels,
26+
getMetricTitleEn,
27+
getSomeEntitiesNotReportingTooltip,
28+
} from '../utils/translationUtils';
29+
30+
type MetricId = 'github.open_prs' | 'jira.open_issues';
31+
32+
function escapeRegex(s: string): string {
33+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
34+
}
35+
36+
export class ScorecardDrillDownPage {
37+
readonly page: Page;
38+
readonly translations: ScorecardMessages;
39+
40+
constructor(page: Page, translations: ScorecardMessages) {
41+
this.page = page;
42+
this.translations = translations;
43+
}
44+
45+
async expectOnPage(metricId: MetricId) {
46+
await expect(this.page).toHaveURL(
47+
new RegExp(`/scorecard/metrics/${metricId.replace('.', '\\.')}`),
48+
);
49+
}
50+
51+
async expectPageTitle(metricId: MetricId) {
52+
await expect(
53+
this.page.getByRole('heading', {
54+
name: this.translations.metric[metricId].title,
55+
level: 1,
56+
}),
57+
).toBeVisible();
58+
}
59+
60+
/** Inner article (the scorecard card); on drill-down page the card is nested inside a wrapper article. */
61+
getDrillDownCard(metricId: MetricId): Locator {
62+
const translatedTitle = this.translations.metric[metricId].title;
63+
const enTitle = getMetricTitleEn(metricId);
64+
const pattern =
65+
translatedTitle === enTitle
66+
? translatedTitle
67+
: new RegExp(`${escapeRegex(translatedTitle)}|${escapeRegex(enTitle)}`);
68+
return this.page.locator('article').filter({ hasText: pattern });
69+
}
70+
71+
getEntitiesTable(): Locator {
72+
return this.page.getByRole('table');
73+
}
74+
75+
getTableFooter(): Locator {
76+
return this.page.locator('tfoot');
77+
}
78+
79+
async clickNextPage(): Promise<void> {
80+
await this.page.getByRole('button', { name: 'next page' }).click();
81+
}
82+
83+
async clickPreviousPage(): Promise<void> {
84+
await this.page.getByRole('button', { name: 'previous page' }).click();
85+
}
86+
87+
async expectTableFooterSnapshot(snapshot: string): Promise<void> {
88+
await expect(this.getTableFooter()).toMatchAriaSnapshot(snapshot);
89+
}
90+
91+
/** Opens the rows-per-page dropdown; pass translated label (e.g. "5 rows", "5 lignes") when testing in non-English locale. */
92+
async openRowsPerPageDropdown(selectedRowsLabel?: string): Promise<void> {
93+
const name = selectedRowsLabel ?? 'rows';
94+
await this.page.getByRole('combobox', { name }).click();
95+
}
96+
97+
async expectRowsPerPageListboxSnapshot(snapshot: string): Promise<void> {
98+
await expect(this.page.getByRole('listbox')).toMatchAriaSnapshot(snapshot);
99+
}
100+
101+
async closeRowsPerPageDropdown(): Promise<void> {
102+
await this.page.keyboard.press('Escape');
103+
}
104+
105+
async expectDrillDownCardSnapshot(metricId: MetricId) {
106+
const card = this.getDrillDownCard(metricId);
107+
await expect(card).toMatchAriaSnapshot(
108+
getDrillDownCardSnapshot(this.translations, metricId),
109+
);
110+
}
111+
112+
async expectCardHasMissingPermission(metricId: MetricId) {
113+
const card = this.getDrillDownCard(metricId);
114+
await expect(card).toContainText(
115+
this.translations.errors.missingPermission,
116+
);
117+
await expect(card).toMatchAriaSnapshot(
118+
getDrillDownMissingPermissionSnapshot(this.translations, metricId),
119+
);
120+
}
121+
122+
async expectTableHasMissingPermission() {
123+
const msg = getEntitiesPageMissingPermission(this.translations);
124+
await expect(this.page.locator('tbody')).toContainText(msg);
125+
}
126+
127+
async expectCardHasNoDataFound(metricId: MetricId) {
128+
const card = this.getDrillDownCard(metricId);
129+
await expect(card).toContainText(this.translations.errors.noDataFound);
130+
await expect(card).toMatchAriaSnapshot(
131+
getDrillDownNoDataFoundSnapshot(this.translations, metricId),
132+
);
133+
}
134+
135+
async expectTableNoDataFound() {
136+
const noDataText = getEntitiesPageNoDataFound(this.translations);
137+
await expect(this.page.locator('tbody')).toContainText(noDataText);
138+
}
139+
140+
/** Verifies the "some entities not reporting" icon tooltip on the drill-down card. */
141+
async verifySomeEntitiesNotReportingTooltip() {
142+
const icon = this.page.getByTestId('ReportProblemOutlinedIcon');
143+
await expect(icon).toBeVisible();
144+
await icon.hover();
145+
const tooltipText = getSomeEntitiesNotReportingTooltip(this.translations);
146+
await expect(this.page.getByRole('tooltip')).toContainText(tooltipText);
147+
}
148+
149+
async expectTableHeadersVisible() {
150+
const tableHeaders = getEntitiesTableHeaderLabels(this.translations);
151+
const headerNames = [
152+
tableHeaders.status,
153+
tableHeaders.value,
154+
tableHeaders.entity,
155+
tableHeaders.owner,
156+
tableHeaders.kind,
157+
tableHeaders.lastUpdated,
158+
];
159+
const entitiesTable = this.getEntitiesTable();
160+
for (const headerLabel of headerNames) {
161+
await expect(
162+
entitiesTable.getByRole('columnheader', { name: headerLabel }),
163+
).toBeVisible();
164+
}
165+
}
166+
167+
async expectEntityNamesVisible(entityNames: string[]) {
168+
for (const name of entityNames) {
169+
await expect(this.page.getByText(name, { exact: true })).toBeVisible();
170+
}
171+
}
172+
173+
async verifyMetricColumnSort() {
174+
const tableHeaders = getEntitiesTableHeaderLabels(this.translations);
175+
const entitiesTable = this.getEntitiesTable();
176+
const statusColumnHeader = entitiesTable.getByRole('columnheader', {
177+
name: tableHeaders.status,
178+
});
179+
180+
const { error, success, warning } = this.translations.thresholds;
181+
const dataRows = entitiesTable.locator('tbody').getByRole('row');
182+
183+
await statusColumnHeader.click();
184+
await expect(dataRows.nth(0)).toContainText(error);
185+
await expect(dataRows.nth(1)).toContainText(success);
186+
187+
await statusColumnHeader.click();
188+
await expect(dataRows.nth(0)).toContainText(warning);
189+
}
190+
}

0 commit comments

Comments
 (0)