Skip to content
Open
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
133 changes: 133 additions & 0 deletions frontend/e2e/pages/knative/add-flow-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import type { Locator } from '@playwright/test';
import { expect } from '@playwright/test';

import BasePage from '../base-page';

export class AddFlowPage extends BasePage {
private readonly gitUrlInput = this.page.locator('#form-input-git-url-field');
private readonly componentNameInput = this.page.locator('#form-input-name-field');
private readonly appNameInput = this.page.locator('#form-input-application-name-field');
private readonly createButton = this.page.getByTestId('save-changes');
private readonly resourcesDropdown = this.page.locator(
'#form-select-input-resources-field',
);
private readonly knativeResourceOption = this.page.locator(
'#select-option-resources-knative',
);
private readonly importStrategyEditButton = this.page.locator(
'.odc-import-strategy-section__edit-strategy-button',
);
private readonly dockerfileStrategy = this.page.getByTestId('import-strategy Dockerfile');
private readonly dockerfilePathInput = this.page.locator(
'#form-input-docker-dockerfilePath-field',
);
private readonly externalRegistryInput = this.page.locator(
'#form-input-searchTerm-field',
);
private readonly pageHeading = this.page.getByTestId('page-heading').locator('h1');

async navigateToAddPage(namespace: string): Promise<void> {
await this.goTo(`/add/ns/${namespace}`);
await this.waitForLoadingComplete();
}

async clickCard(cardId: string): Promise<void> {
const card = this.page.getByTestId(`item ${cardId}`);
await card.scrollIntoViewIfNeeded();
await this.robustClick(card);
await this.waitForLoadingComplete();
}

async clickImportFromGitCard(): Promise<void> {
await this.clickCard('import-from-git');
}

async clickContainerImageCard(): Promise<void> {
await this.clickCard('deploy-image');
}

async enterGitUrl(url: string): Promise<void> {
await this.gitUrlInput.clear();
await this.gitUrlInput.fill(url);
await expect(
this.page.locator('.pf-v6-c-helper-text').filter({ hasText: /Validated|Rate limit/ }).first(),
).toBeVisible({ timeout: 60_000 });

// If rate limited, auto-detection fails — manually select Builder Image strategy and Node.js
const rateLimitMsg = this.page.getByText('Rate limit exceeded');
if ((await rateLimitMsg.count()) > 0) {
// Select "Builder Image" import strategy
const builderImageStrategy = this.page.getByTestId('import-strategy Builder Image');
if ((await builderImageStrategy.count()) > 0) {
await this.robustClick(builderImageStrategy);
}
// Wait for builder image list to load, then select Node.js
const nodeJsImage = this.page.locator('.odc-selector-card').filter({ hasText: 'Node.js' });
if ((await nodeJsImage.count()) > 0) {
await this.robustClick(nodeJsImage.first());
}
}
}

async enterComponentName(name: string): Promise<void> {
await this.componentNameInput.scrollIntoViewIfNeeded();
await this.componentNameInput.click();
await this.componentNameInput.clear();
await this.componentNameInput.fill(name);
await expect(this.componentNameInput).toHaveValue(name);
}

async enterAppName(name: string): Promise<void> {
await this.appNameInput.clear();
await this.appNameInput.fill(name);
}

async selectServerlessDeployment(): Promise<void> {
await this.resourcesDropdown.scrollIntoViewIfNeeded();
await this.robustClick(this.resourcesDropdown);
await this.robustClick(this.knativeResourceOption);
}

async clickCreate(): Promise<void> {
await this.createButton.scrollIntoViewIfNeeded();
await expect(async () => {
await expect(this.createButton).toBeEnabled();
}).toPass({ timeout: 90_000, intervals: [1_000, 2_000, 5_000] });
await this.robustClick(this.createButton);
await this.waitForLoadingComplete();
}

async enterExternalRegistryImageName(imageName: string): Promise<void> {
await this.externalRegistryInput.clear();
await this.externalRegistryInput.fill(imageName);
await expect(
this.page.locator('.pf-v6-c-helper-text').filter({ hasText: /Validated|Loading/ }).first(),
).toBeVisible({ timeout: 60_000 });
await expect(this.componentNameInput).not.toHaveValue('', { timeout: 30_000 });
}

async clickEditImportStrategy(): Promise<void> {
await this.robustClick(this.importStrategyEditButton);
}

async selectDockerfileStrategy(): Promise<void> {
await this.robustClick(this.dockerfileStrategy);
}

async enterDockerfilePath(dockerfilePath: string): Promise<void> {
await this.dockerfilePathInput.clear();
await this.dockerfilePathInput.fill(dockerfilePath);
}

getKnativeServiceOption(): Locator {
return this.knativeResourceOption;
}

getResourceTypeDropdown(): Locator {
return this.resourcesDropdown;
}

getHeading(): Locator {
return this.pageHeading;
}
}
40 changes: 40 additions & 0 deletions frontend/e2e/pages/knative/admin-eventing-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { Locator } from '@playwright/test';

