-
Notifications
You must be signed in to change notification settings - Fork 710
CONSOLE-5240: Migrate Helm Cypress E2E tests to Playwright #16542
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
vikram-raj
wants to merge
1
commit into
openshift:main
Choose a base branch
from
vikram-raj:console-5240
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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()); | ||
| } | ||
| } | ||
|
|
||
| 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); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
Rollbackdisappears, 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