Skip to content

Commit 9881bf8

Browse files
feat(dcm): add Playwright E2E test suite for DCM RHDH plugin (#3249)
* feat(dcm): add Playwright E2E test suite for DCM RHDH plugin 46 browser-based E2E tests covering the full DCM Data Center UI: - Smoke tests: navigation, 6 tabs, seed data verification, API proxy - Providers CRUD: register, edit, search, delete - Policies CRUD: create GLOBAL/USER, toggle, edit, delete - Catalog items: Pet Clinic, create/edit/delete, YAML import - Instances: create dialog, empty state - Regressions: deep link, delete guard, search+pagination, toggle persistence, chip/switch sync, read-only name, whitespace validation, success snackbar, empty table rows, rows-per-page persistence Includes Playwright config, page object (DcmPage), auth fixture, and YAML test data fixtures. Follows rhdh-plugins workspace conventions. Ref: FLPATH-3241, FLPATH-4200 Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dcm): replace Math.random with globalThis.crypto.getRandomValues Addresses SonarQube security hotspots (typescript:S2245) flagging Math.random() as weak PRNG. Uses globalThis.crypto to satisfy both SonarQube and ESLint no-restricted-globals rule. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dcm): update yarn.lock with @playwright/test 1.58.2 Add missing lockfile entries for @playwright/test, playwright, and playwright-core to fix immutable install failure in CI. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(dcm): split Playwright into ci and live projects - 'chromium' project: runs app.test.ts only (merge gate safe) - 'live' project: runs dcm-*.test.ts (requires PLAYWRIGHT_URL) - Fix baseURL to use localhost:3000 (matching other workspaces) - Add e2e-test:live script for running against live environments Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dcm): exclude invalid YAML fixture from Prettier The invalid-catalog-item.yaml is intentionally malformed for testing file import error handling. Prettier cannot parse it and fails CI. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dcm): align app.test.ts with standard workspace CI pattern - Use getByRole('navigation') with auto-waiting assertions (matches app-defaults, translations, konflux patterns) - Add @playwright/test to app-level package.json devDependencies (required by @backstage/no-undeclared-imports ESLint rule) - Update workspace yarn.lock Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dcm): remove duplicate @playwright/test from workspace root Having @playwright/test in both the workspace root and packages/app causes Playwright's singleton check to fail with "Requiring @playwright/test second time". Keep it only in packages/app/package.json (matching app-defaults and konflux patterns). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dcm): keep @playwright/test at workspace root only CI runs `yarn playwright install` and `yarn playwright test` at the workspace root level, so the dependency must be declared there. Remove the duplicate from packages/app/package.json to avoid Playwright's singleton "Requiring second time" error. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dcm): match app.test.ts assertions to DCM sidebar labels The DCM sidebar labels its catalog link "Home" (not "Catalog" like the app-defaults workspace). Update the nav assertion accordingly. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dcm): use unique sidebar links in app.test.ts assertions "Home" matches two elements (logo link + sidebar item) causing strict mode violation. Use "APIs" and "Docs" which are unique sidebar items. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dcm): gitignore playwright test artifacts Add test-results/ and playwright-results.xml to .gitignore so the CI "ensure clean working directory" step doesn't fail after playwright tests produce output files. Co-authored-by: Cursor <cursoragent@cursor.com> * docs(dcm): clarify merge-gate vs downstream E2E project split Add comments to playwright.config.ts explaining which project runs where: chromium (merge gate, local dev server) vs live (downstream Jenkins, requires PLAYWRIGHT_URL pointing at deployed cluster). Co-authored-by: Cursor <cursoragent@cursor.com> * ci: retrigger CI (Node 24 hung on GitHub runner) Co-authored-by: Cursor <cursoragent@cursor.com> * fix(dcm): bump @playwright/test to 1.60.0 Upgrade from 1.58.2 to 1.60.0 to match lightspeed and homepage workspaces. 1.58.2's playwright install --with-deps hangs consistently on Node 24 GitHub Actions runners. Co-authored-by: Cursor <cursoragent@cursor.com> * Address review feedback: shared utils, afterEach cleanup, timeout constants - Extract suffix/uniquePriority/kebabToDisplayName to shared utils/helpers.ts - Extract timeout constants to utils/constants.ts (TIMEOUTS object) - Move cleanup logic from inline to afterEach() with tracking arrays - Set actionTimeout to 10s instead of 0 (infinite) - Prefer ARIA-based locators over CSS selectors in DcmPage.ts - Mark blocked FLPATH-4274 invalid YAML test with test.skip() Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 2bf1ba5 commit 9881bf8

