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
35 changes: 35 additions & 0 deletions frontend/e2e/pages/delete-helm-release-modal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { expect, type Locator, type Page } from '@playwright/test';
import BasePage from './base-page';

export class DeleteHelmReleaseModal extends BasePage {
private readonly modal: Locator;
private readonly modalTitle: Locator;
private readonly resourceNameDisplay: Locator;
private readonly releaseNameInput: Locator;
private readonly deleteButton: Locator;

constructor(page: Page) {
super(page);
this.modal = this.page.locator('[role="dialog"]');
this.modalTitle = this.page.locator('[data-test-id="modal-title"]');
this.resourceNameDisplay = this.page.getByTestId('resource-name');
this.releaseNameInput = this.page.locator('#form-input-resourceName-field');
this.deleteButton = this.page.getByTestId('confirm-action');
}

async verifyModalOpen(releaseName: string): Promise<void> {
await expect(this.modalTitle).toContainText('Delete Helm Release?');
await expect(this.resourceNameDisplay).toHaveText(releaseName);
}

async enterReleaseName(releaseName: string): Promise<void> {
await this.releaseNameInput.fill(releaseName);
}

async clickDelete(): Promise<void> {
await this.robustClick(this.deleteButton, { force: true });

// Wait for modal to close
await expect(this.modal).not.toBeAttached({ timeout: 10_000 });
}
}
81 changes: 81 additions & 0 deletions frontend/e2e/pages/helm-details-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { expect, type Locator, type Page } from '@playwright/test';
import BasePage from './base-page';

