Skip to content
Merged
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
18 changes: 18 additions & 0 deletions frontend/e2e/pages/dev-console/add-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,15 @@ export class AddPage extends BasePage {
return this.page.getByRole('button', { name: 'Create', exact: true });
}

async clickCreate(): Promise<void> {
await this.robustClick(this.getSubmitButton());
}

async selectBuilderImageVersion(version: string): Promise<void> {
await this.robustClick(this.getBuilderImageVersionToggle());
await this.robustClick(this.getBuilderImageVersionItem(version));
}

getCancelButton(): Locator {
return this.page.getByRole('button', { name: 'Cancel', exact: true });
}
Expand Down Expand Up @@ -361,6 +370,15 @@ export class DeployImagePage extends BasePage {
await this.robustClick(option);
}

async selectCustomIcon(iconUrl: string): Promise<void> {
await this.robustClick(this.page.getByTestId('add-custom-icon'));
const urlInput = this.page.getByTestId('import-custom-icon-url-input');
await expect(urlInput).toBeVisible();
await expect(urlInput).toBeEnabled();
await urlInput.fill(iconUrl);
await this.robustClick(this.page.getByTestId('import-custom-icon-confirm'));
}

async selectApplication(appName: string): Promise<void> {
const dropdown = this.page.getByTestId('application-form-app-dropdown');
await this.robustClick(dropdown);
Expand Down
75 changes: 75 additions & 0 deletions frontend/e2e/pages/dev-console/build-config-page.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Locator } from '@playwright/test';

import { expect } from '../../fixtures';
import BasePage from '../base-page';

