Skip to content

Commit eeae24a

Browse files
mvinklerclaude
andcommitted
CONSOLE-5241: Migrate knative-ci.feature Cypress tests to Playwright
Migrate the knative CI smoke test (knative-ci.feature) from Cypress Gherkin to Playwright and remove the original Cypress file. 16 automatable scenarios migrated using serial test mode. 8 manual/ broken-test scenarios dropped (not migrated). New files: - e2e/tests/knative/serverless/knative-ci.spec.ts (16 serial tests) - e2e/pages/knative/topology-knative-page.ts (topology interactions) - e2e/pages/knative/add-flow-page.ts (git import flow) - e2e/pages/knative/admin-eventing-page.ts (admin eventing page) - e2e/setup/knative.setup.ts (Serverless operator install + CR setup) Removed: - features/e2e/knative-ci.feature (replaced by Playwright spec) - test-cypress-knative-headless script and CI references React components modified to add data-test attributes alongside existing legacy test attributes (data-test-id, data-test-action, data-test-dropdown-menu) for Playwright getByTestId() usage. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Michal Vinkler <mvinkler@redhat.com>
1 parent 329e322 commit eeae24a

24 files changed

Lines changed: 1122 additions & 274 deletions

File tree

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import type { Locator } from '@playwright/test';
2+
import { expect } from '@playwright/test';
3+
4+
import BasePage from '../base-page';
5+
6+
export class AddFlowPage extends BasePage {
7+
private readonly gitUrlInput = this.page.locator('#form-input-git-url-field');
8+
private readonly componentNameInput = this.page.locator('#form-input-name-field');
9+
private readonly appNameInput = this.page.locator('#form-input-application-name-field');
10+
private readonly createButton = this.page.getByTestId('save-changes');
11+
private readonly resourcesDropdown = this.page.locator(
12+
'#form-select-input-resources-field',
13+
);
14+
private readonly knativeResourceOption = this.page.locator(
15+
'#select-option-resources-knative',
16+
);
17+
private readonly importStrategyEditButton = this.page.locator(
18+
'.odc-import-strategy-section__edit-strategy-button',
19+
);
20+
private readonly dockerfileStrategy = this.page.getByTestId('import-strategy Dockerfile');
21+
private readonly dockerfilePathInput = this.page.locator(
22+
'#form-input-docker-dockerfilePath-field',
23+
);
24+
private readonly externalRegistryInput = this.page.locator(
25+
'#form-input-searchTerm-field',
26+
);
27+
private readonly pageHeading = this.page.getByTestId('page-heading').locator('h1');
28+
29+
async navigateToAddPage(namespace: string): Promise<void> {
30+
await this.goTo(`/add/ns/${namespace}`);
31+
await this.waitForLoadingComplete();
32+
}
33+
34+
async clickCard(cardId: string): Promise<void> {
35+
const card = this.page.getByTestId(`item ${cardId}`);
36+
await card.scrollIntoViewIfNeeded();
37+
await this.robustClick(card);
38+
await this.waitForLoadingComplete();
39+
}
40+
41+
async clickImportFromGitCard(): Promise<void> {
42+
await this.clickCard('import-from-git');
43+
}
44+
45+
async clickContainerImageCard(): Promise<void> {
46+
await this.clickCard('deploy-image');
47+
}
48+
49+
async enterGitUrl(url: string): Promise<void> {
50+
await this.gitUrlInput.clear();
51+
await this.gitUrlInput.fill(url);
52+
await expect(
53+
this.page.locator('.pf-v6-c-helper-text').filter({ hasText: /Validated|Rate limit/ }).first(),
54+
).toBeVisible({ timeout: 60_000 });
55+
56+
// If rate limited, auto-detection fails — manually select Builder Image strategy and Node.js
57+
const rateLimitMsg = this.page.getByText('Rate limit exceeded');
58+
if ((await rateLimitMsg.count()) > 0) {
59+
// Select "Builder Image" import strategy
60+
const builderImageStrategy = this.page.getByTestId('import-strategy Builder Image');
61+
if ((await builderImageStrategy.count()) > 0) {
62+
await this.robustClick(builderImageStrategy);
63+
}
64+
// Wait for builder image list to load, then select Node.js
65+
const nodeJsImage = this.page.locator('.odc-selector-card').filter({ hasText: 'Node.js' });
66+
if ((await nodeJsImage.count()) > 0) {
67+
await this.robustClick(nodeJsImage.first());
68+
}
69+
}
70+
}
71+
72+
async enterComponentName(name: string): Promise<void> {
73+
await this.componentNameInput.scrollIntoViewIfNeeded();
74+
await this.componentNameInput.click();
75+
await this.componentNameInput.clear();
76+
await this.componentNameInput.fill(name);
77+
await expect(this.componentNameInput).toHaveValue(name);
78+
}
79+
80+
async enterAppName(name: string): Promise<void> {
81+
await this.appNameInput.clear();
82+
await this.appNameInput.fill(name);
83+
}
84+
85+
async selectServerlessDeployment(): Promise<void> {
86+
await this.resourcesDropdown.scrollIntoViewIfNeeded();
87+
await this.robustClick(this.resourcesDropdown);
88+
await this.robustClick(this.knativeResourceOption);
89+
}
90+
91+
async clickCreate(): Promise<void> {
92+
await this.createButton.scrollIntoViewIfNeeded();
93+
await expect(async () => {
94+
await expect(this.createButton).toBeEnabled();
95+
}).toPass({ timeout: 90_000, intervals: [1_000, 2_000, 5_000] });
96+
await this.robustClick(this.createButton);
97+
await this.waitForLoadingComplete();
98+
}
99+
100+
async enterExternalRegistryImageName(imageName: string): Promise<void> {
101+
await this.externalRegistryInput.clear();
102+
await this.externalRegistryInput.fill(imageName);
103+
await expect(
104+
this.page.locator('.pf-v6-c-helper-text').filter({ hasText: /Validated|Loading/ }).first(),
105+
).toBeVisible({ timeout: 60_000 });
106+
await expect(this.componentNameInput).not.toHaveValue('', { timeout: 30_000 });
107+
}
108+
109+
async clickEditImportStrategy(): Promise<void> {
110+
await this.robustClick(this.importStrategyEditButton);
111+
}
112+
113+
async selectDockerfileStrategy(): Promise<void> {
114+
await this.robustClick(this.dockerfileStrategy);
115+
}
116+
117+
async enterDockerfilePath(dockerfilePath: string): Promise<void> {
118+
await this.dockerfilePathInput.clear();
119+
await this.dockerfilePathInput.fill(dockerfilePath);
120+
}
121+
122+
getKnativeServiceOption(): Locator {
123+
return this.knativeResourceOption;
124+
}
125+
126+
getResourceTypeDropdown(): Locator {
127+
return this.resourcesDropdown;
128+
}
129+
130+
getHeading(): Locator {
131+
return this.pageHeading;
132+
}
133+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { Locator } from '@playwright/test';
2+
3+
import BasePage from '../base-page';
4+
5+
export class AdminEventingPage extends BasePage {
6+
private readonly createButton = this.page.getByTestId('tab-list-page-create');
7+
private readonly emptyMessage = this.page.getByTestId('empty-box-body');
8+
9+
private readonly eventSourcesTab = this.page.getByTestId('horizontal-link-Event Sources');
10+
private readonly brokersTab = this.page.getByTestId('horizontal-link-Brokers');
11+
private readonly triggersTab = this.page.getByTestId('horizontal-link-Triggers');
12+
private readonly channelsTab = this.page.getByTestId('horizontal-link-Channels');
13+
private readonly subscriptionsTab = this.page.getByTestId('horizontal-link-Subscriptions');
14+
15+
async navigateToEventing(namespace: string): Promise<void> {
16+
await this.goTo(`/eventing/ns/${namespace}`);
17+
}
18+
19+
getCreateButton(): Locator {
20+
return this.createButton;
21+
}
22+
23+
getEmptyMessage(): Locator {
24+
return this.emptyMessage;
25+
}
26+
27+
async clickTab(
28+
tab: 'Event Sources' | 'Brokers' | 'Triggers' | 'Channels' | 'Subscriptions',
29+
): Promise<void> {
30+
const tabMap = {
31+
'Event Sources': this.eventSourcesTab,
32+
Brokers: this.brokersTab,
33+
Triggers: this.triggersTab,
34+
Channels: this.channelsTab,
35+
Subscriptions: this.subscriptionsTab,
36+
};
37+
await this.robustClick(tabMap[tab]);
38+
await this.waitForLoadingComplete();
39+
}
40+
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import type { Locator } from '@playwright/test';
2+
import { expect } from '../../fixtures';
3+
4+
import BasePage from '../base-page';
5+
6+
export class TopologyKnativePage extends BasePage {
7+
private readonly graphView = this.page.getByTestId('topology-switcher-view');
8+
private readonly fitToScreen = this.page.locator('#fit-to-screen');
9+
private readonly resetView = this.page.locator('#reset-view');
10+
private readonly searchInput = this.page.getByTestId('item-filter');
11+
private readonly highlightedNode = this.page.locator('.is-filtered').first();
12+
private readonly knativeServiceNode = this.page.locator('[data-type="knative-service"]');
13+
private readonly sidePane = this.page.getByTestId('topology-sidepane');
14+
private readonly sidePaneClose = this.page.getByTestId('topology-sidepane').locator('button[aria-label="Close"]');
15+
private readonly editAnnotationsLink = this.page.getByTestId('edit-annotations');
16+
private readonly modalTitle = this.page.getByTestId('modal-title');
17+
private readonly modalCancel = this.page.getByTestId('modal-cancel-action');
18+
19+
async navigateToTopology(namespace: string): Promise<void> {
20+
await this.goTo(`/topology/ns/${namespace}`);
21+
}
22+
23+
async switchToGraphView(): Promise<void> {
24+
const graphViewToggle = this.graphView.and(this.page.locator('[aria-label="Graph view"]'));
25+
const switcherCount = await graphViewToggle.count();
26+
if (switcherCount > 0) {
27+
await this.robustClick(graphViewToggle);
28+
}
29+
}
30+
31+
async fitScreen(): Promise<void> {
32+
await this.robustClick(this.fitToScreen);
33+
}
34+
35+
async resetViewport(): Promise<void> {
36+
await this.robustClick(this.resetView);
37+
}
38+
39+
async search(name: string): Promise<void> {
40+
await this.searchInput.fill(name);
41+
}
42+
43+
async verifyWorkloadVisible(name: string): Promise<void> {
44+
await this.search(name);
45+
await expect(this.highlightedNode).toBeVisible({ timeout: 60_000 });
46+
}
47+
48+
async rightClickOnKnativeService(serviceName: string): Promise<void> {
49+
await this.switchToGraphView();
50+
await this.fitScreen();
51+
const serviceLabel = this.page
52+
.locator('.odc-knative-service__label')
53+
.filter({ hasText: serviceName })
54+
.first();
55+
// eslint-disable-next-line playwright/no-force-option
56+
await serviceLabel.click({ button: 'right', force: true, timeout: 30_000 });
57+
}
58+
59+
async rightClickOnKnativeRevision(serviceName: string): Promise<void> {
60+
await this.switchToGraphView();
61+
await this.fitScreen();
62+
// Target the revision node — revision labels are stable (no controller reconciliation conflict)
63+
const revisionNode = this.page
64+
.locator('[data-type="knative-revision"]')
65+
.filter({ hasText: serviceName })
66+
.first();
67+
// eslint-disable-next-line playwright/no-force-option
68+
await revisionNode.click({ button: 'right', force: true, timeout: 30_000 });
69+
}
70+
71+
async selectContextMenuAction(action: string): Promise<void> {
72+
const menuItem = this.page.locator(
73+
`[data-test="${action}"], [data-test-action="${action}"]`,
74+
);
75+
await this.robustClick(menuItem.first(), { timeout: 10_000 });
76+
}
77+
78+
async rightClickAndSelectAction(serviceName: string, action: string): Promise<void> {
79+
await this.rightClickOnKnativeService(serviceName);
80+
await this.selectContextMenuAction(action);
81+
}
82+
83+
async rightClickRevisionAndSelectAction(serviceName: string, action: string): Promise<void> {
84+
await this.rightClickOnKnativeRevision(serviceName);
85+
await this.selectContextMenuAction(action);
86+
}
87+
88+
async clickOnKnativeService(serviceName: string): Promise<void> {
89+
await this.switchToGraphView();
90+
await this.fitScreen();
91+
const serviceLabel = this.knativeServiceNode
92+
.locator('.odc-base-node__label')
93+
.filter({ hasText: serviceName });
94+
// eslint-disable-next-line playwright/no-force-option
95+
await serviceLabel.click({ force: true, timeout: 30_000 });
96+
}
97+
98+
async clickOnApplicationGrouping(appName: string): Promise<void> {
99+
const appNode = this.page.locator(`[data-id="group:${appName}"]`).first();
100+
// eslint-disable-next-line playwright/no-force-option
101+
await appNode.click({ force: true, timeout: 30_000 });
102+
}
103+
104+
async verifySidePaneOpen(): Promise<void> {
105+
await expect(this.sidePane).toBeVisible({ timeout: 30_000 });
106+
}
107+
108+
async selectSidePaneTab(tabName: string): Promise<void> {
109+
const tab = this.sidePane.getByRole('tab', { name: tabName });
110+
await this.robustClick(tab);
111+
await this.waitForLoadingComplete();
112+
}
113+
114+
async closeSidePane(): Promise<void> {
115+
if ((await this.sidePaneClose.count()) > 0) {
116+
await this.robustClick(this.sidePaneClose);
117+
}
118+
}
119+
120+
getSidePane(): Locator {
121+
return this.sidePane;
122+
}
123+
124+
getKnativeServiceNode(): Locator {
125+
return this.knativeServiceNode;
126+
}
127+
128+
getEditAnnotationsLink(): Locator {
129+
return this.editAnnotationsLink;
130+
}
131+
132+
getModalTitle(): Locator {
133+
return this.modalTitle;
134+
}
135+
136+
getModalCancel(): Locator {
137+
return this.modalCancel;
138+
}
139+
140+
async clickOnTopologyNode(nodeName: string): Promise<void> {
141+
await this.switchToGraphView();
142+
await this.fitScreen();
143+
const nodeLabel = this.page
144+
.locator('g[class$="topology__node__label"] text')
145+
.filter({ hasText: nodeName })
146+
.first();
147+
// eslint-disable-next-line playwright/no-force-option
148+
await nodeLabel.click({ force: true, timeout: 30_000 });
149+
}
150+
151+
async rightClickOnTopologyNode(nodeName: string): Promise<void> {
152+
await this.switchToGraphView();
153+
await this.fitScreen();
154+
const nodeLabel = this.page
155+
.locator('g[class$="topology__node__label"] text')
156+
.filter({ hasText: nodeName })
157+
.first();
158+
// eslint-disable-next-line playwright/no-force-option
159+
await nodeLabel.click({ button: 'right', force: true, timeout: 30_000 });
160+
}
161+
162+
async rightClickNodeAndSelectAction(nodeName: string, action: string): Promise<void> {
163+
await this.rightClickOnTopologyNode(nodeName);
164+
await this.selectContextMenuAction(action);
165+
}
166+
167+
async selectSidebarAction(actionName: string): Promise<void> {
168+
const actionsButton = this.sidePane.locator(
169+
'[data-test="actions-menu-button"], [data-test-id="actions-menu-button"]',
170+
);
171+
await actionsButton.first().click({ timeout: 10_000 });
172+
const actionItem = this.page.locator(
173+
`[data-test="${actionName}"], [data-test-action="${actionName}"]`,
174+
);
175+
await actionItem.first().click({ timeout: 10_000 });
176+
}
177+
}

0 commit comments

Comments
 (0)