export class HelmDetailsPage extends BasePage {
private readonly title: Locator;
private readonly statusText: Locator;
private readonly detailsTab: Locator;
private readonly resourcesTab: Locator;
private readonly revisionHistoryTab: Locator;
private readonly releaseNotesTab: Locator;
private readonly actionsMenuButton: Locator;

constructor(page: Page) {
super(page);
this.title = this.page.locator('[data-test-section-heading="Helm Release details"]');
this.statusText = this.page.getByTestId('helm-release-status-details').getByTestId('status-text');
this.detailsTab = this.page.locator('[data-test-id="horizontal-link-Details"]');
this.resourcesTab = this.page.locator('[data-test-id="horizontal-link-Resources"]');
this.revisionHistoryTab = this.page.getByTestId('horizontal-link-Revision history');
this.releaseNotesTab = this.page.getByTestId('horizontal-link-Release notes');
this.actionsMenuButton = this.page.locator('[data-test-id="actions-menu-button"]');
}

async verifyTitle(): Promise<void> {
await expect(this.title).toBeVisible();
}

async verifyHelmReleaseStatus(): Promise<void> {
await expect(this.statusText).toBeVisible();
}

async verifyAllTabs(): Promise<void> {
await expect(this.detailsTab).toBeVisible();
await expect(this.resourcesTab).toBeVisible();
await expect(this.revisionHistoryTab).toBeVisible();
await expect(this.releaseNotesTab).toBeVisible();
}

async verifyActionsDropdown(): Promise<void> {
await expect(this.actionsMenuButton).toBeVisible();
}

async clickActionsMenu(): Promise<void> {
await this.robustClick(this.actionsMenuButton);
}

async verifyActionsInActionMenu(): Promise<void> {
const actions = ['Upgrade', 'Rollback', 'Delete Helm Release'];
const actionItems = this.page.locator('[data-test-id="action-items"] li');
const count = await actionItems.count();

for (let i = 0; i < count; i++) {
const text = await actionItems.nth(i).textContent();
expect(actions).toContain(text?.trim());
}
}
Comment on lines +47 to +56
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Assert the expected actions explicitly.

This only checks that every rendered item is allowed; it does not prove that all three expected actions are present. If Rollback disappears, this still passes as long as the remaining items are valid.

Suggested tightening
  async verifyActionsInActionMenu(): Promise<void> {
-   const actions = ['Upgrade', 'Rollback', 'Delete Helm Release'];
-   const actionItems = this.page.locator('[data-test-id="action-items"] li');
-   const count = await actionItems.count();
-
-   for (let i = 0; i < count; i++) {
-     const text = await actionItems.nth(i).textContent();
-     expect(actions).toContain(text?.trim());
-   }
+   const expectedActions = ['Upgrade', 'Rollback', 'Delete Helm Release'];
+   const actionItems = this.page.locator('[data-test-id="action-items"] li');
+
+   await expect(actionItems).toHaveCount(expectedActions.length);
+   for (const action of expectedActions) {
+     await expect(actionItems.filter({ hasText: action })).toHaveCount(1);
+   }
  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/pages/helm-details-page.ts` around lines 47 - 56, The test
verifyActionsInActionMenu currently only asserts that each rendered item is in
the allowed list; change it to explicitly assert the expected actions are
present and no extras: collect the text values from the locator
this.page.locator('[data-test-id="action-items"] li') (used in
verifyActionsInActionMenu), trim them, and assert that the resulting array
contains all of ['Upgrade','Rollback','Delete Helm Release'] and that its length
equals 3 (or otherwise assert exact set equality) so missing or extra items will
fail the test.


async selectAction(action: 'Upgrade' | 'Rollback' | 'Delete Helm Release'): Promise<void> {
const actionLocator = this.page
.locator('[data-test-id="action-items"] li')
.filter({ hasText: action });
await this.robustClick(actionLocator);
}

async clickRevisionHistoryTab(): Promise<void> {
await this.robustClick(this.revisionHistoryTab);
}

async verifyRevisionHistoryStatus(): Promise<void> {
await this.clickRevisionHistoryTab();
await expect(this.page.locator('[data-test="helm-revision-list"] [data-test="success-icon"]')).toBeVisible();
}

async verifyFieldValue(fieldName: string, fieldValue: string): Promise<void> {
const field = this.page
.locator('dl dt')
.filter({ hasText: fieldName })
.locator('xpath=following-sibling::dd[1]');
await expect(field).toContainText(fieldValue);
}
}
99 changes: 99 additions & 0 deletions frontend/e2e/pages/helm-install-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { expect, type Locator, type Page } from '@playwright/test';
import BasePage from './base-page';

export class HelmInstallPage extends BasePage {
private readonly pageTitle: Locator;
private readonly releaseNameInput: Locator;
private readonly formViewRadio: Locator;
private readonly yamlViewRadio: Locator;
private readonly createButton: Locator;
private readonly formSections: Locator;

constructor(page: Page) {
super(page);
this.pageTitle = this.page.getByRole('heading', { name: /Create Helm Release/i });
this.releaseNameInput = this.page.locator('[data-test="release-name"]');
this.formViewRadio = this.page.getByRole('radio', { name: 'Form view' });
this.yamlViewRadio = this.page.getByRole('radio', { name: 'YAML view' });
this.createButton = this.page.getByTestId('save-changes');
this.formSections = this.page.locator('.form-group');
}

async verifyPageDisplayed(): Promise<void> {
await expect(this.pageTitle).toBeVisible();
}

async verifyDefaultReleaseName(expectedName: string): Promise<void> {
await expect(this.releaseNameInput).toHaveValue(expectedName);
}

async verifyFormViewSelected(): Promise<void> {
await expect(this.formViewRadio).toBeChecked();
}

async verifyYamlViewEnabled(): Promise<void> {
await expect(this.yamlViewRadio).toBeEnabled();
}

async verifyFormSectionsDisplayed(): Promise<void> {
const count = await this.formSections.count();
expect(count).toBeGreaterThan(0);
}

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

async clickCreate(): Promise<void> {
await this.robustClick(this.createButton);
// Wait for navigation after creating Helm chart
await this.waitForLoadingComplete(120_000);
}

async selectYamlView(): Promise<void> {
await this.robustClick(this.yamlViewRadio);
}

/**
* Complete workflow: Install Helm chart from Software Catalog
*/
async installHelmChartFromCatalog(
chartName: string,
releaseName: string,
namespace: string,
): Promise<void> {
// Navigate to catalog
await this.goTo(`/catalog/ns/${namespace}`);
await this.waitForLoadingComplete();

// Select Helm Charts type
const helmChartsLink = this.page.getByRole('link', { name: /Helm Charts/ });
await this.robustClick(helmChartsLink);
await this.waitForLoadingComplete(60_000);

// Search and select chart
const searchInput = this.page.getByPlaceholder(/Filter by keyword/i);
await searchInput.fill(chartName);
await this.waitForLoadingComplete(60_000);

const chartCard = this.page
.locator('.odc-catalog-tile')
.filter({ hasText: chartName })
.first();
await this.robustClick(chartCard);

// Click Create on sidebar
const createOnSidebar = this.page
.locator('[role="dialog"]')
.getByRole('button', { name: /Create/i });
await this.robustClick(createOnSidebar);
await this.waitForLoadingComplete();

// Enter release name
await this.enterReleaseName(releaseName);

// Click Create
await this.clickCreate();
}
}
172 changes: 172 additions & 0 deletions frontend/e2e/pages/helm-releases-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { expect, type Locator, type Page } from '@playwright/test';
import BasePage from './base-page';

export class HelmReleasesPage extends BasePage {
private readonly emptyStateMessage: Locator;
private readonly catalogLink: Locator;
private readonly helmTable: Locator;
private readonly filterToolbar: Locator;
private readonly filterDropdown: Locator;
private readonly deployedCheckbox: Locator;
private readonly nameFilterInput: Locator;
private readonly statusIcon: Locator;
private readonly statusText: Locator;
private readonly firstKebabButton: Locator;

constructor(page: Page) {
super(page);
this.emptyStateMessage = this.page.getByRole('heading', {
name: 'No Helm Releases found',
level: 3,
});
this.catalogLink = this.page.getByRole('link', {
name: /Browse the catalog to discover available Helm Charts/i,
});
this.helmTable = this.page.locator('table').filter({ hasText: /Name.*Status/ });
this.filterToolbar = this.page.locator('[data-ouia-component-id="DataViewFilters"]');
this.filterDropdown = this.filterToolbar.locator('.pf-v6-c-menu-toggle').first();
this.deployedCheckbox = this.page.locator(
'[data-ouia-component-id="DataViewCheckboxFilter-filter-item-deployed"] input',
);
this.nameFilterInput = this.page.locator('[aria-label="Filter by name"]');
this.statusIcon = this.page.getByTestId('success-icon');
this.statusText = this.page.getByTestId('status-text');
this.firstKebabButton = this.page.locator('[data-test-id="kebab-button"]').first();
}

async navigateToHelmTab(namespace?: string): Promise<void> {
if (namespace) {
// Navigate directly to namespace-specific Helm page
await this.goTo(`/helm/ns/${namespace}`);
await this.waitForLoadingComplete();
} else {
// First go to console home
await this.goTo('/');
await this.waitForLoadingComplete();

// Navigate via sidebar: Ecosystem > Helm
const ecosystemMenu = this.page.getByRole('button', { name: /Ecosystem/ });
await this.robustClick(ecosystemMenu);

const helmLink = this.page.getByRole('link', { name: /^Helm$/ });
await this.robustClick(helmLink);
await this.waitForLoadingComplete();
}
}

async clickHelmReleasesTab(): Promise<void> {
const helmReleasesTab = this.page.locator(
'[data-test-id="horizontal-link-Helm Releases"]',
);
await this.robustClick(helmReleasesTab, { force: true });
}

async verifyEmptyState(): Promise<void> {
await expect(this.emptyStateMessage).toContainText('No Helm Releases found');
await expect(this.catalogLink).toBeVisible();
}

async searchByName(name: string): Promise<void> {
// Open filter dropdown and select Name filter
await this.robustClick(this.filterDropdown);
const nameFilterOption = this.page
.locator('.pf-v6-c-menu__list-item')
.filter({ hasText: 'Name' });
await this.robustClick(nameFilterOption);

// Enter search term
await this.nameFilterInput.clear();
await this.nameFilterInput.fill(name);
}

async verifyHelmReleasesDisplayed(): Promise<void> {
await expect(this.helmTable).toBeVisible();
}

async clickHelmReleaseName(name: string): Promise<void> {
// Scope to the visible table to avoid strict mode violations when multiple namespaces have releases with the same name
const releaseLink = this.helmTable.getByRole('link', { name, exact: true }).first();
await this.robustClick(releaseLink);
}

async selectDeployedFilter(): Promise<void> {
// Open status filter dropdown
await this.robustClick(this.filterDropdown);
const statusFilterOption = this.page
.locator('.pf-v6-c-menu__list-item')
.filter({ hasText: 'Status' });
await this.robustClick(statusFilterOption);

await this.robustClick(this.page.locator('[data-ouia-component-id="DataViewCheckboxFilter"]'));

// Check Deployed checkbox
await this.deployedCheckbox.check();
await expect(this.deployedCheckbox).toBeChecked();

// Verify URL contains filter
await expect(this.page).toHaveURL(/status=deployed/);
}

async verifyDeployedFilterChecked(): Promise<void> {
await expect(this.deployedCheckbox).toBeChecked();
}

async verifyHelmChartsListed(): Promise<void> {
await expect(this.helmTable).toBeVisible();
const rowCount = await this.helmTable.locator('tbody tr').count();
expect(rowCount).toBeGreaterThan(0);
}

async verifyHelmChartStatus(): Promise<void> {
await expect(this.statusIcon).toBeVisible();
await expect(this.statusText).toBeAttached();
}

async verifyStatusInHelmReleasesTable(helmReleaseName: string): Promise<void> {
await expect(this.helmTable).toBeAttached();
const row = this.helmTable
.locator('tr')
.filter({ hasText: helmReleaseName })
.first();
const statusButton = row.locator('td:nth-child(4) button');
await this.robustClick(statusButton);
}

async openKebabMenu(): Promise<void> {
await expect(this.helmTable).toBeAttached();
await this.robustClick(this.firstKebabButton);
}

async selectKebabAction(action: 'Upgrade' | 'Rollback' | 'Delete Helm Release'): Promise<void> {
const actionLocatorMap = {
Upgrade: '[data-test-action="Upgrade"]',
Rollback: '[data-test-action="Rollback"]',
'Delete Helm Release': '[data-test-action="Delete Helm Release"]',
};
const actionLocator = this.page.locator(actionLocatorMap[action]);
await this.robustClick(actionLocator);
}

async verifyFilterDropdownItems(
item1: string,
item2: string,
item3: string,
): Promise<void> {
await this.robustClick(this.filterDropdown);
await this.robustClick(this.page.locator('.pf-v6-c-menu__list-item').filter({ hasText: 'Status' }));
await this.robustClick(this.page.locator('[data-ouia-component-id="DataViewCheckboxFilter"]'));
const pendingInstall = this.page.locator(
'[data-ouia-component-id="DataViewCheckboxFilter-filter-item-pending-install"]',
);
const pendingUpgrade = this.page.locator(
'[data-ouia-component-id="DataViewCheckboxFilter-filter-item-pending-upgrade"]',
);
const pendingRollback = this.page.locator(
'[data-ouia-component-id="DataViewCheckboxFilter-filter-item-pending-rollback"]',
);

await expect(pendingInstall).toContainText(item1);
await expect(pendingUpgrade).toContainText(item2);
await expect(pendingRollback).toContainText(item3);
}
}
Loading