Skip to content

Commit a79251c

Browse files
HusneShabbirHusneShabbir
andauthored
chore(e2e): add i18n support, accessibility & enhance e2e for adoption Insights (#1716)
* Add i18n and accessibility tests to Adoption Insights e2e tests - Add translation support and utilities for e2e tests - Add accessibility test utilities - Restructure translations for better testability - Fix SonarQube issues with regex patterns - Fix flaky tests and improve test stability - Update test helpers for translation support * changes in plugin test --------- Co-authored-by: HusneShabbir <husneshabbir447@gmail.com>
1 parent 862c835 commit a79251c

7 files changed

Lines changed: 494 additions & 101 deletions

File tree

workspaces/adoption-insights/packages/app/e2e-tests/insights.test.ts

Lines changed: 169 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -13,94 +13,155 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
import { test, expect, Page } from '@playwright/test';
16+
import {
17+
test,
18+
expect,
19+
Page,
20+
type BrowserContext,
21+
type TestInfo,
22+
} from '@playwright/test';
23+
import {
24+
navigateToInsights,
25+
getPanel,
26+
selectDateRange,
27+
openDateRangePicker,
28+
closeDateRangePicker,
29+
visitCatalogEntity,
30+
runTemplate,
31+
visitDocs,
32+
performSearch,
33+
waitForDataFlush,
34+
verifyTableEntries,
35+
verifyPanelContainsTexts,
36+
switchToLocale,
37+
} from './utils/insightsHelpers';
38+
import { runAccessibilityTests } from './utils/accessibility.js';
39+
import {
40+
InsightsMessages,
41+
getTranslations,
42+
replaceTemplate,
43+
} from './utils/translations.js';
1744

1845
test.describe.configure({ mode: 'serial' });
1946

2047
let page: Page;
21-
22-
async function navigate(link: string) {
23-
const navLink = page.locator(`nav a:has-text("${link}")`).first();
24-
await navLink.waitFor({ state: 'visible' });
25-
await navLink.click();
26-
}
48+
let context: BrowserContext;
49+
let translations: InsightsMessages;
2750

2851
test.beforeAll(async ({ browser }) => {
29-
page = await browser.newPage();
52+
context = await browser.newContext();
53+
page = await context.newPage();
54+
const currentLocale = await page.evaluate(
55+
() => globalThis.navigator.language,
56+
);
3057
await page.goto('/');
3158
await page.getByRole('button', { name: 'Enter' }).click();
3259

33-
// give the insights plugin some time to crunch the new login
34-
await new Promise(res => setTimeout(res, 8000));
60+
await switchToLocale(page, currentLocale);
61+
translations = getTranslations(currentLocale);
62+
63+
await waitForDataFlush();
3564
});
3665

37-
test('Insights is available', async () => {
38-
await navigate('Adoption Insights');
66+
test.afterAll(async () => {
67+
await context.close();
68+
});
3969

40-
const heading = page.getByRole('heading', { name: 'Insights' }).first();
70+
test('Insights is available', async ({
71+
browser: _browser,
72+
}, testInfo: TestInfo) => {
73+
await navigateToInsights(page, translations.header.title);
74+
75+
const heading = page
76+
.getByRole('heading', { name: translations.header.title })
77+
.first();
4178

4279
expect(page.url()).toContain('/adoption-insights');
4380
await expect(heading).toBeVisible();
81+
82+
await runAccessibilityTests(page, testInfo);
4483
});
4584

4685
test('Select date range', async () => {
47-
const dateRanges = ['Today', 'Last week', 'Last month', 'Last year'];
48-
await page.getByText('Last 28 days').click();
86+
const dateRanges = [
87+
translations.header.dateRange.today,
88+
translations.header.dateRange.lastWeek,
89+
translations.header.dateRange.lastMonth,
90+
translations.header.dateRange.lastYear,
91+
];
92+
await page.getByText(translations.header.dateRange.defaultLabel).click();
4993
for (const range of dateRanges) {
5094
await expect(page.getByRole('option', { name: range })).toBeVisible();
5195
}
52-
const dateRange = page.getByRole('option', { name: `Date range...` });
53-
await expect(dateRange).toBeVisible();
54-
await dateRange.click();
96+
await openDateRangePicker(page, translations.header.dateRange.dateRange);
5597

5698
const datePicker = page.locator('.v5-MuiPaper-root', {
5799
hasText: 'Start date',
58100
});
59101
await expect(datePicker).toBeVisible();
60-
await datePicker.getByRole('button', { name: 'Cancel' }).click();
102+
await closeDateRangePicker(page, translations.header.dateRange.cancel);
61103
await expect(datePicker).not.toBeVisible();
62104

63-
await page.getByRole('option', { name: 'Today' }).click();
105+
await selectDateRange(page, translations.header.dateRange.today);
64106
});
65107

66108
test('Active users panel shows 1 visitor', async () => {
67-
const panel = page.locator('.v5-MuiPaper-root', { hasText: 'Active users' });
109+
const panel = getPanel(page, translations.activeUsers.title);
68110
await expect(panel.locator('.recharts-surface')).toBeVisible();
111+
const averageTextContent = replaceTemplate(
112+
translations.activeUsers.averageText,
113+
{
114+
count: 1,
115+
period: translations.activeUsers.hour,
116+
},
117+
);
118+
const averageText = `${translations.activeUsers.averagePrefix} ${averageTextContent}${translations.activeUsers.averageSuffix}`;
69119
await expect(panel).toMatchAriaSnapshot(`
70-
- heading "Active users" [level=5]
71-
- button "Export CSV"
72-
- paragraph: Average peak active user count was 1 per hour for this period.
120+
- heading "${translations.activeUsers.title}" [level=5]
121+
- button "${translations.common.exportCSV}"
122+
- paragraph: ${averageText}
123+
- paragraph: ${translations.activeUsers.legend.returningUsers}
124+
- paragraph: ${translations.activeUsers.legend.newUsers}
73125
`);
74126
});
75127

76128
test('Total number of users panel shows 1 visitor of 100', async () => {
77-
const panel = page.locator('.v5-MuiPaper-root', {
78-
hasText: 'Total number of users',
79-
});
129+
const panel = getPanel(page, translations.users.title);
80130
await expect(panel.locator('.recharts-surface')).toBeVisible();
131+
const ofTotalText = `1 ${replaceTemplate(translations.users.ofTotal, {
132+
total: 100,
133+
})}`;
81134
await expect(panel).toMatchAriaSnapshot(`
82-
- heading "Total number of users" [level=5]
135+
- heading "${translations.users.title}" [level=5]
83136
- img:
84-
- text: 1 of 100
137+
- text: ${ofTotalText}
85138
- list:
86-
- listitem: Logged-in users
87-
- listitem: Licensed (not logged in)
139+
- listitem: ${translations.users.loggedInUsers}
140+
- listitem: ${translations.users.licensedNotLoggedIn}
88141
- heading "1%" [level=1]
89-
- paragraph: have logged in
142+
- paragraph: ${translations.users.haveLoggedIn}
90143
`);
91144
});
92145

93146
test('Top plugins shows catalog', async () => {
94-
await navigate('Adoption Insights');
95-
const panel = page.locator('.v5-MuiPaper-root', { hasText: 'All plugins' });
147+
await navigateToInsights(page, translations.header.title);
148+
const pluginRegex = new RegExp(
149+
`${translations.plugins.allTitle}|${replaceTemplate(
150+
translations.plugins.topNTitle,
151+
{ count: '\\d' },
152+
)}`,
153+
);
154+
155+
const panel = page.locator('.v5-MuiPaper-root', {
156+
hasText: pluginRegex,
157+
});
96158
await expect(panel).toMatchAriaSnapshot(`
97-
- heading "All plugins" [level=5]
98159
- table:
99160
- rowgroup:
100161
- row :
101-
- columnheader "Name"
102-
- columnheader "Trend"
103-
- columnheader "Views"
162+
- columnheader "${translations.table.headers.name}"
163+
- columnheader "${translations.table.headers.trend}"
164+
- columnheader "${translations.table.headers.views}"
104165
- rowgroup:
105166
- row :
106167
- cell "catalog"
@@ -109,94 +170,107 @@ test('Top plugins shows catalog', async () => {
109170

110171
test('Rest of the panels have no data', async () => {
111172
const titles = [
112-
'Top templates',
113-
'Top catalog entities',
114-
'Top TechDocs',
115-
'Top searches',
173+
translations.templates.title,
174+
translations.catalogEntities.title,
175+
translations.techDocs.title,
176+
translations.searches.title,
116177
];
117178
for (const title of titles) {
118-
const panel = page.locator('.v5-MuiPaper-root', { hasText: title });
119-
await expect(panel).toContainText('No results for this date range.');
179+
const panel = getPanel(page, title);
180+
await expect(panel).toContainText(translations.common.noResults);
120181
}
121182
});
122183

123184
test.describe(() => {
124185
test.beforeAll(async () => {
125186
// visit a catalog entity
126-
await navigate('home');
127-
await page.getByRole('link', { name: 'example-website' }).click();
128-
await page
129-
.getByRole('heading', { name: 'example-website' })
130-
.waitFor({ state: 'visible' });
187+
await visitCatalogEntity(page, 'example-website');
131188

132189
// run a template
133-
await navigate('create...');
134-
await page.getByTestId('template-card-actions--create').click();
135-
await page.getByRole('textbox').fill('reallyUniqueName');
136-
await page.getByRole('button', { name: 'next' }).click();
137-
await page.getByRole('textbox').first().fill('orgthatdoesntexist');
138-
await page.getByRole('textbox').last().fill('repothatdoesntexist');
139-
await page.getByRole('button', { name: 'review' }).click();
140-
await page.getByRole('button', { name: 'create' }).click();
141-
await page
142-
.getByText('Run of Example Node.js Template')
143-
.waitFor({ state: 'visible' });
190+
await runTemplate(
191+
page,
192+
'reallyUniqueName',
193+
'orgthatdoesntexist',
194+
'repothatdoesntexist',
195+
);
144196

145197
// visit the docs
146-
await navigate('docs');
147-
await page.getByText('No documents to show').waitFor({ state: 'visible' });
198+
await visitDocs(page);
148199

149200
// do a search
150-
await page.getByRole('button', { name: 'search' }).click();
151-
await page.getByRole('textbox').fill('searching for something');
152-
153-
const noSearchResults = page.getByRole('heading', {
154-
name: 'Sorry, no results were found',
155-
});
156-
await noSearchResults.waitFor({ state: 'visible' });
157-
await page.getByRole('button', { name: 'close' }).click();
201+
await performSearch(page, 'searching for something');
158202

159203
// wait for the flush interval to be sure
160-
await new Promise(res => setTimeout(res, 8000));
204+
await waitForDataFlush();
161205

162-
await navigate('Adoption Insights');
163-
await page.getByText('Last 28 days').click();
164-
await page.getByRole('option', { name: `Today` }).click();
206+
await navigateToInsights(page);
207+
await page.getByText(translations.header.dateRange.defaultLabel).click();
208+
await selectDateRange(page, translations.header.dateRange.today);
165209
});
166210

167211
test('Visited component shows up in top catalog entities', async () => {
168-
const panel = page.locator('.v5-MuiPaper-root', {
169-
hasText: 'All catalog entities',
170-
});
171-
const entries = panel.locator('tbody').locator('tr');
172-
await expect(entries).toHaveCount(1);
173-
await expect(entries).toContainText('example-website');
212+
const panel = getPanel(page, translations.catalogEntities.allTitle);
213+
await expect(panel).toContainText(translations.filter.selectKind);
214+
await panel.getByLabel(translations.filter.selectKind).click();
215+
await expect(page.getByRole('listbox')).toMatchAriaSnapshot(`
216+
- listbox "${translations.filter.selectKind}":
217+
- option "${translations.filter.all}"
218+
- option "Component"
219+
`);
220+
221+
await verifyPanelContainsTexts(panel, [
222+
translations.table.headers.name,
223+
translations.table.headers.kind,
224+
translations.table.headers.lastUsed,
225+
translations.table.headers.views,
226+
]);
227+
228+
await verifyTableEntries(panel, 1, 'example-website');
174229
});
175230

176231
test('Visited TechDoc shows up in top TechDocs', async () => {
177-
const panel = page.locator('.v5-MuiPaper-root', {
178-
hasText: 'All TechDocs',
179-
});
180-
const entries = panel.locator('tbody').locator('tr');
181-
await expect(entries).toHaveCount(1);
232+
const panel = getPanel(page, translations.techDocs.allTitle);
233+
234+
await verifyPanelContainsTexts(panel, [
235+
translations.table.headers.name,
236+
translations.table.headers.entity,
237+
translations.table.headers.lastUsed,
238+
translations.table.headers.views,
239+
]);
240+
241+
await verifyTableEntries(panel, 1, 'docs');
182242
});
183243

184244
test('New data shows in searches', async () => {
185-
const panel = page.locator('.v5-MuiPaper-root', { hasText: '1 searches' });
245+
const panel = getPanel(
246+
page,
247+
replaceTemplate(translations.searches.totalCount, { count: 1 }),
248+
);
186249
await panel.scrollIntoViewIfNeeded();
187250
await expect(panel.locator('.recharts-surface')).toBeVisible();
188-
await expect(panel).toContainText(
189-
'Average search count was 1 per hour for this period.',
251+
const averageTextContent = replaceTemplate(
252+
translations.searches.averageText,
253+
{
254+
count: 1,
255+
period: translations.searches.hour,
256+
},
190257
);
258+
const averageText = `${translations.searches.averagePrefix} ${averageTextContent}${translations.searches.averageSuffix}`;
259+
await expect(panel).toContainText(averageText);
191260
});
192261

193-
test('New data shows in top templates', async () => {
194-
const panel = page.locator('.v5-MuiPaper-root', {
195-
hasText: 'All templates',
196-
});
262+
test('New data shows in top templates', async ({
263+
browser: _browser,
264+
}, testInfo: TestInfo) => {
265+
const panel = getPanel(page, translations.templates.allTitle);
266+
await verifyPanelContainsTexts(panel, [
267+
translations.table.headers.name,
268+
translations.table.headers.executions,
269+
]);
270+
197271
await panel.scrollIntoViewIfNeeded();
198-
const entries = panel.locator('tbody').locator('tr');
199-
await expect(entries).toHaveCount(1);
200-
await expect(entries).toContainText('example-nodejs-template');
272+
await verifyTableEntries(panel, 1, 'example-nodejs-template');
273+
274+
await runAccessibilityTests(page, testInfo);
201275
});
202276
});
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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 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 AxeBuilder from '@axe-core/playwright';
18+
import { expect, Page, TestInfo } from '@playwright/test';
19+
20+
export async function runAccessibilityTests(
21+
page: Page,
22+
testInfo: TestInfo,
23+
attachName = 'accessibility-scan-results.json',
24+
) {
25+
const accessibilityScanResults = await new AxeBuilder({ page })
26+
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
27+
.analyze();
28+
29+
const filteredViolations = accessibilityScanResults.violations.filter(
30+
v => !['aria-input-field-name', 'button-name'].includes(v.id),
31+
);
32+
33+
await testInfo.attach(attachName, {
34+
body: JSON.stringify(accessibilityScanResults, null, 2),
35+
contentType: 'application/json',
36+
});
37+
38+
expect(filteredViolations, 'Accessibility violations found').toEqual([]);
39+
}

0 commit comments

Comments
 (0)