import BasePage from '../base-page';

export class AdminEventingPage extends BasePage {
private readonly createButton = this.page.getByTestId('tab-list-page-create');
private readonly emptyMessage = this.page.getByTestId('empty-box-body');

private readonly eventSourcesTab = this.page.getByTestId('horizontal-link-Event Sources');
private readonly brokersTab = this.page.getByTestId('horizontal-link-Brokers');
private readonly triggersTab = this.page.getByTestId('horizontal-link-Triggers');
private readonly channelsTab = this.page.getByTestId('horizontal-link-Channels');
private readonly subscriptionsTab = this.page.getByTestId('horizontal-link-Subscriptions');

async navigateToEventing(namespace: string): Promise<void> {
await this.goTo(`/eventing/ns/${namespace}`);
}

getCreateButton(): Locator {
return this.createButton;
}

getEmptyMessage(): Locator {
return this.emptyMessage;
}

async clickTab(
tab: 'Event Sources' | 'Brokers' | 'Triggers' | 'Channels' | 'Subscriptions',
): Promise<void> {
const tabMap = {
'Event Sources': this.eventSourcesTab,
Brokers: this.brokersTab,
Triggers: this.triggersTab,
Channels: this.channelsTab,
Subscriptions: this.subscriptionsTab,
};
await this.robustClick(tabMap[tab]);
await this.waitForLoadingComplete();
}
}
177 changes: 177 additions & 0 deletions frontend/e2e/pages/knative/topology-knative-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import type { Locator } from '@playwright/test';
import { expect } from '../../fixtures';

import BasePage from '../base-page';

