diff --git a/frontend/e2e/pages/dev-console/config-map-page.ts b/frontend/e2e/pages/dev-console/config-map-page.ts new file mode 100644 index 00000000000..066148d4022 --- /dev/null +++ b/frontend/e2e/pages/dev-console/config-map-page.ts @@ -0,0 +1,62 @@ +import type { Page } from '@playwright/test'; + +import BasePage from '../base-page'; + +export class ConfigMapPage extends BasePage { + private readonly createButton = this.page.locator('[data-test="item-create"]'); + private readonly nameInput = this.page.getByTestId('configmap-name'); + private readonly initialKeyInput = this.page.getByTestId('key-0'); + private readonly secondKeyInput = this.page.getByTestId('key-1'); + private readonly valueTextarea = this.page.locator('[data-test-id="file-input-textarea"]'); + private readonly addKeyValueButton = this.page.getByTestId('add-key-value-button'); + private readonly submitButton = this.page.locator('[data-test-id="submit-button"]'); + private readonly dataSectionBody = this.page.locator('.co-m-pane__body'); + + constructor(page: Page) { + super(page); + } + + async clickCreate(): Promise { + await this.robustClick(this.createButton); + } + + async fillName(name: string): Promise { + await this.nameInput.fill(name); + } + + async fillKey(key: string): Promise { + await this.initialKeyInput.scrollIntoViewIfNeeded(); + await this.initialKeyInput.fill(key); + } + + async fillValue(value: string): Promise { + await this.valueTextarea.scrollIntoViewIfNeeded(); + await this.valueTextarea.fill(value); + } + + async submitForm(): Promise { + await this.robustClick(this.submitButton); + } + + async addKeyValue(): Promise { + await this.robustClick(this.addKeyValueButton.first()); + } + + async fillSecondKey(key: string): Promise { + await this.secondKeyInput.scrollIntoViewIfNeeded(); + await this.secondKeyInput.fill(key); + } + + async createConfigMap(name: string, key = 'test-key', value = 'test-value'): Promise { + await this.clickCreate(); + await this.fillName(name); + await this.fillKey(key); + await this.fillValue(value); + await this.submitForm(); + } + + async expectDataSectionToContain(text: string): Promise { + const { expect } = await import('@playwright/test'); + await expect(this.dataSectionBody).toContainText(text); + } +} diff --git a/frontend/e2e/pages/dev-console/customization-page.ts b/frontend/e2e/pages/dev-console/customization-page.ts new file mode 100644 index 00000000000..b200445ec68 --- /dev/null +++ b/frontend/e2e/pages/dev-console/customization-page.ts @@ -0,0 +1,113 @@ +import type { Page } from '@playwright/test'; +import { expect } from '@playwright/test'; + +import BasePage from '../base-page'; + +export class CustomizationPage extends BasePage { + private readonly successAlert = this.page.locator('[aria-label="Success Alert"]'); + private readonly clusterLink = this.page.getByTestId('cluster'); + private readonly customizeAction = this.page.locator('[data-test-action="Customize"]'); + private readonly actionsMenuButton = this.page.locator('[data-test-id="actions-menu-button"]'); + + constructor(page: Page) { + super(page); + } + + async navigateToConsoles(): Promise { + await this.goTo('/search?kind=console.operator.openshift.io~v1~Console'); + await this.waitForLoadingComplete(); + } + + async clickCluster(): Promise { + await this.robustClick(this.clusterLink); + } + + async openCustomization(): Promise { + await this.robustClick(this.actionsMenuButton); + await this.robustClick(this.customizeAction); + await this.waitForLoadingComplete(); + } + + async clickDeveloperTab(): Promise { + const tab = this.page.locator('[role="presentation"]').filter({ hasText: 'Developer' }); + await this.robustClick(tab); + } + + async disableAllSoftwareCatalogItems(): Promise { + const formSection = this.page.getByTestId('catalog-types form-section'); + const removeAll = formSection.locator('[aria-label="Remove all"]'); + await removeAll.scrollIntoViewIfNeeded(); + if (await removeAll.isEnabled()) { + await removeAll.click(); + } + const addAll = formSection.locator('[aria-label="Add all"]'); + await addAll.scrollIntoViewIfNeeded(); + await addAll.click(); + } + + async disableSoftwareCatalogItem(itemName: string): Promise { + const formSection = this.page.getByTestId('catalog-types form-section'); + await formSection.locator('[aria-label="Remove all"]').click(); + await formSection.locator('[aria-label="Available search input"]').fill(itemName); + const option = formSection.locator('[role="option"]').filter({ hasText: itemName }); + await option.scrollIntoViewIfNeeded(); + await option.click(); + await formSection.locator('[aria-label="Add selected"]').scrollIntoViewIfNeeded(); + await formSection.locator('[aria-label="Add selected"]').click(); + } + + async enableOnlySoftwareCatalogItem(itemName: string): Promise { + const formSection = this.page.getByTestId('catalog-types form-section'); + await formSection.locator('[aria-label="Add all"]').click(); + await formSection.locator('[aria-label="Chosen search input"]').fill(itemName); + const option = formSection.locator('[role="option"]').filter({ hasText: itemName }); + await option.click(); + await formSection.locator('[aria-label="Remove selected"]').click(); + } + + async disableAllAddPageItems(): Promise { + const formSection = this.page.getByTestId('add-page form-section'); + await formSection.locator('[aria-label="Add all"]').scrollIntoViewIfNeeded(); + await formSection.locator('[aria-label="Add all"]').click(); + } + + async disableAddPageItem(itemName: string): Promise { + const formSection = this.page.getByTestId('add-page form-section'); + await formSection.locator('[aria-label="Remove all"]').click(); + await formSection.locator('[aria-label="Available search input"]').fill(itemName); + const option = formSection.locator('[role="option"]').filter({ hasText: itemName }); + await option.scrollIntoViewIfNeeded(); + await option.click(); + await formSection.locator('[aria-label="Add selected"]').scrollIntoViewIfNeeded(); + await formSection.locator('[aria-label="Add selected"]').click(); + } + + async expectSaveMessage(): Promise { + await this.successAlert.scrollIntoViewIfNeeded(); + await expect(this.successAlert).toBeVisible(); + } + + async expectPinnedResourceSection(): Promise { + await expect(this.page.getByTestId('pinned-resource form-section')).toBeVisible(); + } + + async expectPerspectiveDropdownValue(value: string): Promise { + const perspectiveGroup = this.page.locator('[data-test="perspectives form-group"]').nth(1); + await expect(perspectiveGroup.locator('button[class*="menu-toggle"]')).toContainText(value); + } + + async selectPerspectiveState(state: string): Promise { + const perspectiveGroup = this.page.locator('[data-test="perspectives form-group"]').nth(1); + const toggle = perspectiveGroup + .locator('button') + .filter({ hasText: /Disabled|Enabled|AccessReview/ }); + await this.robustClick(toggle); + await expect(this.page.locator('[role="listbox"]')).toBeVisible(); + const option = this.page.locator('button[role="option"]').filter({ hasText: state }); + await this.robustClick(option); + } + + async expectSuccessAlert(): Promise { + await expect(this.page.getByTestId('success-alert')).toBeVisible(); + } +} diff --git a/frontend/e2e/pages/dev-console/health-checks-page.ts b/frontend/e2e/pages/dev-console/health-checks-page.ts new file mode 100644 index 00000000000..3bcd2813a80 --- /dev/null +++ b/frontend/e2e/pages/dev-console/health-checks-page.ts @@ -0,0 +1,69 @@ +import type { Page } from '@playwright/test'; +import { expect } from '@playwright/test'; + +import BasePage from '../base-page'; + +export class HealthChecksPage extends BasePage { + private readonly healthChecksForm = this.page.locator('div.odc-heath-check-probe-form'); + private readonly successText = this.page.locator('span.odc-heath-check-probe__successText'); + private readonly checkIcon = this.page.locator('[data-test-id="check-icon"]'); + private readonly addButton = this.page.locator('[data-test-id="submit-button"]'); + private readonly saveButton = this.page.locator('[data-test-id="submit-button"]'); + private readonly typeToggle = this.page.getByTestId('console-select-menu-toggle'); + + constructor(page: Page) { + super(page); + } + + async clickAddProbe(probeName: string): Promise { + const probeButton = this.page.getByRole('button', { name: probeName }); + await probeButton.scrollIntoViewIfNeeded(); + await this.robustClick(probeButton); + await expect(this.healthChecksForm).toBeVisible(); + } + + async selectProbeType(type: string): Promise { + await this.robustClick(this.typeToggle); + const option = this.page.getByTestId('console-select-item').filter({ hasText: type }); + await this.robustClick(option); + + if (type === 'Container command') { + const argInput = this.page.locator('[placeholder="argument"]'); + if ((await argInput.count()) > 0) { + await argInput.fill('example'); + } + } + } + + async clickCheckIcon(): Promise { + await this.robustClick(this.checkIcon); + } + + async clickAddButton(): Promise { + await this.robustClick(this.addButton); + } + + async clickSaveButton(): Promise { + await this.robustClick(this.saveButton); + } + + async removeProbe(probeName: string): Promise { + const probeSuccess = this.page.getByRole('button', { name: probeName }); + const removeIcon = probeSuccess.locator('..').locator('..').locator('[role="img"]'); + await this.robustClick(removeIcon); + } + + async expectProbesAdded(count: number): Promise { + await expect(this.successText).toHaveCount(count); + } + + async expectProbeAdded(probeName: string): Promise { + await expect(this.successText.filter({ hasText: probeName })).toBeVisible(); + } + + async expectEditHealthChecksTitle(): Promise { + await expect(this.page.locator('[data-test="page-heading"] h1')).toContainText( + 'Edit health checks', + ); + } +} diff --git a/frontend/e2e/pages/dev-console/perspective-page.ts b/frontend/e2e/pages/dev-console/perspective-page.ts new file mode 100644 index 00000000000..de8c4f34ba1 --- /dev/null +++ b/frontend/e2e/pages/dev-console/perspective-page.ts @@ -0,0 +1,103 @@ +import type { Page } from '@playwright/test'; + +import BasePage from '../base-page'; + +export class PerspectivePage extends BasePage { + private readonly perspectiveSwitcherToggle = this.page.locator( + '[data-test-id="perspective-switcher-toggle"]', + ); + private readonly perspectiveSwitcherMenu = this.page.locator( + '[data-test-id="perspective-switcher-menu"]', + ); + private readonly sidebar = this.page.locator('#page-sidebar'); + private readonly projectDropdown = this.page.locator('[data-test-id="namespace-bar-dropdown"]'); + private readonly projectFilterInput = this.page.locator( + '[data-test="namespace-dropdown-filter"]', + ); + private readonly createProjectButton = this.page.locator( + '[data-test="namespace-dropdown-create-project"]', + ); + + constructor(page: Page) { + super(page); + } + + private async switchToPerspective(perspectiveName: string): Promise { + await this.perspectiveSwitcherToggle.waitFor({ state: 'visible', timeout: 60_000 }); + const currentText = await this.perspectiveSwitcherToggle.textContent(); + if (currentText?.includes(perspectiveName)) { + return; + } + await this.robustClick(this.perspectiveSwitcherToggle); + const option = this.perspectiveSwitcherMenu + .locator('[data-test-id="perspective-switcher-menu-option"]') + .filter({ hasText: perspectiveName }); + await this.robustClick(option); + await this.waitForLoadingComplete(); + } + + async switchToDeveloper(): Promise { + await this.switchToPerspective('Developer'); + } + + async switchToAdministrator(): Promise { + await this.switchToPerspective('Administrator'); + // Also handle "Core platform" label (OpenShift 5.0+) + const currentText = await this.perspectiveSwitcherToggle.textContent(); + if ( + !currentText?.includes('Administrator') && + !currentText?.includes('Core platform') + ) { + await this.switchToPerspective('Core platform'); + } + } + + async selectOrCreateProject(name: string): Promise { + await this.robustClick(this.projectDropdown); + await this.projectFilterInput.fill(name); + + const projectLink = this.page.locator(`[id="${name}-link"]`); + if ((await projectLink.count()) > 0) { + await this.robustClick(projectLink); + } else { + await this.robustClick(this.createProjectButton); + await this.page.locator('#input-name').fill(name); + await this.robustClick(this.page.locator('[data-test="confirm-action"]')); + } + await this.waitForLoadingComplete(); + } + + async navigateToDevMenu(menuItem: string): Promise { + const menuLink = this.sidebar.locator(`[data-test-id="${menuItem}-header"]`); + await this.robustClick(menuLink); + await this.waitForLoadingComplete(); + } + + async navigateToAdd(): Promise { + await this.navigateToDevMenu('+Add'); + } + + async navigateToTopology(): Promise { + await this.navigateToDevMenu('topology'); + } + + async navigateToSearch(): Promise { + await this.navigateToDevMenu('search'); + } + + async navigateToProject(): Promise { + await this.navigateToDevMenu('project-details'); + } + + async navigateToRoutes(): Promise { + const routesLink = this.sidebar.locator('a[href*="Route"]'); + await this.robustClick(routesLink); + await this.waitForLoadingComplete(); + } + + async navigateToConfigMaps(): Promise { + const configMapsLink = this.sidebar.locator('a[href*="ConfigMap"]'); + await this.robustClick(configMapsLink); + await this.waitForLoadingComplete(); + } +} diff --git a/frontend/e2e/pages/dev-console/pod-list-page.ts b/frontend/e2e/pages/dev-console/pod-list-page.ts new file mode 100644 index 00000000000..0008a49037f --- /dev/null +++ b/frontend/e2e/pages/dev-console/pod-list-page.ts @@ -0,0 +1,29 @@ +import type { Page } from '@playwright/test'; +import { expect } from '@playwright/test'; + +import BasePage from '../base-page'; + +export class PodListPage extends BasePage { + private readonly manageColumnsButton = this.page.locator('button[data-test="manage-columns"]'); + private readonly createdColumnCheckbox = this.page.locator('input[id="created"]'); + private readonly receivingTrafficCheckbox = this.page.locator('input[id="trafficStatus"]'); + private readonly confirmActionButton = this.page.locator('button[data-test="confirm-action"]'); + private readonly receivingTrafficColumnLabel = this.page.locator( + '[data-label="Receiving Traffic"]', + ); + + constructor(page: Page) { + super(page); + } + + async enableReceivingTrafficColumn(): Promise { + await this.robustClick(this.manageColumnsButton); + await this.createdColumnCheckbox.uncheck(); + await this.receivingTrafficCheckbox.check(); + await this.robustClick(this.confirmActionButton); + } + + async expectReceivingTrafficColumnVisible(): Promise { + await expect(this.receivingTrafficColumnLabel).toBeVisible(); + } +} diff --git a/frontend/e2e/pages/dev-console/route-page.ts b/frontend/e2e/pages/dev-console/route-page.ts new file mode 100644 index 00000000000..f5dbe2bf6bb --- /dev/null +++ b/frontend/e2e/pages/dev-console/route-page.ts @@ -0,0 +1,69 @@ +import type { Page } from '@playwright/test'; +import { expect } from '@playwright/test'; + +import BasePage from '../base-page'; + +export class RoutePage extends BasePage { + private readonly createButton = this.page.locator('[data-test="item-create"]'); + private readonly nameInput = this.page.locator('#name'); + private readonly hostnameInput = this.page.locator('#hostname'); + private readonly serviceDropdown = this.page.locator('#service'); + private readonly targetPortDropdown = this.page.locator('#target-port'); + private readonly submitButton = this.page.locator('[data-test-id="submit-button"]'); + private readonly breadcrumb = this.page.locator('[aria-label="Breadcrumb"]'); + + constructor(page: Page) { + super(page); + } + + async clickCreateRoute(): Promise { + await this.robustClick(this.createButton); + } + + async fillName(name: string): Promise { + await this.nameInput.fill(name); + } + + async fillHostname(hostname: string): Promise { + await this.hostnameInput.scrollIntoViewIfNeeded(); + await this.hostnameInput.fill(hostname); + } + + async selectService(serviceName: string): Promise { + await this.serviceDropdown.scrollIntoViewIfNeeded(); + await this.robustClick(this.serviceDropdown); + await this.robustClick(this.page.locator(`[data-test-dropdown-menu="${serviceName}"]`)); + } + + async selectTargetPort(targetPort: string): Promise { + await this.targetPortDropdown.scrollIntoViewIfNeeded(); + await this.robustClick(this.targetPortDropdown); + const port = targetPort.substring(0, 4); + await this.robustClick(this.page.locator(`[data-test-dropdown-menu="${port}-tcp"]`)); + } + + async submitForm(): Promise { + await this.robustClick(this.submitButton); + } + + async createRoute( + name: string, + service: string, + targetPort: string, + hostname?: string, + ): Promise { + await this.clickCreateRoute(); + await this.fillName(name); + if (hostname) { + await this.fillHostname(hostname); + } + await this.selectService(service); + await this.selectTargetPort(targetPort); + await this.submitForm(); + await expect(this.breadcrumb).toContainText('Routes'); + } + + async expectBreadcrumbToContainRoutes(): Promise { + await expect(this.breadcrumb).toContainText('Routes'); + } +} diff --git a/frontend/e2e/pages/dev-console/samples-page.ts b/frontend/e2e/pages/dev-console/samples-page.ts new file mode 100644 index 00000000000..66b52babbef --- /dev/null +++ b/frontend/e2e/pages/dev-console/samples-page.ts @@ -0,0 +1,109 @@ +import type { Page } from '@playwright/test'; +import { expect } from '@playwright/test'; + +import BasePage from '../base-page'; + +export class SamplesPage extends BasePage { + private readonly pageHeading = this.page.locator('[data-test="page-heading"] h1'); + private readonly viewAllSamplesLink = this.page.getByTestId('view-all-samples'); + private readonly samplesCard = this.page.getByTestId('card Samples'); + private readonly nameInput = this.page.locator('[data-test-id="application-form-app-name"]'); + private readonly gitUrlInput = this.page.locator('[data-test-id="git-form-input-url"]'); + private readonly builderImageVersionToggle = this.page.getByTestId('console-select-menu-toggle'); + private readonly submitButton = this.page.locator('[data-test-id="submit-button"]'); + private readonly cancelButton = this.page.locator('[data-test-id="reset-button"]'); + private readonly devfileName = this.page.locator( + '[data-test-id="import-devfile"] #form-input-name-field', + ); + + constructor(page: Page) { + super(page); + } + + async clickViewAllSamples(): Promise { + await this.robustClick(this.viewAllSamplesLink); + } + + async expectSamplesPageHeading(): Promise { + await expect(this.pageHeading).toContainText('Sample'); + } + + async clickSamplesCard(): Promise { + await this.robustClick(this.samplesCard); + } + + async searchAndSelectSample(sampleName: string): Promise { + const searchInput = this.page.locator('[data-test="search-catalog"]'); + await searchInput.fill(sampleName); + const card = this.page.locator(`[data-test^="${sampleName}"]`).first(); + await this.robustClick(card); + } + + async selectSampleCard(sampleName: string): Promise { + const card = this.page.locator(`[data-test="${sampleName}"]`).first(); + if ((await card.count()) === 0) { + const altCard = this.page.locator(`[data-test*="${sampleName}"]`).first(); + await this.robustClick(altCard); + } else { + await this.robustClick(card); + } + } + + async expectFormHeader(headerText: string): Promise { + await expect(this.pageHeading).toContainText(headerText); + } + + async clickCreate(): Promise { + await this.robustClick(this.submitButton); + } + + async expectNameSectionVisible(): Promise { + await expect(this.nameInput).toBeVisible(); + } + + async expectBuilderImageVersionDropdownVisible(): Promise { + await expect(this.builderImageVersionToggle).toBeVisible(); + } + + async expectBuilderImageVisible(): Promise { + await expect(this.page.locator('img[alt="Icon"]')).toBeVisible(); + } + + async expectGitUrlReadonly(): Promise { + await expect(this.gitUrlInput).toBeVisible(); + } + + async expectCreateAndCancelButtons(): Promise { + await expect(this.submitButton).toBeVisible(); + await expect(this.cancelButton).toBeVisible(); + } + + async fillName(name: string): Promise { + await this.nameInput.clear(); + await this.nameInput.fill(name); + } + + async changeBuilderImageVersion(version: string): Promise { + await this.robustClick(this.builderImageVersionToggle); + await this.robustClick( + this.page.getByTestId('console-select-item').filter({ hasText: version }), + ); + } + + async fillDevfileName(name: string): Promise { + await this.waitForLoadingComplete(); + await this.devfileName.waitFor({ state: 'visible', timeout: 30_000 }); + await this.devfileName.clear(); + await this.devfileName.fill(name); + } + + async expectSampleApplicationsVisible(): Promise { + await expect(this.page.locator('[data-test*="Devfile"]').first()).toBeVisible(); + await expect(this.page.locator('[data-test*="BuilderImage"]').first()).toBeVisible(); + } + + async expectBuilderImageBasedSamples(): Promise { + const count = await this.page.locator('[data-test^="BuilderImage"]').count(); + expect(count).toBeGreaterThanOrEqual(1); + } +} diff --git a/frontend/e2e/pages/dev-console/search-page.ts b/frontend/e2e/pages/dev-console/search-page.ts new file mode 100644 index 00000000000..0f529c86a70 --- /dev/null +++ b/frontend/e2e/pages/dev-console/search-page.ts @@ -0,0 +1,48 @@ +import type { Page } from '@playwright/test'; +import { expect } from '@playwright/test'; + +import BasePage from '../base-page'; + +export class SearchPage extends BasePage { + private readonly resourceFilterInput = this.page.locator('input[placeholder="Resources"]'); + private readonly resourceOptionsMenu = this.page.locator('[aria-label="Options menu"]'); + private readonly recentlyUsedSection = this.page.locator('[aria-labelledby="Recently-used"]'); + private readonly clearHistoryButton = this.page.locator('[data-test-id="close-icon"]'); + + constructor(page: Page) { + super(page); + } + + async searchAndSelectResource(resourceName: string): Promise { + await this.robustClick(this.page.locator('[aria-label="Type to filter"]')); + await this.resourceFilterInput.clear(); + await this.resourceFilterInput.fill(resourceName); + await this.robustClick(this.page.locator(`label[id$="${resourceName}"]`)); + } + + async openResourceFilter(): Promise { + await expect(this.resourceOptionsMenu).toBeVisible(); + await this.robustClick(this.resourceOptionsMenu); + } + + async expectRecentlyUsedToContain(resourceName: string): Promise { + await expect( + this.recentlyUsedSection.locator(`[data-filter-text="AR${resourceName}"]`), + ).toBeVisible(); + } + + async expectRecentlyUsedToContainAll(resources: string[]): Promise { + for (const resource of resources) { + await expect(this.recentlyUsedSection.locator('label')).toContainText(resource); + } + } + + async clearHistory(): Promise { + await expect(this.clearHistoryButton).toBeVisible(); + await this.robustClick(this.clearHistoryButton, { force: true }); + } + + async expectRecentlyUsedNotVisible(): Promise { + await expect(this.recentlyUsedSection).not.toBeAttached(); + } +} diff --git a/frontend/e2e/pages/dev-console/user-preferences-page.ts b/frontend/e2e/pages/dev-console/user-preferences-page.ts new file mode 100644 index 00000000000..fdfebd52578 --- /dev/null +++ b/frontend/e2e/pages/dev-console/user-preferences-page.ts @@ -0,0 +1,89 @@ +import type { Page } from '@playwright/test'; +import { expect } from '@playwright/test'; + +import BasePage from '../base-page'; + +export class UserPreferencesPage extends BasePage { + private readonly userMenuToggle = this.page.locator('[data-test="user-dropdown-toggle"]'); + private readonly perspectiveSwitcherToggle = this.page.locator( + '[data-test-id="perspective-switcher-toggle"]', + ); + + constructor(page: Page) { + super(page); + } + + async openUserPreferences(): Promise { + await this.robustClick(this.userMenuToggle); + const menuItem = this.page.locator('[role="menu"] li').filter({ hasText: 'User Preferences' }); + await this.robustClick(menuItem); + await this.waitForLoadingComplete(); + } + + async expectTabVisible(tabName: string): Promise { + const tab = this.page.locator(`[data-test~="tab"][data-test~="${tabName.toLowerCase()}"]`); + await expect(tab).toBeVisible(); + } + + async clickTab(tabName: string): Promise { + const tab = this.page.locator(`[data-test~="tab"][data-test~="${tabName.toLowerCase()}"]`); + await this.robustClick(tab); + } + + private getPreferenceDropdownId(preference: string): string { + switch (preference) { + case 'Perspective': + return 'console.preferredPerspective'; + case 'Project': + return 'console.preferredNamespace'; + case 'Topology': + return 'topology.preferredView'; + case 'Create/Edit resource method': + return 'console.preferredCreateEditMethod'; + case 'Language': + return 'console.preferredLanguage'; + case 'Resource Type': + return 'devconsole.preferredResource'; + default: + throw new Error(`Unknown preference: ${preference}`); + } + } + + async changePreferenceDropdown(preference: string, value: string): Promise { + const dropdownId = this.getPreferenceDropdownId(preference); + const dropdown = this.page.locator(`[id="${dropdownId}"]`); + await this.robustClick(dropdown); + const option = this.page + .locator('[role="option"], [role="menuitem"]') + .filter({ hasText: value }); + await this.robustClick(option); + } + + async reloadConsole(): Promise { + await this.page.reload(); + await this.waitForLoadingComplete(); + } + + async expectPerspective(perspectiveName: string): Promise { + await expect(this.perspectiveSwitcherToggle).toContainText(perspectiveName); + } + + async expectTopologyGraphView(): Promise { + await expect(this.page.locator('[data-id="odc-topology-graph"]')).toBeVisible(); + } + + async expectTopologyListView(): Promise { + await expect(this.page.locator('[aria-label="Topology List View"]')).toBeVisible(); + } + + async expectYamlViewSelected(): Promise { + await expect(this.page.locator('#form-radiobutton-editorType-yaml-field')).toBeChecked(); + } + + async expectResourceTypeSelected(resourceType: string): Promise { + const dropdown = this.page.locator('[data-test-id="dropdown-button"]').filter({ + hasText: resourceType, + }); + await expect(dropdown.first()).toBeVisible(); + } +} diff --git a/frontend/e2e/pages/dev-console/vulnerability-page.ts b/frontend/e2e/pages/dev-console/vulnerability-page.ts new file mode 100644 index 00000000000..124ed9956b3 --- /dev/null +++ b/frontend/e2e/pages/dev-console/vulnerability-page.ts @@ -0,0 +1,129 @@ +import type { Page } from '@playwright/test'; +import { expect } from '@playwright/test'; + +import BasePage from '../base-page'; + +export class VulnerabilityPage extends BasePage { + private readonly vulnerabilityTab = this.page.locator( + '[data-test-id="horizontal-link-Vulnerabilities"]', + ); + private readonly vulnerabilityTable = this.page.locator( + '[aria-label="Image Manifest Vulnerabilities"]', + ); + private readonly filterDropdownButton = this.page.getByTestId('console-select-menu-toggle'); + private readonly imageVulnerabilityAlert = this.page.getByTestId('Image Vulnerabilities'); + private readonly imageVulnerabilityPopup = this.page.getByTestId('vul popup'); + private readonly viewAllLink = this.page.getByTestId('view-all'); + private readonly firstVulnerabilityLink = this.page.getByTestId('vuln-0'); + private readonly vulnerabilityTypeGroup = this.page.locator('[aria-label="Vulnerability type"]'); + private readonly detailsTab = this.page.locator('[data-test-id="horizontal-link-Details"]'); + private readonly yamlTab = this.page.locator('[data-test-id="horizontal-link-YAML"]'); + private readonly affectedPodsTab = this.page.locator( + '[data-test-id="horizontal-link-Affected Pods"]', + ); + private readonly filterDropdownToggle = this.page.locator( + '[data-test-id="filter-dropdown-toggle"]', + ); + private readonly severitySection = this.page.locator('[aria-labelledby="Severity"]'); + private readonly sectionHeading = this.page.locator( + '[data-test-section-heading="Image Manifest Vulnerabilities details"]', + ); + + constructor(page: Page) { + super(page); + } + + async clickVulnerabilityTab(): Promise { + await expect(this.vulnerabilityTab).toBeVisible(); + await this.robustClick(this.vulnerabilityTab); + } + + async expectVulnerabilityTableVisible(): Promise { + await expect(this.vulnerabilityTable.locator('[data-id="0-0"]')).toBeVisible(); + } + + async expectFilterSelected(filterName: string): Promise { + await expect(this.filterDropdownButton).toHaveText(filterName); + } + + async expectTableHeaders(): Promise { + const headers = ['Image name', 'Highest severity', 'Affected Pods', 'Fixable', 'Total']; + for (const header of headers) { + await expect(this.vulnerabilityTable).toContainText(header); + } + } + + async clickImageVulnerabilityAlert(): Promise { + await expect(this.imageVulnerabilityAlert).toBeVisible(); + await this.robustClick(this.imageVulnerabilityAlert); + await expect(this.imageVulnerabilityPopup).toBeVisible(); + } + + async clickViewAllLink(): Promise { + await expect(this.viewAllLink).toBeVisible(); + await this.robustClick(this.viewAllLink, { force: true }); + } + + async clickFilterDropdown(): Promise { + await expect(this.filterDropdownButton).toBeVisible(); + await this.robustClick(this.filterDropdownButton); + } + + async selectFilterOption(option: string): Promise { + await this.robustClick(this.page.locator(`[data-test-dropdown-menu="${option}"]`)); + } + + async clickFirstVulnerabilityLink(): Promise { + await expect(this.firstVulnerabilityLink).toBeVisible(); + await this.robustClick(this.firstVulnerabilityLink, { force: true }); + } + + async clickVulnerabilityType(type: string): Promise { + await expect(this.vulnerabilityTypeGroup).toBeVisible(); + await this.robustClick(this.page.locator(`[id="${type}"]`), { force: true }); + } + + async expectDetailsTabs(): Promise { + await expect(this.detailsTab).toBeVisible(); + await expect(this.yamlTab).toBeVisible(); + await expect(this.affectedPodsTab).toBeVisible(); + } + + async expectVulnerabilityTypeSelected(type: string): Promise { + const tab = this.page.locator(`[id="${type}"]`); + await expect(tab).toHaveAttribute('aria-pressed', 'true'); + } + + async navigateToVulnerabilityDetailsPage(imageName: string): Promise { + await this.clickVulnerabilityTab(); + await this.robustClick(this.page.locator(`[data-test-id="${imageName}"]`)); + await expect(this.sectionHeading).toBeVisible(); + } + + async openFilterDropdownInVulnerabilities(): Promise { + await this.filterDropdownToggle.scrollIntoViewIfNeeded(); + await expect(this.filterDropdownToggle).toBeVisible(); + await this.robustClick(this.filterDropdownToggle); + } + + async clickTypeFilter(filterType: string): Promise { + const checkbox = this.page.locator( + `[data-test-row-filter="${filterType}"] input[id="${filterType}"]`, + ); + await expect(checkbox).toBeVisible(); + await checkbox.click(); + } + + async expectTypeFilterChecked(filterType: string): Promise { + const checkbox = this.page.locator( + `[data-test-row-filter="${filterType}"] input[id="${filterType}"]`, + ); + await expect(checkbox).toBeChecked(); + } + + async expectSeverityFilters(severities: string[]): Promise { + for (const severity of severities) { + await expect(this.severitySection).toContainText(severity); + } + } +} diff --git a/frontend/e2e/tests/dev-console/add-health-checks-git-form.spec.ts b/frontend/e2e/tests/dev-console/add-health-checks-git-form.spec.ts new file mode 100644 index 00000000000..70f7634dd8c --- /dev/null +++ b/frontend/e2e/tests/dev-console/add-health-checks-git-form.spec.ts @@ -0,0 +1,7 @@ +import { test } from '../../fixtures'; + +test.describe('Health Checks in git import form', { tag: ['@smoke', '@dev-console'] }, () => { + test('health checks option in advanced options', async () => { + test.skip(true, 'Marked @to-do in Cypress — covered by A-04-TC12'); + }); +}); diff --git a/frontend/e2e/tests/dev-console/add-health-checks-topology-sidebar.spec.ts b/frontend/e2e/tests/dev-console/add-health-checks-topology-sidebar.spec.ts new file mode 100644 index 00000000000..3f3d7314891 --- /dev/null +++ b/frontend/e2e/tests/dev-console/add-health-checks-topology-sidebar.spec.ts @@ -0,0 +1,179 @@ +import { test, expect } from '../../fixtures'; +import KubernetesClient from '../../clients/kubernetes-client'; +import { PerspectivePage } from '../../pages/dev-console/perspective-page'; +import { HealthChecksPage } from '../../pages/dev-console/health-checks-page'; + +test.describe('Perform Health Checks related Actions', { tag: ['@smoke', '@dev-console'] }, () => { + const namespace = `aut-monitoring-sidebar-${Date.now()}`; + let k8s: KubernetesClient; + + test.beforeAll(async () => { + k8s = new KubernetesClient({ + clusterUrl: process.env.CLUSTER_URL || '', + username: process.env.OPENSHIFT_USERNAME || 'kubeadmin', + password: process.env.BRIDGE_KUBEADMIN_PASSWORD || '', + }); + await k8s.createNamespace(namespace); + + const deployments = ['health-checks-d', 'http-d', 'tcp-d', 'command-d']; + + for (const name of deployments) { + await k8s.appsV1Api.createNamespacedDeployment({ + namespace, + body: { + apiVersion: 'apps/v1', + kind: 'Deployment', + metadata: { name, namespace }, + spec: { + replicas: 1, + selector: { matchLabels: { app: name } }, + template: { + metadata: { labels: { app: name } }, + spec: { + containers: [ + { + name, + image: 'registry.access.redhat.com/ubi8/nodejs-18:latest', + ports: [{ containerPort: 8080, protocol: 'TCP' }], + }, + ], + }, + }, + }, + }, + }); + } + }); + + test.afterAll(async () => { + await k8s.deleteNamespace(namespace); + }); + + test('add health checks page', async ({ page }) => { + const perspectivePage = new PerspectivePage(page); + + await test.step('Navigate to topology', async () => { + await page.goto('/'); + await perspectivePage.switchToDeveloper(); + await perspectivePage.selectOrCreateProject(namespace); + await perspectivePage.navigateToTopology(); + }); + + await test.step('Open workload sidebar and select Add Health Checks', async () => { + const searchInput = page.locator('[data-test-id="item-filter"]'); + await searchInput.fill('health-checks-d'); + const node = page.locator('[data-test-id="health-checks-d"]').first(); + await node.click(); + const actionsDropdown = page.locator('[data-test-id="actions-menu-button"]'); + await actionsDropdown.click(); + await page.locator('[data-test-action="Add Health Checks"]').click(); + }); + + await test.step('Verify health checks page', async () => { + await expect(page.locator('[data-test="page-heading"] h1')).toContainText('health checks', { + ignoreCase: true, + }); + }); + }); + + for (const { workloadName, probeType } of [ + { workloadName: 'http-d', probeType: 'HTTP GET' }, + { workloadName: 'tcp-d', probeType: 'TCP socket' }, + { workloadName: 'command-d', probeType: 'Container command' }, + ]) { + test(`add all 3 health checks to deployment: ${probeType}`, async ({ page }) => { + const perspectivePage = new PerspectivePage(page); + const healthChecksPage = new HealthChecksPage(page); + + await test.step('Navigate to topology', async () => { + await page.goto('/'); + await perspectivePage.switchToDeveloper(); + await perspectivePage.selectOrCreateProject(namespace); + await perspectivePage.navigateToTopology(); + }); + + await test.step('Open workload sidebar', async () => { + const searchInput = page.locator('[data-test-id="item-filter"]'); + await searchInput.fill(workloadName); + const node = page.locator(`[data-test-id="${workloadName}"]`).first(); + await node.click(); + }); + + await test.step('Select Add Health Checks from actions', async () => { + const actionsDropdown = page.locator('[data-test-id="actions-menu-button"]'); + await actionsDropdown.click(); + await page.locator('[data-test-action="Add Health Checks"]').click(); + }); + + await test.step('Add readiness probe', async () => { + await healthChecksPage.clickAddProbe('Add Readiness Probe'); + await healthChecksPage.selectProbeType(probeType); + await healthChecksPage.clickCheckIcon(); + }); + + await test.step('Add liveness probe', async () => { + await healthChecksPage.clickAddProbe('Add Liveness Probe'); + await healthChecksPage.selectProbeType(probeType); + await healthChecksPage.clickCheckIcon(); + }); + + await test.step('Add startup probe', async () => { + await healthChecksPage.clickAddProbe('Add Startup Probe'); + await healthChecksPage.selectProbeType(probeType); + await healthChecksPage.clickCheckIcon(); + }); + + await test.step('Save and verify', async () => { + await healthChecksPage.clickAddButton(); + await page.waitForURL(/\/topology\//); + }); + + await test.step('Verify all 3 probes added', async () => { + await page.goto( + `/k8s/ns/${namespace}/deployments/${workloadName}/containers/${workloadName}/health-checks`, + ); + await healthChecksPage.expectEditHealthChecksTitle(); + await healthChecksPage.expectProbesAdded(3); + }); + }); + } + + test('add health check from context menu', { tag: ['@regression'] }, async ({ page }) => { + const perspectivePage = new PerspectivePage(page); + const healthChecksPage = new HealthChecksPage(page); + + await test.step('Navigate to topology', async () => { + await page.goto('/'); + await perspectivePage.switchToDeveloper(); + await perspectivePage.selectOrCreateProject(namespace); + await perspectivePage.navigateToTopology(); + }); + + await test.step('Right-click workload and select Add Health Checks', async () => { + const searchInput = page.locator('[data-test-id="item-filter"]'); + await searchInput.fill('health-checks-d'); + const node = page.locator('[data-test-id="health-checks-d"]').first(); + await node.click({ button: 'right' }); + await page.locator('[data-test-action="Add Health Checks"]').click(); + }); + + await test.step('Add readiness probe', async () => { + await healthChecksPage.clickAddProbe('Add Readiness Probe'); + await healthChecksPage.selectProbeType('HTTP GET'); + await healthChecksPage.clickCheckIcon(); + }); + + await test.step('Save and verify', async () => { + await healthChecksPage.clickAddButton(); + await page.waitForURL(/\/topology\//); + }); + + await test.step('Verify readiness probe added', async () => { + await page.goto( + `/k8s/ns/${namespace}/deployments/health-checks-d/containers/health-checks-d/health-checks`, + ); + await healthChecksPage.expectEditHealthChecksTitle(); + await healthChecksPage.expectProbeAdded('Readiness probe added'); + }); + }); +}); diff --git a/frontend/e2e/tests/dev-console/config-maps-dev-perspective.spec.ts b/frontend/e2e/tests/dev-console/config-maps-dev-perspective.spec.ts new file mode 100644 index 00000000000..97b8255e83f --- /dev/null +++ b/frontend/e2e/tests/dev-console/config-maps-dev-perspective.spec.ts @@ -0,0 +1,89 @@ +import { test } from '../../fixtures'; +import KubernetesClient from '../../clients/kubernetes-client'; +import { DetailsPage } from '../../pages/details-page'; +import { ListPage } from '../../pages/list-page'; +import { ConfigMapPage } from '../../pages/dev-console/config-map-page'; +import { PerspectivePage } from '../../pages/dev-console/perspective-page'; + +test.describe('Config maps form view', { tag: ['@smoke', '@dev-console'] }, () => { + const namespace = `aut-config-map-${Date.now()}`; + let k8s: KubernetesClient; + + test.beforeAll(async () => { + k8s = new KubernetesClient({ + clusterUrl: process.env.CLUSTER_URL || '', + username: process.env.OPENSHIFT_USERNAME || 'kubeadmin', + password: process.env.BRIDGE_KUBEADMIN_PASSWORD || '', + }); + await k8s.createNamespace(namespace); + }); + + test.afterAll(async () => { + await k8s.deleteNamespace(namespace); + }); + + test('create config map using form view', async ({ page }) => { + const perspectivePage = new PerspectivePage(page); + const configMapPage = new ConfigMapPage(page); + const detailsPage = new DetailsPage(page); + + await test.step('Navigate to ConfigMaps', async () => { + await page.goto('/'); + await perspectivePage.switchToDeveloper(); + await perspectivePage.selectOrCreateProject(namespace); + await perspectivePage.navigateToConfigMaps(); + }); + + await test.step('Create config map', async () => { + await configMapPage.createConfigMap('test-config-map', 'test-key', 'test-value'); + }); + + await test.step('Verify config map details', async () => { + await detailsPage.titleShouldContain('test-config-map'); + }); + }); + + test('edit config map using form view', { tag: ['@regression'] }, async ({ page }) => { + const perspectivePage = new PerspectivePage(page); + const configMapPage = new ConfigMapPage(page); + const detailsPage = new DetailsPage(page); + const listPage = new ListPage(page); + const configMapName = `test-config-map1-${Date.now()}`; + + await test.step('Create config map via API', async () => { + await k8s.coreV1Api.createNamespacedConfigMap({ + namespace, + body: { + apiVersion: 'v1', + kind: 'ConfigMap', + metadata: { name: configMapName, namespace }, + data: { 'test-key': 'test-value' }, + }, + }); + }); + + await test.step('Navigate to ConfigMaps list', async () => { + await page.goto('/'); + await perspectivePage.switchToDeveloper(); + await perspectivePage.selectOrCreateProject(namespace); + await perspectivePage.navigateToConfigMaps(); + }); + + await test.step('Open kebab menu and click Edit', async () => { + const row = listPage.resourceRow(configMapName); + await row.locator('[data-test-id="kebab-button"]').click(); + await page.locator('[data-test-action="Edit ConfigMap"]').click({ force: true }); + }); + + await test.step('Add a new key-value pair', async () => { + await configMapPage.addKeyValue(); + await configMapPage.fillSecondKey('key-test1'); + await configMapPage.submitForm(); + }); + + await test.step('Verify updated config map', async () => { + await detailsPage.titleShouldContain(configMapName); + await configMapPage.expectDataSectionToContain('key-test1'); + }); + }); +}); diff --git a/frontend/e2e/tests/dev-console/configure-perspectives.spec.ts b/frontend/e2e/tests/dev-console/configure-perspectives.spec.ts new file mode 100644 index 00000000000..9659fce7397 --- /dev/null +++ b/frontend/e2e/tests/dev-console/configure-perspectives.spec.ts @@ -0,0 +1,79 @@ +import { test, expect } from '../../fixtures'; +import { NavPage } from '../../pages/nav-page'; +import { PerspectivePage } from '../../pages/dev-console/perspective-page'; +import { CustomizationPage } from '../../pages/dev-console/customization-page'; + +test.describe('Configure perspectives', { tag: ['@regression', '@dev-console'] }, () => { + test('configuring available perspectives via YAML - disable admin', async () => { + test.skip(true, 'Manual test — requires YAML editing of cluster console resource'); + }); + + test('configuring available perspectives - add access review check', async () => { + test.skip(true, 'Manual test — requires YAML editing of cluster console resource'); + }); + + test('configuring available perspectives - add empty perspectives', async () => { + test.skip(true, 'Manual test — requires YAML editing of cluster console resource'); + }); + + test('enable dev perspective', async ({ page }) => { + const perspectivePage = new PerspectivePage(page); + const navPage = new NavPage(page); + const customizationPage = new CustomizationPage(page); + + await test.step('Navigate to admin perspective search page', async () => { + await page.goto('/'); + await perspectivePage.switchToAdministrator(); + }); + + await test.step('Search for console resource', async () => { + await navPage.clickNavLink(['Home', 'Search']); + const combobox = page.locator('[role="combobox"]'); + await combobox.click(); + const filterInput = page.locator('[aria-label="Type to filter"]'); + await filterInput.fill('console'); + const consoleItem = page.locator('[class="co-resource-item"]').filter({ + hasText: 'operator.openshift.io', + }); + if ((await consoleItem.count()) > 0) { + await consoleItem.click(); + } else { + await page.locator('[class="co-resource-item"]').first().click(); + } + await page + .locator('button[aria-label="Clear input value"]') + .click() + .catch(() => {}); + await page.locator('body').click(); + }); + + await test.step('Click on cluster', async () => { + await customizationPage.clickCluster(); + }); + + await test.step('Open customization and enable developer perspective', async () => { + const customizeButton = page.locator('button[data-test-action="Customize"]'); + await customizeButton.click(); + await expect(page.locator('[data-test="page-heading"] h1')).toHaveText( + 'Cluster configuration', + ); + await customizationPage.selectPerspectiveState('Enabled'); + }); + + await test.step('Verify saved alert', async () => { + await customizationPage.expectSuccessAlert(); + }); + + await test.step('Verify developer perspective available', async () => { + await page.reload(); + await page.waitForLoadState('domcontentloaded'); + const switcher = page.locator('[data-test-id="perspective-switcher-toggle"]'); + await switcher.click(); + await expect( + page.locator('[data-test-id="perspective-switcher-menu-option"]').filter({ + hasText: 'Developer', + }), + ).toBeVisible(); + }); + }); +}); diff --git a/frontend/e2e/tests/dev-console/configure-pinned-resources.spec.ts b/frontend/e2e/tests/dev-console/configure-pinned-resources.spec.ts new file mode 100644 index 00000000000..f0a0ac7a6af --- /dev/null +++ b/frontend/e2e/tests/dev-console/configure-pinned-resources.spec.ts @@ -0,0 +1,35 @@ +import { test, expect } from '../../fixtures'; +import { PerspectivePage } from '../../pages/dev-console/perspective-page'; + +test.describe('Configure pinned resources', { tag: ['@regression', '@dev-console'] }, () => { + test( + 'user has not configured the pre-pinned resources', + { tag: ['@smoke'] }, + async ({ page }) => { + const perspectivePage = new PerspectivePage(page); + + await test.step('Navigate to developer perspective', async () => { + await page.goto('/'); + await perspectivePage.switchToDeveloper(); + }); + + await test.step('Verify default pinned resources', async () => { + const pinnedItems = page.locator('[data-test="draggable-pinned-resource-item"]'); + await expect(pinnedItems.filter({ hasText: 'Secrets' })).toBeVisible(); + await expect(pinnedItems.filter({ hasText: 'ConfigMaps' })).toBeVisible(); + }); + }, + ); + + test('configuring pre-pinned resources', async () => { + test.skip(true, 'Manual test — requires YAML editing of cluster console resource'); + }); + + test('user customizing pinned resources on developer perspective navigation', async () => { + test.skip(true, 'Manual test — requires YAML editing of cluster console resource'); + }); + + test('user removing the pinnedResources customization from console config', async () => { + test.skip(true, 'Manual test — requires YAML editing of cluster console resource'); + }); +}); diff --git a/frontend/e2e/tests/dev-console/customization-of-catalog-add-page-form-view.spec.ts b/frontend/e2e/tests/dev-console/customization-of-catalog-add-page-form-view.spec.ts new file mode 100644 index 00000000000..5c200a4434d --- /dev/null +++ b/frontend/e2e/tests/dev-console/customization-of-catalog-add-page-form-view.spec.ts @@ -0,0 +1,190 @@ +import { test, expect } from '../../fixtures'; +import KubernetesClient from '../../clients/kubernetes-client'; +import { PerspectivePage } from '../../pages/dev-console/perspective-page'; +import { CustomizationPage } from '../../pages/dev-console/customization-page'; + +test.describe( + 'Customization of catalogs and Add page through form view', + { tag: ['@regression', '@dev-console'] }, + () => { + const namespace = `aut-software-catalog-${Date.now()}`; + let k8s: KubernetesClient; + + test.beforeAll(async () => { + k8s = new KubernetesClient({ + clusterUrl: process.env.CLUSTER_URL || '', + username: process.env.OPENSHIFT_USERNAME || 'kubeadmin', + password: process.env.BRIDGE_KUBEADMIN_PASSWORD || '', + }); + await k8s.createNamespace(namespace); + }); + + test.afterAll(async () => { + await k8s.deleteNamespace(namespace); + }); + + test('when all the sub-catalogs are disabled', async ({ page }) => { + const perspectivePage = new PerspectivePage(page); + const customizationPage = new CustomizationPage(page); + + await test.step('Navigate to console customization', async () => { + await page.goto('/'); + await perspectivePage.switchToDeveloper(); + await perspectivePage.selectOrCreateProject(namespace); + await customizationPage.navigateToConsoles(); + await customizationPage.clickCluster(); + await customizationPage.openCustomization(); + }); + + await test.step('Disable all software catalog items', async () => { + await customizationPage.clickDeveloperTab(); + await customizationPage.disableAllSoftwareCatalogItems(); + }); + + await test.step('Verify save message', async () => { + await customizationPage.expectSaveMessage(); + }); + + await test.step('Verify software catalog not visible', async () => { + await perspectivePage.switchToAdministrator(); + await perspectivePage.switchToDeveloper(); + await perspectivePage.selectOrCreateProject(namespace); + await perspectivePage.navigateToAdd(); + await page.reload(); + await page.waitForLoadState('domcontentloaded'); + await expect(page.getByTestId('card developer-catalog')).toBeHidden(); + }); + }); + + test('when specific sub-catalog is disabled', async ({ page }) => { + const perspectivePage = new PerspectivePage(page); + const customizationPage = new CustomizationPage(page); + + await test.step('Navigate to customization', async () => { + await page.goto('/'); + await perspectivePage.switchToDeveloper(); + await perspectivePage.selectOrCreateProject(namespace); + await customizationPage.navigateToConsoles(); + await customizationPage.clickCluster(); + await customizationPage.openCustomization(); + }); + + await test.step('Disable Helm Charts in software catalog', async () => { + await customizationPage.clickDeveloperTab(); + await customizationPage.disableSoftwareCatalogItem('Helm Charts'); + }); + + await test.step('Verify save message', async () => { + await customizationPage.expectSaveMessage(); + }); + + await test.step('Verify Helm Charts not visible but catalog exists', async () => { + await perspectivePage.switchToAdministrator(); + await perspectivePage.switchToDeveloper(); + await perspectivePage.selectOrCreateProject(namespace); + await perspectivePage.navigateToAdd(); + await page.reload(); + await page.waitForLoadState('domcontentloaded'); + await expect(page.getByTestId('card developer-catalog')).toBeVisible({ timeout: 60_000 }); + await expect(page.getByTestId('item helm')).toBeHidden(); + }); + }); + + test('when specific sub-catalog is enabled', async ({ page }) => { + const perspectivePage = new PerspectivePage(page); + const customizationPage = new CustomizationPage(page); + + await test.step('Navigate to customization', async () => { + await page.goto('/'); + await perspectivePage.switchToDeveloper(); + await perspectivePage.selectOrCreateProject(namespace); + await customizationPage.navigateToConsoles(); + await customizationPage.clickCluster(); + await customizationPage.openCustomization(); + }); + + await test.step('Enable only Helm Charts', async () => { + await customizationPage.clickDeveloperTab(); + await customizationPage.enableOnlySoftwareCatalogItem('Helm Charts'); + }); + + await test.step('Verify save message', async () => { + await customizationPage.expectSaveMessage(); + }); + + await test.step('Verify only Helm Charts visible', async () => { + await perspectivePage.switchToAdministrator(); + await perspectivePage.switchToDeveloper(); + await perspectivePage.selectOrCreateProject(namespace); + await perspectivePage.navigateToAdd(); + await page.reload(); + await page.waitForLoadState('domcontentloaded'); + await expect(page.getByTestId('card developer-catalog')).toBeVisible({ timeout: 60_000 }); + await expect(page.getByTestId('item helm')).toBeVisible(); + await expect(page.getByTestId('item operator-backed')).toBeHidden(); + }); + }); + + test('when all the add page items are disabled', async ({ page }) => { + const perspectivePage = new PerspectivePage(page); + const customizationPage = new CustomizationPage(page); + + await test.step('Navigate to customization', async () => { + await page.goto('/'); + await perspectivePage.switchToDeveloper(); + await perspectivePage.selectOrCreateProject(namespace); + await customizationPage.navigateToConsoles(); + await customizationPage.clickCluster(); + await customizationPage.openCustomization(); + }); + + await test.step('Disable all add page items', async () => { + await customizationPage.clickDeveloperTab(); + await customizationPage.disableAllAddPageItems(); + }); + + await test.step('Verify save message', async () => { + await customizationPage.expectSaveMessage(); + }); + + await test.step('Verify add page only shows Getting Started', async () => { + await perspectivePage.navigateToAdd(); + await page.reload(); + await page.waitForLoadState('domcontentloaded'); + await expect(page.getByTestId('getting-started')).toBeVisible({ timeout: 60_000 }); + const cards = page.getByTestId('add-page').locator('[data-test^=card]'); + await expect(cards).toHaveCount(2); + }); + }); + + test('when specific add page item is disabled', async ({ page }) => { + const perspectivePage = new PerspectivePage(page); + const customizationPage = new CustomizationPage(page); + + await test.step('Navigate to customization', async () => { + await page.goto('/'); + await perspectivePage.switchToDeveloper(); + await perspectivePage.selectOrCreateProject(namespace); + await customizationPage.navigateToConsoles(); + await customizationPage.clickCluster(); + await customizationPage.openCustomization(); + }); + + await test.step('Disable Import from Git', async () => { + await customizationPage.clickDeveloperTab(); + await customizationPage.disableAddPageItem('Import from Git'); + }); + + await test.step('Verify save message', async () => { + await customizationPage.expectSaveMessage(); + }); + + await test.step('Verify Import from Git not visible', async () => { + await perspectivePage.navigateToAdd(); + await page.reload(); + await page.waitForLoadState('domcontentloaded'); + await expect(page.getByTestId('card git-repository')).toBeHidden(); + }); + }); + }, +); diff --git a/frontend/e2e/tests/dev-console/customization-of-catalog.spec.ts b/frontend/e2e/tests/dev-console/customization-of-catalog.spec.ts new file mode 100644 index 00000000000..ffd9d30fb60 --- /dev/null +++ b/frontend/e2e/tests/dev-console/customization-of-catalog.spec.ts @@ -0,0 +1,23 @@ +import { test } from '../../fixtures'; + +test.describe( + 'Customization of catalogs via YAML', + { tag: ['@regression', '@dev-console'] }, + () => { + test('when all the sub-catalogs are disabled', async () => { + test.skip(true, 'Manual test — requires YAML editing of cluster console resource'); + }); + + test('when all the sub-catalogs are enabled', async () => { + test.skip(true, 'Manual test — requires YAML editing of cluster console resource'); + }); + + test('when specific sub-catalog is disabled', async () => { + test.skip(true, 'Manual test — requires YAML editing of cluster console resource'); + }); + + test('when specific sub-catalog is enabled', async () => { + test.skip(true, 'Manual test — requires YAML editing of cluster console resource'); + }); + }, +); diff --git a/frontend/e2e/tests/dev-console/customization-of-pinned-resource.spec.ts b/frontend/e2e/tests/dev-console/customization-of-pinned-resource.spec.ts new file mode 100644 index 00000000000..cc00aa3057d --- /dev/null +++ b/frontend/e2e/tests/dev-console/customization-of-pinned-resource.spec.ts @@ -0,0 +1,62 @@ +import { test } from '../../fixtures'; +import KubernetesClient from '../../clients/kubernetes-client'; +import { PerspectivePage } from '../../pages/dev-console/perspective-page'; +import { CustomizationPage } from '../../pages/dev-console/customization-page'; + +test.describe( + 'Customization of pre-pinned resources', + { tag: ['@regression', '@dev-console'] }, + () => { + const namespace = `aut-pinned-resources-${Date.now()}`; + let k8s: KubernetesClient; + + test.beforeAll(async () => { + k8s = new KubernetesClient({ + clusterUrl: process.env.CLUSTER_URL || '', + username: process.env.OPENSHIFT_USERNAME || 'kubeadmin', + password: process.env.BRIDGE_KUBEADMIN_PASSWORD || '', + }); + await k8s.createNamespace(namespace); + }); + + test.afterAll(async () => { + await k8s.deleteNamespace(namespace); + }); + + test('navigating to cluster configuration page shows pinned resources section', async ({ + page, + }) => { + const perspectivePage = new PerspectivePage(page); + const customizationPage = new CustomizationPage(page); + + await test.step('Navigate to developer perspective', async () => { + await page.goto('/'); + await perspectivePage.switchToDeveloper(); + await perspectivePage.selectOrCreateProject(namespace); + }); + + await test.step('Navigate to Consoles and open customization', async () => { + await customizationPage.navigateToConsoles(); + await customizationPage.clickCluster(); + await customizationPage.openCustomization(); + }); + + await test.step('Click Developer tab and verify pinned resources section', async () => { + await customizationPage.clickDeveloperTab(); + await customizationPage.expectPinnedResourceSection(); + }); + }); + + test('when pre-pinned resources customization is not added', async () => { + test.skip(true, 'Manual test — requires YAML verification of cluster console resource'); + }); + + test('when resource is selected for pre-pinned navigation', async () => { + test.skip(true, 'Manual test — requires YAML verification of cluster console resource'); + }); + + test('when resource is removed from pre-pinned navigation', async () => { + test.skip(true, 'Manual test — requires YAML verification of cluster console resource'); + }); + }, +); diff --git a/frontend/e2e/tests/dev-console/getting-started-to-dev-perspective.spec.ts b/frontend/e2e/tests/dev-console/getting-started-to-dev-perspective.spec.ts new file mode 100644 index 00000000000..886879a28ed --- /dev/null +++ b/frontend/e2e/tests/dev-console/getting-started-to-dev-perspective.spec.ts @@ -0,0 +1,7 @@ +import { test } from '../../fixtures'; + +test.describe('Login to developer perspective', { tag: ['@regression', '@dev-console'] }, () => { + test('developer perspective display on login with developer credentials', async () => { + test.skip(true, 'Marked @to-do in Cypress — requires separate developer credentials'); + }); +}); diff --git a/frontend/e2e/tests/dev-console/getting-started-tour-dev-perspective.spec.ts b/frontend/e2e/tests/dev-console/getting-started-tour-dev-perspective.spec.ts new file mode 100644 index 00000000000..34311481499 --- /dev/null +++ b/frontend/e2e/tests/dev-console/getting-started-tour-dev-perspective.spec.ts @@ -0,0 +1,19 @@ +import { test } from '../../fixtures'; + +test.describe( + 'Getting Started tour of developer perspective', + { tag: ['@regression', '@dev-console'] }, + () => { + test('quick tour when user logs in for the first time', async () => { + test.skip(true, 'Manual test — guided tour disabled in Cypress due to flakes'); + }); + + test('quick tour from help menu', async () => { + test.skip(true, 'Manual test — guided tour disabled in Cypress due to flakes'); + }); + + test('stopping quick tour in mid of the tour', async () => { + test.skip(true, 'Manual test — guided tour disabled in Cypress due to flakes'); + }); + }, +); diff --git a/frontend/e2e/tests/dev-console/pod-list-page.spec.ts b/frontend/e2e/tests/dev-console/pod-list-page.spec.ts new file mode 100644 index 00000000000..9596d4289ae --- /dev/null +++ b/frontend/e2e/tests/dev-console/pod-list-page.spec.ts @@ -0,0 +1,104 @@ +import { test } from '../../fixtures'; +import KubernetesClient from '../../clients/kubernetes-client'; +import { NavPage } from '../../pages/nav-page'; +import { PerspectivePage } from '../../pages/dev-console/perspective-page'; +import { PodListPage } from '../../pages/dev-console/pod-list-page'; + +test.describe('Traffic Status details for pods', { tag: ['@regression', '@dev-console'] }, () => { + const namespace = `aut-pods-${Date.now()}`; + let k8s: KubernetesClient; + + test.beforeAll(async () => { + k8s = new KubernetesClient({ + clusterUrl: process.env.CLUSTER_URL || '', + username: process.env.OPENSHIFT_USERNAME || 'kubeadmin', + password: process.env.BRIDGE_KUBEADMIN_PASSWORD || '', + }); + await k8s.createNamespace(namespace); + + await k8s.appsV1Api.createNamespacedDeployment({ + namespace, + body: { + apiVersion: 'apps/v1', + kind: 'Deployment', + metadata: { name: 'nodejs-ex-git', namespace }, + spec: { + replicas: 1, + selector: { matchLabels: { app: 'nodejs-ex-git' } }, + template: { + metadata: { labels: { app: 'nodejs-ex-git' } }, + spec: { + containers: [ + { + name: 'nodejs-ex-git', + image: 'registry.access.redhat.com/ubi8/nodejs-18:latest', + ports: [{ containerPort: 8080, protocol: 'TCP' }], + }, + ], + }, + }, + }, + }, + }); + + await k8s + .waitForCustomResourceCondition( + 'apps', + 'v1', + namespace, + 'deployments', + 'nodejs-ex-git', + (resource: any) => resource?.status?.availableReplicas > 0, + 120_000, + ) + .catch(() => { + // Deployment may not be fully ready, but we need pods to exist + }); + }); + + test.afterAll(async () => { + await k8s.deleteNamespace(namespace); + }); + + test('check traffic status for pods in a project', async ({ page }) => { + const perspectivePage = new PerspectivePage(page); + const navPage = new NavPage(page); + const podListPage = new PodListPage(page); + + await test.step('Navigate to administrator pods tab', async () => { + await page.goto('/'); + await perspectivePage.switchToAdministrator(); + await perspectivePage.selectOrCreateProject(namespace); + await navPage.clickNavLink(['Workloads', 'Pods']); + }); + + await test.step('Enable Receiving Traffic column', async () => { + await podListPage.enableReceivingTrafficColumn(); + }); + + await test.step('Verify Receiving Traffic column is visible', async () => { + await podListPage.expectReceivingTrafficColumnVisible(); + }); + }); + + test('check traffic status for pods for all projects', async ({ page }) => { + const perspectivePage = new PerspectivePage(page); + const navPage = new NavPage(page); + const podListPage = new PodListPage(page); + + await test.step('Navigate to administrator pods tab', async () => { + await page.goto('/'); + await perspectivePage.switchToAdministrator(); + await perspectivePage.selectOrCreateProject('All Projects'); + await navPage.clickNavLink(['Workloads', 'Pods']); + }); + + await test.step('Enable Receiving Traffic column', async () => { + await podListPage.enableReceivingTrafficColumn(); + }); + + await test.step('Verify Receiving Traffic column is visible', async () => { + await podListPage.expectReceivingTrafficColumnVisible(); + }); + }); +}); diff --git a/frontend/e2e/tests/dev-console/project-access.spec.ts b/frontend/e2e/tests/dev-console/project-access.spec.ts new file mode 100644 index 00000000000..c73915ba826 --- /dev/null +++ b/frontend/e2e/tests/dev-console/project-access.spec.ts @@ -0,0 +1,11 @@ +import { test } from '../../fixtures'; + +test.describe('Customize Project Access roles', { tag: ['@regression', '@dev-console'] }, () => { + test('adding custom role in project membership', async () => { + test.skip(true, 'Manual test — requires YAML editing of cluster console resource'); + }); + + test('removing custom role in project membership', async () => { + test.skip(true, 'Manual test — requires YAML editing of cluster console resource'); + }); +}); diff --git a/frontend/e2e/tests/dev-console/project-creation.spec.ts b/frontend/e2e/tests/dev-console/project-creation.spec.ts new file mode 100644 index 00000000000..1922cf37408 --- /dev/null +++ b/frontend/e2e/tests/dev-console/project-creation.spec.ts @@ -0,0 +1,28 @@ +import { test, expect } from '../../fixtures'; +import { ModalPage } from '../../pages/modal-page'; +import { PerspectivePage } from '../../pages/dev-console/perspective-page'; + +test.describe('OpenShift Namespaces', { tag: ['@smoke', '@dev-console'] }, () => { + test('create a namespace via the Create Project modal', async ({ page, cleanup }) => { + const perspectivePage = new PerspectivePage(page); + const modalPage = new ModalPage(page); + const projectName = `aut-project-${Date.now()}-ns`; + + await test.step('Switch to developer perspective', async () => { + await page.goto('/'); + await perspectivePage.switchToDeveloper(); + }); + + await test.step('Open Create Project modal', async () => { + await perspectivePage.selectOrCreateProject(projectName); + cleanup.trackNamespace(projectName); + }); + + await test.step('Verify modal closes and project is created', async () => { + await modalPage.shouldBeClosed(); + await expect(page.locator('[data-test-id="namespace-bar-dropdown"]')).toContainText( + projectName, + ); + }); + }); +}); diff --git a/frontend/e2e/tests/dev-console/project-vulnerability.spec.ts b/frontend/e2e/tests/dev-console/project-vulnerability.spec.ts new file mode 100644 index 00000000000..fe8ae32a30c --- /dev/null +++ b/frontend/e2e/tests/dev-console/project-vulnerability.spec.ts @@ -0,0 +1,153 @@ +import { test } from '../../fixtures'; +import KubernetesClient from '../../clients/kubernetes-client'; +import { PerspectivePage } from '../../pages/dev-console/perspective-page'; +import { VulnerabilityPage } from '../../pages/dev-console/vulnerability-page'; + +test.describe('Image Vulnerability in Project', { tag: ['@regression', '@dev-console'] }, () => { + const namespace = `aut-image-vulnerability-${Date.now()}`; + let k8s: KubernetesClient; + + test.beforeAll(async () => { + k8s = new KubernetesClient({ + clusterUrl: process.env.CLUSTER_URL || '', + username: process.env.OPENSHIFT_USERNAME || 'kubeadmin', + password: process.env.BRIDGE_KUBEADMIN_PASSWORD || '', + }); + await k8s.createNamespace(namespace); + + await k8s.createCustomResource('apps', 'v1', namespace, 'deployments', { + apiVersion: 'apps/v1', + kind: 'Deployment', + metadata: { name: 'test-vulnerability', namespace }, + spec: { + selector: { matchLabels: { app: 'quarkus' } }, + replicas: 3, + template: { + metadata: { labels: { app: 'quarkus' } }, + spec: { + containers: [ + { + name: 'quarkus', + image: 'quay.io/redhat-appstudio-qe/quarkus:7dd062e2e8cb4ba599185e48d628b65a', + ports: [{ containerPort: 8080 }], + }, + ], + }, + }, + }, + }); + }); + + test.afterAll(async () => { + await k8s.deleteNamespace(namespace); + }); + + test('vulnerability tab shows image manifest vulnerabilities', async ({ page }) => { + const perspectivePage = new PerspectivePage(page); + const vulnerabilityPage = new VulnerabilityPage(page); + + await test.step('Navigate to project tab', async () => { + await page.goto('/'); + await perspectivePage.switchToDeveloper(); + await perspectivePage.selectOrCreateProject(namespace); + await perspectivePage.navigateToProject(); + }); + + await test.step('Click vulnerability tab and verify content', async () => { + await vulnerabilityPage.clickVulnerabilityTab(); + await vulnerabilityPage.expectVulnerabilityTableVisible(); + await vulnerabilityPage.expectFilterSelected('Name'); + await vulnerabilityPage.expectTableHeaders(); + }); + }); + + test('visit quay page from status in project overview', async () => { + test.skip(true, 'Manual test — requires quay.io verification'); + }); + + test('filter in vulnerability tab', async ({ page }) => { + const perspectivePage = new PerspectivePage(page); + const vulnerabilityPage = new VulnerabilityPage(page); + + await test.step('Navigate to project tab', async () => { + await page.goto('/'); + await perspectivePage.switchToDeveloper(); + await perspectivePage.selectOrCreateProject(namespace); + await perspectivePage.navigateToProject(); + }); + + await test.step('Open vulnerability popup and view all', async () => { + await vulnerabilityPage.clickImageVulnerabilityAlert(); + await vulnerabilityPage.clickViewAllLink(); + }); + + await test.step('Change filter to Label', async () => { + await vulnerabilityPage.clickFilterDropdown(); + await vulnerabilityPage.selectFilterOption('LABEL'); + }); + + await test.step('Verify Label filter is selected', async () => { + await vulnerabilityPage.expectFilterSelected('Label'); + }); + }); + + test('image manifests vulnerability details page', async ({ page }) => { + const perspectivePage = new PerspectivePage(page); + const vulnerabilityPage = new VulnerabilityPage(page); + + await test.step('Navigate to project tab', async () => { + await page.goto('/'); + await perspectivePage.switchToDeveloper(); + await perspectivePage.selectOrCreateProject(namespace); + await perspectivePage.navigateToProject(); + }); + + await test.step('Click image vulnerability and navigate to details', async () => { + await vulnerabilityPage.clickImageVulnerabilityAlert(); + await vulnerabilityPage.clickFirstVulnerabilityLink(); + }); + + await test.step('Select Base image vulnerability type', async () => { + await vulnerabilityPage.clickVulnerabilityType('Base image'); + }); + + await test.step('Verify details tabs and selected type', async () => { + await vulnerabilityPage.expectDetailsTabs(); + await vulnerabilityPage.expectVulnerabilityTypeSelected('Base image'); + }); + }); + + test('filters of vulnerability section in details page', async ({ page }) => { + const perspectivePage = new PerspectivePage(page); + const vulnerabilityPage = new VulnerabilityPage(page); + + await test.step('Navigate to vulnerability details page', async () => { + await page.goto('/'); + await perspectivePage.switchToDeveloper(); + await perspectivePage.selectOrCreateProject(namespace); + await perspectivePage.navigateToProject(); + await vulnerabilityPage.navigateToVulnerabilityDetailsPage('redhat-appstudio-qe/quarkus'); + }); + + await test.step('Open filter dropdown and select App dependency', async () => { + await vulnerabilityPage.openFilterDropdownInVulnerabilities(); + await vulnerabilityPage.clickTypeFilter('App dependency'); + }); + + await test.step('Verify App dependency filter is checked', async () => { + await vulnerabilityPage.expectTypeFilterChecked('App dependency'); + }); + + await test.step('Verify severity filters', async () => { + await vulnerabilityPage.expectSeverityFilters([ + 'Defcon1', + 'Critical', + 'High', + 'Medium', + 'Low', + 'Negligible', + 'Unknown', + ]); + }); + }); +}); diff --git a/frontend/e2e/tests/dev-console/route-dev-perspective.spec.ts b/frontend/e2e/tests/dev-console/route-dev-perspective.spec.ts new file mode 100644 index 00000000000..815185226f3 --- /dev/null +++ b/frontend/e2e/tests/dev-console/route-dev-perspective.spec.ts @@ -0,0 +1,134 @@ +import { test, expect } from '../../fixtures'; +import KubernetesClient from '../../clients/kubernetes-client'; +import { DetailsPage } from '../../pages/details-page'; +import { ListPage } from '../../pages/list-page'; +import { PerspectivePage } from '../../pages/dev-console/perspective-page'; +import { RoutePage } from '../../pages/dev-console/route-page'; + +test.describe('Route form view', { tag: ['@smoke', '@dev-console'] }, () => { + const namespace = `aut-routes-${Date.now()}`; + let k8s: KubernetesClient; + + test.beforeAll(async () => { + k8s = new KubernetesClient({ + clusterUrl: process.env.CLUSTER_URL || '', + username: process.env.OPENSHIFT_USERNAME || 'kubeadmin', + password: process.env.BRIDGE_KUBEADMIN_PASSWORD || '', + }); + await k8s.createNamespace(namespace); + + await k8s.appsV1Api.createNamespacedDeployment({ + namespace, + body: { + apiVersion: 'apps/v1', + kind: 'Deployment', + metadata: { name: 'nodejs-ex-git1', namespace }, + spec: { + replicas: 1, + selector: { matchLabels: { app: 'nodejs-ex-git1' } }, + template: { + metadata: { labels: { app: 'nodejs-ex-git1' } }, + spec: { + containers: [ + { + name: 'nodejs-ex-git1', + image: 'registry.access.redhat.com/ubi8/nodejs-18:latest', + ports: [{ containerPort: 8080, protocol: 'TCP' }], + }, + ], + }, + }, + }, + }, + }); + + await k8s.coreV1Api.createNamespacedService({ + namespace, + body: { + apiVersion: 'v1', + kind: 'Service', + metadata: { name: 'nodejs-ex-git1', namespace }, + spec: { + selector: { app: 'nodejs-ex-git1' }, + ports: [{ port: 8080, targetPort: 8080, protocol: 'TCP' }], + }, + }, + }); + }); + + test.afterAll(async () => { + await k8s.deleteNamespace(namespace); + }); + + test('create route using form view', async ({ page }) => { + const perspectivePage = new PerspectivePage(page); + const routePage = new RoutePage(page); + const detailsPage = new DetailsPage(page); + + await test.step('Navigate to Routes', async () => { + await page.goto('/'); + await perspectivePage.switchToDeveloper(); + await perspectivePage.selectOrCreateProject(namespace); + await perspectivePage.navigateToRoutes(); + }); + + await test.step('Create route', async () => { + await routePage.clickCreateRoute(); + await routePage.fillName('test-route'); + await routePage.fillHostname('example.com'); + await routePage.selectService('nodejs-ex-git1'); + await routePage.selectTargetPort('8080 → 8080 (TCP)'); + await routePage.submitForm(); + }); + + await test.step('Verify route details', async () => { + await routePage.expectBreadcrumbToContainRoutes(); + await detailsPage.titleShouldContain('test-route'); + }); + }); + + test('edit route using form view', { tag: ['@regression'] }, async ({ page }) => { + const perspectivePage = new PerspectivePage(page); + const routePage = new RoutePage(page); + const detailsPage = new DetailsPage(page); + const listPage = new ListPage(page); + + await test.step('Create a route to edit', async () => { + await k8s.createCustomResource('route.openshift.io', 'v1', namespace, 'routes', { + apiVersion: 'route.openshift.io/v1', + kind: 'Route', + metadata: { name: 'test-route1', namespace }, + spec: { + host: 'original.example.com', + to: { kind: 'Service', name: 'nodejs-ex-git1', weight: 100 }, + port: { targetPort: 8080 }, + }, + }); + }); + + await test.step('Navigate to Routes list', async () => { + await page.goto('/'); + await perspectivePage.switchToDeveloper(); + await perspectivePage.selectOrCreateProject(namespace); + await perspectivePage.navigateToRoutes(); + }); + + await test.step('Open kebab menu and edit route', async () => { + const row = listPage.resourceRow('test-route1'); + await row.locator('[data-test-id="kebab-button"]').click(); + await page.locator('[data-test-action="Edit Route"]').click({ force: true }); + }); + + await test.step('Change hostname and save', async () => { + await routePage.fillHostname('test.com'); + await routePage.submitForm(); + }); + + await test.step('Verify updated route', async () => { + await detailsPage.titleShouldContain('test-route1'); + await expect(page.locator('[data-test-selector="details-item-value__Host"]')).toContainText( + 'test.com', + ); + }); + }); +}); diff --git a/frontend/e2e/tests/dev-console/sample-card-add-page.spec.ts b/frontend/e2e/tests/dev-console/sample-card-add-page.spec.ts new file mode 100644 index 00000000000..9421bfbd4bf --- /dev/null +++ b/frontend/e2e/tests/dev-console/sample-card-add-page.spec.ts @@ -0,0 +1,158 @@ +import { test, expect } from '../../fixtures'; +import KubernetesClient from '../../clients/kubernetes-client'; +import { PerspectivePage } from '../../pages/dev-console/perspective-page'; +import { SamplesPage } from '../../pages/dev-console/samples-page'; + +test.describe('Create Sample Application', { tag: ['@regression', '@dev-console'] }, () => { + const namespace = `aut-addflow-catalog-${Date.now()}`; + let k8s: KubernetesClient; + + test.beforeAll(async () => { + k8s = new KubernetesClient({ + clusterUrl: process.env.CLUSTER_URL || '', + username: process.env.OPENSHIFT_USERNAME || 'kubeadmin', + password: process.env.BRIDGE_KUBEADMIN_PASSWORD || '', + }); + await k8s.createNamespace(namespace); + }); + + test.afterAll(async () => { + await k8s.deleteNamespace(namespace); + }); + + test('sample card in add flow', async ({ page }) => { + const perspectivePage = new PerspectivePage(page); + const samplesPage = new SamplesPage(page); + + await test.step('Navigate to Add page', async () => { + await page.goto('/'); + await perspectivePage.switchToDeveloper(); + await perspectivePage.selectOrCreateProject(namespace); + await perspectivePage.navigateToAdd(); + }); + + await test.step('Click View all samples link', async () => { + await samplesPage.clickViewAllSamples(); + }); + + await test.step('Verify samples page content', async () => { + await samplesPage.expectSamplesPageHeading(); + await samplesPage.expectSampleApplicationsVisible(); + await samplesPage.expectBuilderImageBasedSamples(); + }); + }); + + for (const { cardName, formHeader, workloadName } of [ + { cardName: 'Httpd', formHeader: 'Create Sample application', workloadName: 'httpd-sample' }, + { cardName: 'Basic Go', formHeader: 'Import from Git', workloadName: 'go-basic' }, + ]) { + test(`create sample application: ${cardName}`, async ({ page }) => { + const perspectivePage = new PerspectivePage(page); + const samplesPage = new SamplesPage(page); + + await test.step('Navigate to Add page', async () => { + await page.goto('/'); + await perspectivePage.switchToDeveloper(); + await perspectivePage.selectOrCreateProject(namespace); + await perspectivePage.navigateToAdd(); + }); + + await test.step('Select sample and create', async () => { + await samplesPage.clickSamplesCard(); + await samplesPage.searchAndSelectSample(cardName); + await samplesPage.expectFormHeader(formHeader); + await samplesPage.clickCreate(); + }); + + await test.step('Verify workload in topology', async () => { + await page.waitForURL(/\/topology\//); + await expect(page.locator(`[data-test-id="${workloadName}"]`).first()).toBeVisible({ + timeout: 60_000, + }); + }); + }); + } + + test('review sample application form', async ({ page }) => { + const perspectivePage = new PerspectivePage(page); + const samplesPage = new SamplesPage(page); + + await test.step('Navigate to samples page', async () => { + await page.goto('/'); + await perspectivePage.switchToDeveloper(); + await perspectivePage.selectOrCreateProject(namespace); + await perspectivePage.navigateToAdd(); + await samplesPage.clickViewAllSamples(); + await samplesPage.expectSamplesPageHeading(); + }); + + await test.step('Select Go sample', async () => { + await samplesPage.searchAndSelectSample('Go'); + await samplesPage.expectFormHeader('Create Sample application'); + }); + + await test.step('Verify form elements', async () => { + await samplesPage.expectNameSectionVisible(); + await samplesPage.expectBuilderImageVersionDropdownVisible(); + await samplesPage.expectBuilderImageVisible(); + await samplesPage.expectGitUrlReadonly(); + await samplesPage.expectCreateAndCancelButtons(); + }); + }); + + test('edit sample application form', async ({ page }) => { + const perspectivePage = new PerspectivePage(page); + const samplesPage = new SamplesPage(page); + + await test.step('Navigate to samples page', async () => { + await page.goto('/'); + await perspectivePage.switchToDeveloper(); + await perspectivePage.selectOrCreateProject(namespace); + await perspectivePage.navigateToAdd(); + await samplesPage.clickViewAllSamples(); + await samplesPage.expectSamplesPageHeading(); + }); + + await test.step('Select Go sample and edit', async () => { + await samplesPage.searchAndSelectSample('Go'); + await samplesPage.expectFormHeader('Create Sample application'); + await samplesPage.fillName('golang-sample-app1'); + await samplesPage.changeBuilderImageVersion('latest'); + await samplesPage.clickCreate(); + }); + + await test.step('Verify workload in topology', async () => { + await page.waitForURL(/\/topology\//); + await expect(page.locator('[data-test-id="golang-sample-app1"]').first()).toBeVisible({ + timeout: 60_000, + }); + }); + }); + + test('create basic nodejs devfile sample application', async ({ page }) => { + const perspectivePage = new PerspectivePage(page); + const samplesPage = new SamplesPage(page); + + await test.step('Navigate to samples page', async () => { + await page.goto('/'); + await perspectivePage.switchToDeveloper(); + await perspectivePage.selectOrCreateProject(namespace); + await perspectivePage.navigateToAdd(); + await samplesPage.clickViewAllSamples(); + await samplesPage.expectSamplesPageHeading(); + }); + + await test.step('Select Basic Node.js and create', async () => { + await samplesPage.searchAndSelectSample('Basic Node.js'); + await samplesPage.fillDevfileName('node-js-basic-sample1'); + await samplesPage.clickCreate(); + }); + + await test.step('Verify workload in topology', async () => { + await page.waitForURL(/\/topology\//); + await expect(page.locator('[data-test-id="node-js-basic-sample1"]').first()).toBeVisible({ + timeout: 60_000, + }); + }); + }); +}); diff --git a/frontend/e2e/tests/dev-console/search-devperspective.spec.ts b/frontend/e2e/tests/dev-console/search-devperspective.spec.ts new file mode 100644 index 00000000000..64714dad980 --- /dev/null +++ b/frontend/e2e/tests/dev-console/search-devperspective.spec.ts @@ -0,0 +1,112 @@ +import { test } from '../../fixtures'; +import KubernetesClient from '../../clients/kubernetes-client'; +import { PerspectivePage } from '../../pages/dev-console/perspective-page'; +import { SearchPage } from '../../pages/dev-console/search-page'; + +test.describe('Search page', { tag: ['@smoke', '@dev-console'] }, () => { + const namespace = `aut-search-enhancement-${Date.now()}`; + let k8s: KubernetesClient; + + test.beforeAll(async () => { + k8s = new KubernetesClient({ + clusterUrl: process.env.CLUSTER_URL || '', + username: process.env.OPENSHIFT_USERNAME || 'kubeadmin', + password: process.env.BRIDGE_KUBEADMIN_PASSWORD || '', + }); + await k8s.createNamespace(namespace); + }); + + test.afterAll(async () => { + await k8s.deleteNamespace(namespace); + }); + + test('recently searched section shows searched resource', async ({ page }) => { + const perspectivePage = new PerspectivePage(page); + const searchPage = new SearchPage(page); + + await test.step('Set up dev perspective and namespace', async () => { + await page.goto('/'); + await perspectivePage.switchToDeveloper(); + await perspectivePage.selectOrCreateProject(namespace); + }); + + await test.step('Search for AlertingRule', async () => { + await perspectivePage.navigateToSearch(); + await searchPage.searchAndSelectResource('AlertingRule'); + }); + + await test.step('Navigate away and return to Search', async () => { + await perspectivePage.navigateToTopology(); + await perspectivePage.navigateToSearch(); + }); + + await test.step('Verify AlertingRule in Recently used', async () => { + await searchPage.openResourceFilter(); + await searchPage.expectRecentlyUsedToContain('AlertingRule'); + }); + }); + + test( + 'five recently searched items appear in dropdown', + { tag: ['@regression'] }, + async ({ page }) => { + const perspectivePage = new PerspectivePage(page); + const searchPage = new SearchPage(page); + const resources = [ + 'AlertingRule', + 'Deployment', + 'DeploymentConfig', + 'ConfigMap', + 'BuildConfig', + ]; + + await test.step('Set up dev perspective and namespace', async () => { + await page.goto('/'); + await perspectivePage.switchToDeveloper(); + await perspectivePage.selectOrCreateProject(namespace); + }); + + await test.step('Search for 5 resources', async () => { + for (const resource of resources) { + await perspectivePage.navigateToSearch(); + await searchPage.searchAndSelectResource(resource); + await perspectivePage.navigateToTopology(); + } + }); + + await test.step('Verify all 5 in Recently used', async () => { + await perspectivePage.navigateToSearch(); + await searchPage.openResourceFilter(); + await searchPage.expectRecentlyUsedToContainAll(resources); + }); + }, + ); + + test( + 'clear history removes recently used section', + { tag: ['@regression'] }, + async ({ page }) => { + const perspectivePage = new PerspectivePage(page); + const searchPage = new SearchPage(page); + + await test.step('Set up and search for a resource', async () => { + await page.goto('/'); + await perspectivePage.switchToDeveloper(); + await perspectivePage.selectOrCreateProject(namespace); + await perspectivePage.navigateToSearch(); + await searchPage.searchAndSelectResource('AlertingRule'); + }); + + await test.step('Navigate away and return', async () => { + await perspectivePage.navigateToTopology(); + await perspectivePage.navigateToSearch(); + }); + + await test.step('Clear history and verify removal', async () => { + await searchPage.openResourceFilter(); + await searchPage.clearHistory(); + await searchPage.expectRecentlyUsedNotVisible(); + }); + }, + ); +}); diff --git a/frontend/e2e/tests/dev-console/user-preferences-dev-perspective.spec.ts b/frontend/e2e/tests/dev-console/user-preferences-dev-perspective.spec.ts new file mode 100644 index 00000000000..2c131c80e71 --- /dev/null +++ b/frontend/e2e/tests/dev-console/user-preferences-dev-perspective.spec.ts @@ -0,0 +1,222 @@ +import { test, expect } from '../../fixtures'; +import KubernetesClient from '../../clients/kubernetes-client'; +import { PerspectivePage } from '../../pages/dev-console/perspective-page'; +import { UserPreferencesPage } from '../../pages/dev-console/user-preferences-page'; + +test.describe('Manage user preferences', { tag: ['@dev-console'] }, () => { + const namespace = `aut-user-preferences-${Date.now()}`; + let k8s: KubernetesClient; + + test.beforeAll(async () => { + k8s = new KubernetesClient({ + clusterUrl: process.env.CLUSTER_URL || '', + username: process.env.OPENSHIFT_USERNAME || 'kubeadmin', + password: process.env.BRIDGE_KUBEADMIN_PASSWORD || '', + }); + await k8s.createNamespace(namespace); + }); + + test.afterAll(async () => { + await k8s.deleteNamespace(namespace); + }); + + test('visiting user preference page', { tag: ['@smoke'] }, async ({ page }) => { + const perspectivePage = new PerspectivePage(page); + const userPreferencesPage = new UserPreferencesPage(page); + + await test.step('Navigate to admin perspective', async () => { + await page.goto('/'); + await perspectivePage.switchToAdministrator(); + }); + + await test.step('Open user preferences', async () => { + await userPreferencesPage.openUserPreferences(); + }); + + await test.step('Verify tabs', async () => { + await userPreferencesPage.expectTabVisible('General'); + await userPreferencesPage.expectTabVisible('Language'); + }); + }); + + test( + 'setting developer preference for perspective', + { tag: ['@regression'] }, + async ({ page }) => { + const perspectivePage = new PerspectivePage(page); + const userPreferencesPage = new UserPreferencesPage(page); + + await test.step('Navigate to admin perspective', async () => { + await page.goto('/'); + await perspectivePage.switchToAdministrator(); + }); + + await test.step('Set perspective preference to Developer', async () => { + await userPreferencesPage.openUserPreferences(); + await userPreferencesPage.changePreferenceDropdown('Perspective', 'Developer'); + }); + + await test.step('Reload and verify developer perspective', async () => { + await page.goto('/'); + await page.waitForLoadState('domcontentloaded'); + await userPreferencesPage.expectPerspective('Developer'); + }); + + await test.step('Reset preference', async () => { + await userPreferencesPage.openUserPreferences(); + await userPreferencesPage.changePreferenceDropdown('Perspective', 'Last viewed'); + }); + }, + ); + + test('setting a preference for a project', { tag: ['@regression'] }, async () => { + test.skip(true, 'Marked @broken-test in Cypress'); + }); + + test('creating project with project preference', { tag: ['@regression'] }, async () => { + test.skip(true, 'Marked @broken-test in Cypress'); + }); + + test('setting graph preference for topology', { tag: ['@regression'] }, async ({ page }) => { + const perspectivePage = new PerspectivePage(page); + const userPreferencesPage = new UserPreferencesPage(page); + + await test.step('Create deployment workload', async () => { + await k8s.appsV1Api.createNamespacedDeployment({ + namespace, + body: { + apiVersion: 'apps/v1', + kind: 'Deployment', + metadata: { name: 'node1', namespace }, + spec: { + replicas: 1, + selector: { matchLabels: { app: 'node1' } }, + template: { + metadata: { labels: { app: 'node1' } }, + spec: { + containers: [ + { + name: 'node1', + image: 'registry.access.redhat.com/ubi8/nodejs-18:latest', + ports: [{ containerPort: 8080, protocol: 'TCP' }], + }, + ], + }, + }, + }, + }, + }); + }); + + await test.step('Set topology preference to Graph', async () => { + await page.goto('/'); + await perspectivePage.switchToAdministrator(); + await userPreferencesPage.openUserPreferences(); + await userPreferencesPage.changePreferenceDropdown('Topology', 'Graph'); + }); + + await test.step('Reload and navigate to topology', async () => { + await userPreferencesPage.reloadConsole(); + await perspectivePage.switchToDeveloper(); + await perspectivePage.selectOrCreateProject(namespace); + await perspectivePage.navigateToTopology(); + }); + + await test.step('Verify graph view', async () => { + await userPreferencesPage.expectTopologyGraphView(); + }); + }); + + test('setting list preference for topology', { tag: ['@regression'] }, async () => { + test.skip(true, 'Marked @broken-test in Cypress — bugzilla 2014313'); + }); + + test( + 'setting form preference for create/edit resource method', + { tag: ['@regression'] }, + async () => { + test.skip(true, 'Marked @broken-test in Cypress'); + }, + ); + + test( + 'setting YAML preference for create/edit resource method', + { tag: ['@regression'] }, + async ({ page }) => { + const perspectivePage = new PerspectivePage(page); + const userPreferencesPage = new UserPreferencesPage(page); + + await test.step('Set create/edit preference to YAML', async () => { + await page.goto('/'); + await perspectivePage.switchToAdministrator(); + await userPreferencesPage.openUserPreferences(); + await userPreferencesPage.changePreferenceDropdown('Create/Edit resource method', 'YAML'); + }); + + await test.step('Navigate to Helm charts and install', async () => { + await perspectivePage.switchToDeveloper(); + await perspectivePage.selectOrCreateProject(namespace); + await perspectivePage.navigateToAdd(); + const helmCard = page.getByTestId('card helm'); + await helmCard.click(); + const searchInput = page.locator('[data-test="search-catalog"]'); + await searchInput.fill('Nodejs'); + const helmChartCard = page + .locator('[data-test^="HelmChart"]') + .filter({ hasText: 'Nodejs' }) + .first(); + await helmChartCard.click(); + const installButton = page.locator('[data-test="install-helm"]'); + await installButton.click(); + }); + + await test.step('Verify YAML view selected', async () => { + await userPreferencesPage.expectYamlViewSelected(); + }); + }, + ); + + test('setting a preference for language', { tag: ['@regression'] }, async () => { + test.skip(true, 'Marked @broken-test in Cypress'); + }); + + test('setting routing options preference for import form', { tag: ['@regression'] }, async () => { + test.skip(true, 'Marked @broken-test in Cypress — ODC-6303'); + }); + + test('setting theme preference for console', { tag: ['@regression'] }, async () => { + test.skip(true, 'Manual test — ODC-5990'); + }); + + test( + 'setting resource type preference for console', + { tag: ['@regression'] }, + async ({ page }) => { + const perspectivePage = new PerspectivePage(page); + const userPreferencesPage = new UserPreferencesPage(page); + + await test.step('Set resource type to DeploymentConfig', async () => { + await page.goto('/'); + await perspectivePage.switchToAdministrator(); + await userPreferencesPage.openUserPreferences(); + await userPreferencesPage.clickTab('Applications'); + await userPreferencesPage.changePreferenceDropdown('Resource Type', 'DeploymentConfig'); + }); + + await test.step('Navigate to Container images', async () => { + await perspectivePage.switchToDeveloper(); + await perspectivePage.selectOrCreateProject(namespace); + await perspectivePage.navigateToAdd(); + const containerImageCard = page.getByTestId('card container-image'); + await containerImageCard.click(); + }); + + await test.step('Verify DeploymentConfig is selected', async () => { + const resourceDropdown = page.locator('[data-test-id="dropdown-button"]').filter({ + hasText: 'DeploymentConfig', + }); + await expect(resourceDropdown.first()).toBeVisible({ timeout: 30_000 }); + }); + }, + ); +});