export class BuildConfigPage extends BasePage {
Expand Down Expand Up @@ -27,6 +28,80 @@ export class BuildConfigPage extends BasePage {
return this.page.getByText('Git Repo URL');
}

getGitRepoUrlInput(): Locator {
return this.page.getByRole('textbox', { name: 'Git Repo URL' });
}

getEnvironmentSection(): Locator {
return this.page.getByTestId('section environment-variables');
}

getEnvironmentVariableNames(): Locator {
return this.getEnvironmentSection().getByTestId('pairs-list-name');
}

getEnvironmentVariableValues(): Locator {
return this.getEnvironmentSection().getByTestId('pairs-list-value');
}

getImageOption(type: 'build-from' | 'push-to'): Locator {
return this.page.getByTestId(`${type} type`);
}

getImageInput(
type: 'build-from' | 'push-to',
input: 'image-stream-image' | 'docker-image',
): Locator {
return this.page.getByTestId(`${type} ${input}`);
}

getImageStreamTagSection(type: 'build-from' | 'push-to'): Locator {
return this.page.getByTestId(`${type} image-stream-tag`);
}

getContextDirInput(): Locator {
return this.page.getByRole('textbox', { name: 'Context dir' });
}

async selectImageOption(type: 'build-from' | 'push-to', option: string): Promise<void> {
await this.robustClick(this.getImageOption(type));
await this.robustClick(this.page.getByRole('option', { name: option, exact: true }));
}

async selectImageStreamTag(
type: 'build-from' | 'push-to',
project: string,
imageStream: string,
tag: string,
): Promise<void> {
await this.selectImageOption(type, 'Image Stream Tag');
const section = this.getImageStreamTagSection(type);
for (const [label, value] of [
['Project', project],
['Image Stream', imageStream],
['Tag', tag],
]) {
await this.robustClick(section.getByRole('button', { name: label }));
await this.robustClick(this.page.getByRole('option', { name: value, exact: true }));
}
}

async expandAdvancedGitOptions(): Promise<void> {
await this.robustClick(this.page.getByRole('button', { name: /advanced Git options/i }));
await expect(this.getContextDirInput()).toBeVisible({ timeout: 30_000 });
}

async addEnvironmentVariable(name: string, value: string): Promise<void> {
const section = this.getEnvironmentSection();
await this.robustClick(section.getByTestId('add-button'));
await this.getEnvironmentVariableNames().last().fill(name);
await this.getEnvironmentVariableValues().last().fill(value);
}

async save(): Promise<void> {
await this.robustClick(this.page.getByRole('button', { name: 'Save', exact: true }));
}

async expandAdvancedOption(optionName: string): Promise<void> {
const toggle = this.page.getByRole('button', { name: optionName });
await this.robustClick(toggle);
Expand Down
79 changes: 79 additions & 0 deletions frontend/e2e/pages/dev-console/cluster-customization-page.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Locator } from '@playwright/test';

import { expect } from '../../fixtures';
import BasePage from '../base-page';

export class ClusterCustomizationPage extends BasePage {
Expand Down Expand Up @@ -35,4 +36,82 @@ export class ClusterCustomizationPage extends BasePage {
getPinnedResources(): Locator {
return this.page.getByText('Pinned Resources', { exact: true });
}

getFormSection(name: 'catalog-types' | 'add-page'): Locator {
return this.page.getByTestId(`${name} form-section`);
}

private getSelector(name: 'catalog-types' | 'add-page'): Locator {
return this.page.getByTestId(`${name}-selector`);
}

private getSelectorPane(
name: 'catalog-types' | 'add-page',
list: 'available' | 'chosen',
): Locator {
return this.getSelector(name).locator(
`#${name}-selector-list-${list === 'available' ? 'available' : 'chosen'}-pane`,
);
}

private async getPaneOrSelector(
Comment thread
shahsahil264 marked this conversation as resolved.
name: 'catalog-types' | 'add-page',
list: 'available' | 'chosen',
): Promise<Locator> {
const pane = this.getSelectorPane(name, list);
const searchName = list === 'available' ? 'Available search input' : 'Chosen search input';
if ((await pane.getByRole('textbox', { name: searchName }).count()) > 0) return pane;
const selector = this.getSelector(name);
return (await selector.getByRole('textbox', { name: searchName }).count()) > 0
? selector
: this.getFormSection(name);
}

private async ensureFormSection(name: 'catalog-types' | 'add-page'): Promise<Locator> {
const section = this.getFormSection(name);
if (!(await section.isVisible().catch(() => false))) {
await this.robustClick(this.page.getByRole('tab', { name: 'Developer' }));
}
await expect(section).toBeVisible({ timeout: 30_000 });
return section;
}

async waitForItemInList(
name: 'catalog-types' | 'add-page',
item: string,
list: 'available' | 'chosen',
): Promise<void> {
await this.ensureFormSection(name);
const searchName = list === 'available' ? 'Available search input' : 'Chosen search input';
const pane = await this.getPaneOrSelector(name, list);
const search = pane.getByRole('textbox', { name: searchName });
await search.fill(item);
await expect(pane.getByRole('option').filter({ hasText: item }).first()).toBeVisible({
timeout: 30_000,
});
}

async moveAvailableToChosen(name: 'catalog-types' | 'add-page', item: string): Promise<void> {
await this.ensureFormSection(name);
const pane = await this.getPaneOrSelector(name, 'available');
const search = pane.getByRole('textbox', { name: 'Available search input' });
await search.fill(item);
const option = pane.getByRole('option').filter({ hasText: item }).first();
const addButton = this.getFormSection(name).getByRole('button', { name: 'Add selected' });
await this.robustClick(option);
await expect(addButton).toBeEnabled({ timeout: 10_000 });
await this.robustClick(addButton);
}

async moveChosenToAvailable(name: 'catalog-types' | 'add-page', item: string): Promise<void> {
await this.ensureFormSection(name);
const pane = await this.getPaneOrSelector(name, 'chosen');
const search = pane.getByRole('textbox', { name: 'Chosen search input' });
await search.fill(item);
const option = pane.getByRole('option').filter({ hasText: item }).first();
const removeButton = this.getFormSection(name).getByRole('button', { name: 'Remove selected' });
await this.robustClick(option);
await expect(removeButton).toBeEnabled({ timeout: 10_000 });
await this.robustClick(removeButton);
}
}
35 changes: 35 additions & 0 deletions frontend/e2e/pages/dev-console/deployment-page.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Locator } from '@playwright/test';

import { expect } from '../../fixtures';
import BasePage from '../base-page';

Expand Down Expand Up @@ -29,4 +31,37 @@ export class DeploymentPage extends BasePage {
await expect(this.createButton).toBeEnabled();
await this.robustClick(this.createButton);
}

async navigateToEditForm(
namespace: string,
name: string,
resource: 'deployments' | 'deploymentconfigs' = 'deployments',
): Promise<void> {
await this.goTo(`/k8s/ns/${namespace}/${resource}/${name}/form`);
await this.ensureFormView(this.nameInput);
}

getAutoDeployImage(): Locator {
return this.page.getByRole('checkbox', { name: 'Auto deploy when new Image is available' });
}

getAutoDeployConfig(): Locator {
return this.page.getByRole('checkbox', {
name: 'Auto deploy when deployment configuration changes',
});
}

async reloadIfStale(): Promise<void> {
const reloadButton = this.page.getByRole('button', { name: 'Reload', exact: true });
const staleAlert = this.page.getByText('This object has been updated.', { exact: true });
if (await staleAlert.isVisible().catch(() => false)) {
await this.robustClick(reloadButton);
await this.ensureFormView(this.nameInput);
await expect(staleAlert).not.toBeVisible({ timeout: 30_000 });
}
}

async save(): Promise<void> {
await this.robustClick(this.page.getByRole('button', { name: 'Save', exact: true }));
}
}
78 changes: 78 additions & 0 deletions frontend/e2e/pages/dev-console/route-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { expect } from '../../fixtures';
import BasePage from '../base-page';