export class TopologyKnativePage extends BasePage {
private readonly graphView = this.page.getByTestId('topology-switcher-view');
private readonly fitToScreen = this.page.locator('#fit-to-screen');
private readonly resetView = this.page.locator('#reset-view');
private readonly searchInput = this.page.getByTestId('item-filter');
private readonly highlightedNode = this.page.locator('.is-filtered').first();
private readonly knativeServiceNode = this.page.locator('[data-type="knative-service"]');
private readonly sidePane = this.page.getByTestId('topology-sidepane');
private readonly sidePaneClose = this.page.getByTestId('topology-sidepane').locator('button[aria-label="Close"]');
private readonly editAnnotationsLink = this.page.getByTestId('edit-annotations');
private readonly modalTitle = this.page.getByTestId('modal-title');
private readonly modalCancel = this.page.getByTestId('modal-cancel-action');

async navigateToTopology(namespace: string): Promise<void> {
await this.goTo(`/topology/ns/${namespace}`);
}

async switchToGraphView(): Promise<void> {
const graphViewToggle = this.graphView.and(this.page.locator('[aria-label="Graph view"]'));
const switcherCount = await graphViewToggle.count();
if (switcherCount > 0) {
await this.robustClick(graphViewToggle);
}
}

async fitScreen(): Promise<void> {
await this.robustClick(this.fitToScreen);
}

async resetViewport(): Promise<void> {
await this.robustClick(this.resetView);
}

async search(name: string): Promise<void> {
await this.searchInput.fill(name);
}

async verifyWorkloadVisible(name: string): Promise<void> {
await this.search(name);
await expect(this.highlightedNode).toBeVisible({ timeout: 60_000 });
}

async rightClickOnKnativeService(serviceName: string): Promise<void> {
await this.switchToGraphView();
await this.fitScreen();
const serviceLabel = this.page
.locator('.odc-knative-service__label')
.filter({ hasText: serviceName })
.first();
// eslint-disable-next-line playwright/no-force-option
await serviceLabel.click({ button: 'right', force: true, timeout: 30_000 });
}

async rightClickOnKnativeRevision(serviceName: string): Promise<void> {
await this.switchToGraphView();
await this.fitScreen();
// Target the revision node — revision labels are stable (no controller reconciliation conflict)
const revisionNode = this.page
.locator('[data-type="knative-revision"]')
.filter({ hasText: serviceName })
.first();
// eslint-disable-next-line playwright/no-force-option
await revisionNode.click({ button: 'right', force: true, timeout: 30_000 });
}

async selectContextMenuAction(action: string): Promise<void> {
const menuItem = this.page.locator(
`[data-test="${action}"], [data-test-action="${action}"]`,
);
await this.robustClick(menuItem.first(), { timeout: 10_000 });
}

async rightClickAndSelectAction(serviceName: string, action: string): Promise<void> {
await this.rightClickOnKnativeService(serviceName);
await this.selectContextMenuAction(action);
}

async rightClickRevisionAndSelectAction(serviceName: string, action: string): Promise<void> {
await this.rightClickOnKnativeRevision(serviceName);
await this.selectContextMenuAction(action);
}

async clickOnKnativeService(serviceName: string): Promise<void> {
await this.switchToGraphView();
await this.fitScreen();
const serviceLabel = this.knativeServiceNode
.locator('.odc-base-node__label')
.filter({ hasText: serviceName });
// eslint-disable-next-line playwright/no-force-option
await serviceLabel.click({ force: true, timeout: 30_000 });
}

async clickOnApplicationGrouping(appName: string): Promise<void> {
const appNode = this.page.locator(`[data-id="group:${appName}"]`).first();
// eslint-disable-next-line playwright/no-force-option
await appNode.click({ force: true, timeout: 30_000 });
}

async verifySidePaneOpen(): Promise<void> {
await expect(this.sidePane).toBeVisible({ timeout: 10_000 });
}

async selectSidePaneTab(tabName: string): Promise<void> {
const tab = this.sidePane.getByRole('tab', { name: tabName });
await this.robustClick(tab);
await this.waitForLoadingComplete();
}

async closeSidePane(): Promise<void> {
if ((await this.sidePaneClose.count()) > 0) {
await this.robustClick(this.sidePaneClose);
}
}

getSidePane(): Locator {
return this.sidePane;
}

getKnativeServiceNode(): Locator {
return this.knativeServiceNode;
}

getEditAnnotationsLink(): Locator {
return this.editAnnotationsLink;
}

getModalTitle(): Locator {
return this.modalTitle;
}

getModalCancel(): Locator {
return this.modalCancel;
}

async clickOnTopologyNode(nodeName: string): Promise<void> {
await this.switchToGraphView();
await this.fitScreen();
const nodeLabel = this.page
.locator('g[class$="topology__node__label"] text')
.filter({ hasText: nodeName })
.first();
// eslint-disable-next-line playwright/no-force-option
await nodeLabel.click({ force: true, timeout: 30_000 });
}

async rightClickOnTopologyNode(nodeName: string): Promise<void> {
await this.switchToGraphView();
await this.fitScreen();
const nodeLabel = this.page
.locator('g[class$="topology__node__label"] text')
.filter({ hasText: nodeName })
.first();
// eslint-disable-next-line playwright/no-force-option
await nodeLabel.click({ button: 'right', force: true, timeout: 30_000 });
}

async rightClickNodeAndSelectAction(nodeName: string, action: string): Promise<void> {
await this.rightClickOnTopologyNode(nodeName);
await this.selectContextMenuAction(action);
}

async selectSidebarAction(actionName: string): Promise<void> {
const actionsButton = this.sidePane.locator(
'[data-test="actions-menu-button"], [data-test-id="actions-menu-button"]',
);
await actionsButton.first().click({ timeout: 10_000 });
const actionItem = this.page.locator(
`[data-test="${actionName}"], [data-test-action="${actionName}"]`,
);
await actionItem.first().click({ timeout: 10_000 });
}
}
Loading