17 files changed

Lines changed: 1952 additions & 1 deletion

workspaces/dcm/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,7 @@ site
5252

5353
# E2E test reports
5454
e2e-test-report/
55+
56+
# Playwright test artifacts
57+
**/test-results/
58+
playwright-results.xml

workspaces/dcm/.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ dist-types
33
coverage
44
.vscode
55
.eslintrc.js
6+
packages/app/e2e-tests/fixtures/dcm/invalid-catalog-item.yaml

workspaces/dcm/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
"prettier:check": "prettier --check .",
2828
"prettier:fix": "prettier --write .",
2929
"new": "backstage-cli new --scope @red-hat-developer-hub",
30-
"postinstall": "cd ../../ && yarn install"
30+
"postinstall": "cd ../../ && yarn install",
31+
"e2e-test": "playwright test",
32+
"e2e-test:live": "playwright test --project=live"
3133
},
3234
"workspaces": {
3335
"packages": [
@@ -46,6 +48,7 @@
4648
"@backstage/e2e-test-utils": "^0.1.1",
4749
"@backstage/repo-tools": "^0.16.2",
4850
"@changesets/cli": "^2.27.1",
51+
"@playwright/test": "1.60.0",
4952
"@types/jest": "^29.5.12",
5053
"@types/node": "^20.0.0",
5154
"@types/webpack-env": "^1.18.4",
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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 { test, expect } from '@playwright/test';
18+
19+
test('App should render the welcome page', async ({ page }) => {
20+
await page.goto('/');
21+
22+
const enterButton = page.getByRole('button', { name: 'Enter' });
23+
await expect(enterButton).toBeVisible();
24+
await enterButton.click();
25+
26+
const nav = page.getByRole('navigation');
27+
await expect(nav.getByRole('link', { name: 'APIs' })).toBeVisible();
28+
await expect(nav.getByRole('link', { name: 'Docs' })).toBeVisible();
29+
});
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
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 { test, expect } from '@playwright/test';
18+
import { DcmPage } from './pages/DcmPage';
19+
import { suffix } from './utils/helpers';
20+
import { TIMEOUTS } from './utils/constants';
21+
import * as path from 'node:path';
22+
23+
test.describe('DCM Catalog Items & Instances @dcm', () => {
24+
let dcm: DcmPage;
25+
26+
test.beforeEach(async ({ page }) => {
27+
dcm = new DcmPage(page);
28+
await dcm.loginAsGuest();
29+
await dcm.navigateToDataCenter();
30+
});
31+
32+
// ── Catalog Items ─────────────────────────────────────────────────────
33+
34+
test('FLPATH-4200: Catalog items tab shows Pet Clinic with correct columns', async () => {
35+
await dcm.clickTab('Catalog items');
36+
await dcm.verifyTableVisible();
37+
38+
await dcm.verifyColumnHeader('Display name');
39+
await dcm.verifyColumnHeader('API version');
40+
await dcm.verifyColumnHeader('Service type');
41+
await dcm.verifyColumnHeader('Fields');
42+
await dcm.verifyColumnHeader('Created');
43+
44+
await dcm.verifyCellContent('Pet Clinic');
45+
await dcm.verifyCellContent('three-tier-app-demo');
46+
});
47+
48+
test('FLPATH-4200: Pet Clinic has Edit and Delete actions', async () => {
49+
await dcm.clickTab('Catalog items');
50+
await dcm.verifyTableVisible();
51+
52+
const row = dcm.page.locator('table tbody tr', { hasText: 'Pet Clinic' });
53+
await expect(row.getByRole('button', { name: 'Edit' })).toBeVisible();
54+
await expect(row.getByRole('button', { name: 'Delete' })).toBeVisible();
55+
});
56+
57+
test('FLPATH-4200: Create and delete a catalog item via drawer', async ({
58+
page,
59+
}) => {
60+
const name = `E2E Item ${suffix()}`;
61+
await dcm.clickTab('Catalog items');
62+
await dcm.clickCreateCatalogItem();
63+
64+
await dcm.fillCatalogItemForm({
65+
displayName: name,
66+
apiVersion: 'v1alpha1',
67+
serviceType: 'container',
68+
});
69+
70+
const pathField = page.locator('label:has-text("Path *") + div input');
71+
if ((await pathField.count()) > 0) {
72+
await pathField.first().click();
73+
await pathField.first().fill('config.replicas');
74+
} else {
75+
const altPath = page.getByLabel('Path *');
76+
await altPath.click();
77+
await altPath.fill('config.replicas');
78+
}
79+
80+
await dcm.submitDialog('Create');
81+
await dcm.waitForTableRefresh();
82+
83+
await dcm.verifyCellContent(name);
84+
85+
await dcm.clickDeleteOnRow(name);
86+
await dcm.confirmDelete();
87+
await dcm.waitForDialogClosed();
88+
await dcm.waitForTableRefresh();
89+
await dcm.verifyNoCellContent(name);
90+
});
91+
92+
test('FLPATH-4200: Edit a catalog item', async () => {
93+
await dcm.clickTab('Catalog items');
94+
95+
await dcm.clickEditOnRow('Pet Clinic');
96+
await expect(
97+
dcm.page.getByRole('heading', { name: 'Edit catalog item' }),
98+
).toBeVisible({ timeout: TIMEOUTS.short });
99+
100+
await dcm.cancelDialog();
101+
});
102+
103+
// ── Instances ─────────────────────────────────────────────────────────
104+
105+
test('FLPATH-4200: Instances tab shows correct columns or empty state', async ({
106+
page,
107+
}) => {
108+
await dcm.clickTab('Instances');
109+
const hasTable = await page
110+
.locator('table')
111+
.first()
112+
.isVisible()
113+
.catch(() => false);
114+
115+
if (hasTable) {
116+
await dcm.verifyColumnHeader('Display name');
117+
await dcm.verifyColumnHeader('Catalog item');
118+
await dcm.verifyColumnHeader('Resource ID');
119+
await dcm.verifyColumnHeader('API version');
120+
await dcm.verifyColumnHeader('Created');
121+
}
122+
123+
await expect(
124+
page.getByRole('button', { name: 'Create', exact: true }),
125+
).toBeVisible();
126+
});
127+
128+
test('FLPATH-4200: Create instance dialog opens and form is fillable', async ({
129+
page,
130+
}) => {
131+
await dcm.clickTab('Instances');
132+
await dcm.clickCreateInstance();
133+
134+
await dcm.fillInstanceForm({
135+
displayName: `E2E Instance ${suffix()}`,
136+
catalogItem: 'Pet Clinic',
137+
apiVersion: 'v1alpha1',
138+
});
139+
140+
await expect(page.getByText('Field values')).toBeVisible({
141+
timeout: TIMEOUTS.short,
142+
});
143+
144+
await dcm.submitDialog('Create');
145+
await page.waitForTimeout(TIMEOUTS.networkSettle);
146+
147+
const dialogVisible = await page
148+
.locator('[role="dialog"]')
149+
.isVisible()
150+
.catch(() => false);
151+
152+
if (dialogVisible) {
153+
await expect(page.locator('[role="alert"]').first()).toBeVisible({
154+
timeout: TIMEOUTS.short,
155+
});
156+
await dcm.cancelDialog();
157+
} else {
158+
await dcm.waitForTableRefresh();
159+
const hasInstance = await page
160+
.getByRole('cell', { name: /E2E Instance/ })
161+
.first()
162+
.isVisible()
163+
.catch(() => false);
164+
if (hasInstance) {
165+
const row = page.locator('table tbody tr', {
166+
hasText: /E2E Instance/,
167+
});
168+
await row.getByRole('button', { name: 'Delete instance' }).click();
169+
await dcm.confirmDelete();
170+
await dcm.waitForDialogClosed();
171+
}
172+
}
173+
});
174+
175+
// ── File Import ──────────────────────────────────────────────────────
176+
177+
test('FLPATH-4274: Valid YAML file import populates catalog item form', async ({
178+
page,
179+
}) => {
180+
const fixturePath = path.resolve(
181+
__dirname,
182+
'fixtures/dcm/valid-catalog-item.yaml',
183+
);
184+
await dcm.clickTab('Catalog items');
185+
await dcm.clickCreateCatalogItem();
186+
187+
await dcm.importCatalogItemFile(fixturePath);
188+
189+
const displayName = page.locator(
190+
'label:has-text("Display name *") + div input',
191+
);
192+
await expect(displayName.first()).toHaveValue('E2E Import Test Item');
193+
194+
const apiVersion = page.locator(
195+
'label:has-text("API version *") + div input',
196+
);
197+
await expect(apiVersion.first()).toHaveValue('v1alpha1');
198+
199+
const pathFields = page.locator('label:has-text("Path *") + div input');
200+
await expect(pathFields).toHaveCount(2, { timeout: TIMEOUTS.short });
201+
await expect(pathFields.nth(0)).toHaveValue('config.replicas');
202+
await expect(pathFields.nth(1)).toHaveValue('config.region');
203+
204+
await dcm.submitDialog('Create');
205+
await dcm.waitForTableRefresh();
206+
await dcm.verifyCellContent('E2E Import Test Item');
207+
208+
await dcm.clickDeleteOnRow('E2E Import Test Item');
209+
await dcm.confirmDelete();
210+
await dcm.waitForDialogClosed();
211+
await dcm.waitForTableRefresh();
212+
});
213+
214+
test.skip('FLPATH-4274: Invalid YAML file import should show error feedback — blocked on FLPATH-4274 fix', async ({
215+
page,
216+
}) => {
217+
const fixturePath = path.resolve(
218+
__dirname,
219+
'fixtures/dcm/invalid-catalog-item.yaml',
220+
);
221+
await dcm.clickTab('Catalog items');
222+
await dcm.clickCreateCatalogItem();
223+
224+
const displayNameBefore = await page
225+
.locator('label:has-text("Display name *") + div input')
226+
.first()
227+
.inputValue();
228+
229+
await dcm.importCatalogItemFile(fixturePath);
230+
231+
const displayNameAfter = await page
232+
.locator('label:has-text("Display name *") + div input')
233+
.first()
234+
.inputValue();
235+
expect(displayNameAfter).toBe(displayNameBefore);
236+
237+
// FLPATH-4274: Error feedback MUST be shown for invalid files.
238+
// This will fail until the fix ships, then pass automatically.
239+
const errorFeedback = page
240+
.locator('[class*="MuiAlert"]')
241+
.first()
242+
.or(page.locator('[class*="MuiSnackbar"]').first());
243+
await expect(errorFeedback).toBeVisible({ timeout: TIMEOUTS.short });
244+
245+
await dcm.cancelDialog();
246+
});
247+
248+
// ── Resources ─────────────────────────────────────────────────────────
249+
250+
test('FLPATH-4200: Resources tab shows content or empty state', async ({
251+
page,
252+
}) => {
253+
await dcm.clickTab('Resources');
254+
255+
await expect(
256+
page.locator('table').first().or(page.getByText('No resources found')),
257+
).toBeVisible({ timeout: TIMEOUTS.table });
258+
});
259+
});

0 commit comments

Comments
 (0)