export class RoutePage extends BasePage {
private readonly name = this.page.getByRole('textbox', { name: 'Name', exact: true });
private readonly service = this.page.getByRole('combobox', { name: /Service/i });
private readonly targetPort = this.page.getByRole('combobox', { name: /Target Port/i });
private readonly hostname = this.page.getByRole('textbox', { name: 'Hostname' });
private readonly createButton = this.page.getByRole('button', { name: 'Create', exact: true });

async navigateToCreate(namespace: string): Promise<void> {
await this.goTo(`/k8s/ns/${namespace}/routes/~new/form`);
}

async navigateToEdit(namespace: string, name: string): Promise<void> {
await this.goTo(`/k8s/ns/${namespace}/routes/${name}`);
await this.robustClick(this.page.getByRole('button', { name: 'Actions' }));
await this.robustClick(this.page.getByRole('menuitem', { name: 'Edit Route' }));
}

isFormView(): Promise<boolean> {
const yamlEditor = this.page.getByRole('textbox', { name: /Editor content/ });
return expect(this.name.or(yamlEditor).first())
.toBeVisible({ timeout: 30_000 })
.then(async () => this.name.isVisible());
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

async fillForm(name: string, service: string, port: string, host?: string): Promise<void> {
await this.name.fill(name);
await this.robustClick(this.service);
await this.robustClick(this.page.getByRole('option', { name: service, exact: true }));
await this.robustClick(this.targetPort);
await this.robustClick(this.page.getByRole('option', { name: port, exact: true }));
if (host) await this.hostname.fill(host);
}

async fillYaml(name: string, service: string, port: string, host?: string): Promise<void> {
await this.setEditorContent(
[
'apiVersion: route.openshift.io/v1',
'kind: Route',
'metadata:',
` name: ${name}`,
'spec:',
` ${host ? `host: ${host}\n ` : ''}to:`,
' kind: Service',
` name: ${service}`,
' port:',
` targetPort: ${port}`,
].join('\n'),
);
}

async create(): Promise<void> {
await this.robustClick(this.createButton);
}

async save(): Promise<void> {
await this.robustClick(this.page.getByRole('button', { name: 'Save', exact: true }));
}

async setFormHostname(host: string): Promise<void> {
await expect(this.hostname).toBeVisible({ timeout: 30_000 });
await this.hostname.fill(host);
}

async setYamlHostname(host: string): Promise<void> {
await expect(this.page.getByRole('textbox', { name: /Editor content/ })).toBeVisible({
timeout: 30_000,
});
const content = await this.getEditorContent();
if (!/^spec:\n/m.test(content)) {
throw new Error('Route YAML does not contain a top-level spec field');
}
const updated = content.replace(/^(spec:\n)/m, `$1 host: ${host}\n`);
await this.setEditorContent(updated);
}
}
11 changes: 11 additions & 0 deletions frontend/e2e/pages/topology-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ export class TopologyPage extends BasePage {
return this.graphSurface;
}

getHighlightedNode(): Locator {
return this.highlightedNode;
}

async clickStartBuilding(): Promise<void> {
await this.robustClick(this.startBuildingLink);
}
Expand All @@ -96,6 +100,13 @@ export class TopologyPage extends BasePage {
await expect(this.highlightedNode.first()).toBeAttached({ timeout });
}

async verifyWorkloadIcon(workloadName: string, iconUrl: string): Promise<void> {
await this.ensureGraphView();
await this.search(workloadName);
const icon = this.highlightedNode.first().getByTestId('base-node-handler').locator('image');
await expect(icon).toHaveAttribute('xlink:href', iconUrl);
}

// PF Topology internal class — no data-test available; may break on PF upgrades
async verifyGroupLabel(workloadName: string, groupName: string, timeout = 15_000): Promise<void> {
await this.ensureGraphView();
Expand Down
10 changes: 9 additions & 1 deletion frontend/e2e/tests/console/crud/other-routes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { testA11y } from '../../../utils/a11y';
type RouteConfig = {
path: string;
assertLoaded?: (page: Page) => Promise<void>;
streaming?: boolean;
};

async function assertLoadedListPage(page: Page): Promise<void> {
Expand All @@ -14,6 +15,10 @@ async function assertLoadedListPage(page: Page): Promise<void> {
).toBeVisible();
}

async function assertLoadingComplete(page: Page): Promise<void> {
await expect(page.getByTestId('loading-indicator')).toHaveCount(0);
}

const routes: RouteConfig[] = [
{
path: '/',
Expand All @@ -36,6 +41,7 @@ const routes: RouteConfig[] = [
},
{
path: '/k8s/all-namespaces/events',
streaming: true,
assertLoaded: async (page) => {
await expect(page.getByTestId('event-totals')).toBeVisible();
},
Expand Down Expand Up @@ -139,7 +145,9 @@ test.describe('Visiting other routes', { tag: ['@admin', '@smoke'] }, () => {
await page.goto(route.path, { timeout: 90_000 });
const expectedPath = route.path.split('?')[0];
await expect(page).toHaveURL((url) => url.pathname === expectedPath);
await expect(page.getByTestId('loading-indicator')).toHaveCount(0);
if (!route.streaming) {
await assertLoadingComplete(page);
}
await expect(page.getByTestId('error-page')).not.toBeAttached();

if (route.assertLoaded) {
Expand Down
Loading