diff --git a/frontend/e2e/clients/kubernetes-client.ts b/frontend/e2e/clients/kubernetes-client.ts index 9338645e53f..556a25ec391 100644 --- a/frontend/e2e/clients/kubernetes-client.ts +++ b/frontend/e2e/clients/kubernetes-client.ts @@ -377,9 +377,8 @@ export default class KubernetesClient { await this.k8sApi.patchNamespacedConfigMap({ name, namespace, - body: { data: mergedData }, - contentType: k8s.PatchStrategy.MergePatch, - } as any); + body: [{ op: 'replace', path: '/data', value: mergedData }], + }); } async createConfigMap( diff --git a/frontend/e2e/pages/base-page.ts b/frontend/e2e/pages/base-page.ts index 21c86aa2098..b0e77965b4f 100644 --- a/frontend/e2e/pages/base-page.ts +++ b/frontend/e2e/pages/base-page.ts @@ -1,4 +1,6 @@ -import { type Locator, type Page, expect } from '@playwright/test'; +import type { Locator, Page } from '@playwright/test'; + +import { expect } from '../fixtures'; export async function getEditorContent(page: Page): Promise { await page.waitForFunction( @@ -166,14 +168,14 @@ export default abstract class BasePage { Administrator: ['Administrator', 'Core platform'], Developer: ['Developer'], }; - const toggle = this.page.locator('[data-test-id="perspective-switcher-toggle"]'); + const toggle = this.page.getByTestId('perspective-switcher-toggle'); const labels = labelMap[target] || [target]; const currentText = (await toggle.textContent()) || ''; if (labels.some((label) => currentText.includes(label))) { return; } await this.robustClick(toggle); - const menuOption = this.page.locator('[data-test-id="perspective-switcher-menu-option"]'); + const menuOption = this.page.getByTestId('perspective-switcher-menu-option'); for (const label of labels) { const option = menuOption.filter({ hasText: label }); if ((await option.count()) > 0) { diff --git a/frontend/e2e/pages/dev-console/add-page.ts b/frontend/e2e/pages/dev-console/add-page.ts new file mode 100644 index 00000000000..b692219c1e6 --- /dev/null +++ b/frontend/e2e/pages/dev-console/add-page.ts @@ -0,0 +1,519 @@ +import type { Locator, Page } from '@playwright/test'; + +import { expect } from '../../fixtures'; +import BasePage, { warmupSPA } from '../base-page'; + +export class AddPage extends BasePage { + private readonly pageHeading: Locator = this.page.getByTestId('page-heading'); + private readonly gettingStartedResources: Locator = this.page.getByTestId('getting-started'); + private readonly detailsSwitch: Locator = this.page.getByTestId('details-switch'); + + async navigateToAdd(namespace: string): Promise { + await this.goTo(`/add/ns/${namespace}`); + await expect(this.pageHeading).toBeVisible({ timeout: 60_000 }); + } + + async switchToDeveloper(): Promise { + await warmupSPA(this.page); + await this.switchPerspective('Developer'); + } + + async ensureDevPerspectiveAndNavigate(namespace: string): Promise { + await warmupSPA(this.page); + await this.switchPerspective('Developer'); + await this.navigateToAdd(namespace); + } + + getCard(cardId: string): Locator { + return this.page.getByTestId(`card ${cardId}`); + } + + getCardItem(itemId: string): Locator { + return this.page.getByTestId(`item ${itemId}`); + } + + getGettingStartedResources(): Locator { + return this.gettingStartedResources; + } + + getDetailsSwitch(): Locator { + return this.detailsSwitch; + } + + async clickDetailsSwitch(): Promise { + await this.robustClick(this.detailsSwitch.locator('input')); + } + + async clickShowGettingStartedResources(): Promise { + const link = this.page.getByTestId('restore-getting-started'); + // eslint-disable-next-line no-restricted-syntax + await link.waitFor({ state: 'visible', timeout: 30_000 }); + await this.robustClick(link); + } + + async hideGettingStarted(): Promise { + const closeButton = this.gettingStartedResources.getByRole('button', { name: 'Close' }); + await this.robustClick(closeButton); + } + + getQuickSearchInput(): Locator { + return this.page.getByRole('textbox', { name: 'Quick search bar' }); + } + + async clickAddToProject(): Promise { + await this.robustClick(this.page.getByTestId('quick-search')); + } + + async clickImportFromGit(): Promise { + await this.robustClick(this.getCardItem('import-from-git')); + } + + async clickContainerImage(): Promise { + await this.robustClick(this.getCardItem('deploy-image')); + } + + async clickDatabaseCard(): Promise { + await this.robustClick(this.getCardItem('dev-catalog-databases')); + } + + async clickImportYAML(): Promise { + await this.robustClick(this.getCardItem('import-yaml')); + } + + async clickSamples(): Promise { + await this.robustClick(this.getCardItem('import-from-samples')); + } + + async clickViewAllSamples(): Promise { + const viewAll = this.page.getByRole('link', { name: /view all samples/i }); + await this.robustClick(viewAll); + } + + getViewAllSamples(): Locator { + return this.page.getByRole('link', { name: /view all samples/i }); + } + + getSampleCard(name: string): Locator { + return this.page.getByTestId(`sample-${name}`); + } + + getPageHeading(): Locator { + return this.pageHeading; + } + + getAddCardHeading(name: string): Locator { + return this.page.getByTestId('add-cards').getByRole('heading', { name, exact: true }); + } + + getViewAllLink(): Locator { + return this.page.getByRole('link', { name: /view all/i }); + } + + getNoResultsMessage(): Locator { + return this.page.getByText('No results'); + } +} +export class ImportFromGitPage extends BasePage { + private readonly gitRepoUrlInput: Locator = this.page.getByRole('textbox', { + name: 'Git Repo URL', + }); + private readonly appNameInput: Locator = this.page.locator('[data-test-id="application-form-app-input"]'); + private readonly nameInput: Locator = this.page.locator('[data-test-id="application-form-app-name"]'); + private readonly createButton: Locator = this.page.getByRole('button', { name: 'Create', exact: true }); + private readonly cancelButton: Locator = this.page.getByRole('button', { name: 'Cancel', exact: true }); + + async navigateToImportFromGit(namespace: string): Promise { + await this.goTo(`/import/ns/${namespace}`); + await expect(this.gitRepoUrlInput).toBeVisible({ timeout: 60_000 }); + } + + async enterGitRepoURL(url: string): Promise { + await this.gitRepoUrlInput.fill(url); + } + + async waitForGitValidation(): Promise { + const validatedIndicator = this.page.getByTestId('git-url-validated'); + const errorIndicator = this.page.getByTestId('git-url-error'); + try { + // eslint-disable-next-line no-restricted-syntax + await validatedIndicator.or(errorIndicator).first().waitFor({ timeout: 30_000 }); + } catch { + // Validation may not always appear for non-standard git types + } + } + + async enterApplicationName(name: string): Promise { + await this.appNameInput.clear(); + await this.appNameInput.fill(name); + } + + async enterName(name: string): Promise { + await this.nameInput.clear(); + await this.nameInput.fill(name); + } + + async selectResourceType(type: string): Promise { + // Resource type changed from radio buttons to SingleDropdownField in OCP 5.0 + const toggle = this.page.locator('#form-select-input-resources-field'); + // eslint-disable-next-line no-restricted-syntax + await toggle.waitFor({ state: 'visible', timeout: 30_000 }); + await this.robustClick(toggle); + const option = this.page.getByRole('option', { name: type, exact: true }); + await this.robustClick(option); + } + + async clickCreate(): Promise { + await this.robustClick(this.createButton); + } + + async clickCancel(): Promise { + await this.robustClick(this.cancelButton); + } + + async uncheckCreateRoute(): Promise { + const routeCheckbox = this.page.getByRole('checkbox', { name: /create a route/i }); + if (await routeCheckbox.isChecked()) { + await routeCheckbox.uncheck(); + } + } + + async clickAdvancedOption(optionText: string): Promise { + const link = this.page.getByRole('button', { name: optionText }); + await this.robustClick(link); + } + + async enterWorkloadName(name: string): Promise { + await this.enterName(name); + } + + async ensureFormView(): Promise { + const formViewButton = this.page.getByTestId('form-view-input'); + if ((await formViewButton.count()) > 0) { + const isChecked = await formViewButton.isChecked(); + if (!isChecked) { + await this.robustClick(formViewButton); + } + } + } + + getDevfileStrategyDisabled(): Locator { + return this.page + .getByTestId('import-strategy-Devfile') + .and(this.page.locator('[aria-disabled="true"]')); + } + + async clickEditImportStrategy(): Promise { + const editBtn = this.page.getByRole('button', { name: /edit import strategy/i }); + await this.robustClick(editBtn); + } + + async selectBuilderImage(imageName: string): Promise { + const imageCard = this.page.getByTestId(`card ${imageName}`); + await this.robustClick(imageCard); + } + + async selectImportStrategyBuilderImage(): Promise { + const radio = this.page.getByTestId('import-strategy-Builder Image'); + await this.robustClick(radio); + } + + async enterDevfilePath(path: string): Promise { + const input = this.page.getByTestId('git-form-devfile-path-input'); + await input.clear(); + await input.fill(path); + } + + async selectGitType(gitType: string): Promise { + const dropdown = this.page.getByTestId('git-type-dropdown'); + await this.robustClick(dropdown); + const option = this.page.getByRole('option', { name: gitType }); + await this.robustClick(option); + } + + getAppNameInput(): Locator { + return this.appNameInput; + } + + getNameInput(): Locator { + return this.nameInput; + } + + getDevfileNotDetectedMessage(): Locator { + return this.page.getByText('Devfile not detected'); + } +} + +export class DeployImagePage extends BasePage { + private readonly imageNameInput: Locator = this.page.getByRole('textbox', { name: 'Image name' }); + private readonly nameInput: Locator = this.page.locator('[data-test-id="application-form-app-name"]'); + private readonly appNameInput: Locator = this.page.locator('[data-test-id="application-form-app-input"]'); + private readonly createButton: Locator = this.page.getByRole('button', { name: 'Create', exact: true }); + private readonly cancelButton: Locator = this.page.getByRole('button', { name: 'Cancel', exact: true }); + private readonly saveButton: Locator = this.page.getByRole('button', { name: 'Create', exact: true }); + + async navigateToDeployImage(namespace: string): Promise { + await this.goTo(`/deploy-image/ns/${namespace}`); + await expect(this.imageNameInput.or(this.page.getByTestId('internal-view-input')).first()).toBeVisible({ timeout: 60_000 }); + } + + async enterExternalRegistryImage(imageName: string): Promise { + const externalRadio = this.page.getByTestId('image-name-radio'); + if ((await externalRadio.count()) > 0) { + await this.robustClick(externalRadio); + } + await this.imageNameInput.fill(imageName); + // Wait for image lookup to complete by checking that the name field auto-populates + await expect(this.nameInput).not.toHaveValue('', { timeout: 30_000 }); + } + + async selectImageStreamTag(): Promise { + const radio = this.page.getByTestId('internal-view-input'); + await this.robustClick(radio); + } + + // Image stream dropdowns all share data-test="console-select-menu-toggle" + // so Formik-generated IDs are the only way to distinguish them + async selectProject(projectName: string): Promise { + const dropdown = this.page.locator('#form-ns-dropdown-imageStream-namespace-field'); + await this.robustClick(dropdown); + const option = this.page.getByRole('option', { name: projectName, exact: true }); + await this.robustClick(option); + } + + async selectImageStream(streamName: string): Promise { + const dropdown = this.page.locator('#form-ns-dropdown-imageStream-image-field'); + await this.robustClick(dropdown); + const option = this.page.getByRole('option', { name: streamName, exact: true }); + await this.robustClick(option); + } + + async selectTag(tag: string): Promise { + const dropdown = this.page.locator('#form-dropdown-imageStream-tag-field'); + await this.robustClick(dropdown); + const option = this.page.getByRole('option', { name: tag }); + await this.robustClick(option); + } + + async selectRuntimeIcon(iconName: string): Promise { + // Scope to the FormGroup with fieldId="runtimeIcon" to avoid matching other ConsoleSelect toggles + const iconSection = this.page.locator('.odc-icon-dropdown'); + // eslint-disable-next-line no-restricted-syntax + await iconSection.waitFor({ state: 'visible', timeout: 30_000 }); + await this.robustClick(iconSection.getByTestId('console-select-menu-toggle')); + const option = this.page.getByRole('option', { name: iconName }); + await this.robustClick(option); + } + + async selectApplication(appName: string): Promise { + const dropdown = this.page.getByTestId('application-form-app-dropdown'); + await this.robustClick(dropdown); + const option = this.page.getByRole('option', { name: appName }); + await this.robustClick(option); + } + + async enterName(name: string): Promise { + await this.nameInput.clear(); + await this.nameInput.fill(name); + } + + async enterApplicationName(name: string): Promise { + await this.appNameInput.clear(); + await this.appNameInput.fill(name); + } + + async selectResourceType(type: string): Promise { + // Resource type changed from radio buttons to SingleDropdownField in OCP 5.0 + const toggle = this.page.locator('#form-select-input-resources-field'); + // eslint-disable-next-line no-restricted-syntax + await toggle.waitFor({ state: 'visible', timeout: 30_000 }); + await this.robustClick(toggle); + const option = this.page.getByRole('option', { name: type, exact: true }); + await this.robustClick(option); + } + + async clickCreate(): Promise { + await this.robustClick(this.createButton); + } + + async clickCancel(): Promise { + await this.robustClick(this.cancelButton); + } + + async clickSave(): Promise { + await this.robustClick(this.saveButton); + } + + getAppNameInput(): Locator { + return this.appNameInput; + } + + getNameInput(): Locator { + return this.nameInput; + } +} + +export class SoftwareCatalogPage extends BasePage { + private readonly pageHeading: Locator = this.page.getByTestId('page-heading'); + private readonly filterInput: Locator = this.page.getByPlaceholder('Filter by keyword'); + + async navigateToCatalog(namespace: string): Promise { + await this.goTo(`/catalog/ns/${namespace}`); + await expect(this.pageHeading).toBeVisible({ timeout: 60_000 }); + } + + async navigateToAllNamespacesCatalog(): Promise { + await this.goTo('/catalog/all-namespaces'); + await expect(this.pageHeading).toBeVisible({ timeout: 60_000 }); + } + + async navigateToTemplates(namespace: string): Promise { + await this.goTo(`/catalog/ns/${namespace}?catalogType=Template`); + await expect(this.pageHeading).toBeVisible({ timeout: 60_000 }); + } + + async selectTypeOption(typeName: string): Promise { + const typeFilter = this.page.getByTestId(`catalog-${typeName}`); + if ((await typeFilter.count()) > 0) { + await this.robustClick(typeFilter); + return; + } + // data-test-group-item: legacy attr from CatalogServiceProvider (no React source to add data-test) + const checkbox = this.page.locator(`[data-test-group-item="${typeName}"]`); + if ((await checkbox.count()) > 0) { + await this.robustClick(checkbox); + return; + } + // Final fallback: text-based + const link = this.page.getByRole('link', { name: typeName }); + await this.robustClick(link); + } + + async selectTemplateCategory(category: string): Promise { + const categoryFilter = this.page.getByTestId(`category-${category}`); + if ((await categoryFilter.count()) > 0) { + await this.robustClick(categoryFilter); + return; + } + const categoryLink = this.page.getByRole('link', { name: category, exact: true }); + await this.robustClick(categoryLink); + } + + async searchAndSelectCard(cardName: string): Promise { + await this.filterInput.fill(cardName); + // Wait for filtered results to render + const card = this.page.getByTestId(`catalog-tile-${cardName}`); + // co-catalog-tile: Console's catalog tile class from CatalogTile.tsx + const fallbackCard = this.page.locator('.co-catalog-tile').filter({ hasText: cardName }); + const anyResult = card.or(fallbackCard.first()); + await expect(anyResult).toBeVisible({ timeout: 10_000 }); + if ((await card.count()) > 0) { + await this.robustClick(card); + return; + } + await this.robustClick(fallbackCard.first()); + } + + async clickInstantiateTemplate(): Promise { + const button = this.page.getByTestId('catalog-details-modal-cta'); + await this.robustClick(button); + } + + async clickCreateApplicationButton(): Promise { + const button = this.page.getByRole('link', { name: /create application/i }); + await this.robustClick(button); + } + + async filterByKeyword(keyword: string): Promise { + await this.filterInput.fill(keyword); + } + + getPageHeading(): Locator { + return this.pageHeading; + } + + getFilterInput(): Locator { + return this.filterInput; + } + + getHelpText(text: string): Locator { + return this.page.getByText(text); + } + + getCatalogTiles(): Locator { + // co-catalog-tile: Console's catalog tile class from CatalogTile.tsx + return this.page.locator('.co-catalog-tile'); + } + + getFormSubmitButton(): Locator { + return this.page.getByRole('button', { name: 'Create', exact: true }); + } + + getProjectSelectionMessage(): Locator { + return this.page.getByText('Select a Project to view the software catalog'); + } +} + +export class ImportYAMLPage extends BasePage { + private readonly submitButton: Locator = this.page.getByRole('button', { name: 'Create', exact: true }); + private readonly cancelButton: Locator = this.page.getByRole('button', { name: 'Cancel', exact: true }); + + getSubmitButton(): Locator { + return this.submitButton; + } + + getCancelButton(): Locator { + return this.cancelButton; + } + + async clickCreate(): Promise { + await this.robustClick(this.submitButton); + } + + async clickCancel(): Promise { + await this.robustClick(this.cancelButton); + } +} + +export class TopologyPage extends BasePage { + async navigateToTopology(namespace: string): Promise { + await this.goTo(`/topology/ns/${namespace}`); + await this.waitForLoadingComplete(30_000); + } + + getWorkload(name: string): Locator { + return this.page.locator(`[data-id="${name}"] text`).first().or( + this.page.locator('.pf-topology-content').getByText(name, { exact: true }), + ); + } + + async clickWorkload(name: string): Promise { + const workload = this.getWorkload(name); + // eslint-disable-next-line no-restricted-syntax + await workload.waitFor({ state: 'visible', timeout: 60_000 }); + // Dismiss any open sidebar that may intercept clicks on topology nodes + await this.page.keyboard.press('Escape'); + await this.robustClick(workload); + } + + getSidebar(): Locator { + return this.page.getByTestId('topology-sidebar'); + } + + getSidebarTitle(): Locator { + return this.page.getByTestId('resource-title'); + } + + async waitForWorkload(name: string, timeoutMs = 120_000): Promise { + await this.page.waitForURL(/\/topology\//, { timeout: timeoutMs }); + const workload = this.getWorkload(name); + // eslint-disable-next-line no-restricted-syntax + await workload.waitFor({ state: 'visible', timeout: timeoutMs }); + } + + async switchToListView(): Promise { + const listViewBtn = this.page.getByTestId('topology-list-view'); + if ((await listViewBtn.count()) > 0) { + await this.robustClick(listViewBtn); + } + } +} diff --git a/frontend/e2e/tests/dev-console/catalog.spec.ts b/frontend/e2e/tests/dev-console/catalog.spec.ts new file mode 100644 index 00000000000..7ca5da06000 --- /dev/null +++ b/frontend/e2e/tests/dev-console/catalog.spec.ts @@ -0,0 +1,180 @@ +import { test, expect } from '../../fixtures'; +import { warmupSPA } from '../../pages/base-page'; +import { + AddPage, + SoftwareCatalogPage, + TopologyPage, +} from '../../pages/dev-console/add-page'; + +/** + * Migrated from: + * frontend/packages/dev-console/integration-tests/features/addFlow/create-from-catalog.feature + * frontend/packages/dev-console/integration-tests/features/addFlow/create-from-database.feature + * frontend/packages/dev-console/integration-tests/features/addFlow/catalog-all-namespaces.feature + * frontend/packages/dev-console/integration-tests/features/addFlow/software-catalog-details.feature + * + * Skipped scenarios: + * - A-09-TC07 (@manual) - Helm Charts Repositories + * - A-09-TC08 (@manual) - Software Catalog Customization - Add empty Categories + * - A-09-TC09 (@manual) - Software Catalog - Categories under Schema tab + * - A-09-TC10 (@manual) - Software Catalog Customization - Edit Categories + * - A-09-TC011 (@manual) - Devfiles on Software Catalog + */ + +test.describe( + 'Create Application from Catalog', + { tag: ['@dev-console', '@smoke'] }, + () => { + const ns = `aut-addflow-catalog-${Date.now()}`; + let addPage: AddPage; + let catalogPage: SoftwareCatalogPage; + let topologyPage: TopologyPage; + + test.beforeEach(async ({ page, k8sClient, cleanup }) => { + addPage = new AddPage(page); + catalogPage = new SoftwareCatalogPage(page); + topologyPage = new TopologyPage(page); + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + await warmupSPA(page); + }); + + test('deploy application using Catalog Template - MariaDB [A-01-TC02]', async () => { + test.slow(); + + await test.step('Navigate to Templates in Software Catalog', async () => { + await catalogPage.navigateToTemplates(ns); + }); + + await test.step('Select Databases category and MariaDB template', async () => { + await catalogPage.selectTemplateCategory('Databases'); + await catalogPage.searchAndSelectCard('MariaDB'); + }); + + await test.step('Instantiate template', async () => { + await catalogPage.clickInstantiateTemplate(); + await catalogPage.getFormSubmitButton().click(); + }); + + await test.step('Verify workload in topology', async () => { + await topologyPage.waitForWorkload('mariadb'); + await expect(topologyPage.getWorkload('mariadb')).toBeVisible(); + }); + }); + }, +); + +test.describe( + 'Create Database from Add page', + { tag: ['@dev-console', '@smoke'] }, + () => { + const ns = `aut-addflow-database-${Date.now()}`; + let addPage: AddPage; + let catalogPage: SoftwareCatalogPage; + let topologyPage: TopologyPage; + + test.beforeEach(async ({ page, k8sClient, cleanup }) => { + addPage = new AddPage(page); + catalogPage = new SoftwareCatalogPage(page); + topologyPage = new TopologyPage(page); + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + await addPage.ensureDevPerspectiveAndNavigate(ns); + }); + + test('create Database from Add page - MariaDB [A-03-TC01]', async () => { + test.slow(); + + await test.step('Click Database card', async () => { + await addPage.clickDatabaseCard(); + }); + + await test.step('Select MariaDB and instantiate', async () => { + await catalogPage.searchAndSelectCard('MariaDB'); + await catalogPage.clickInstantiateTemplate(); + await catalogPage.getFormSubmitButton().click(); + }); + + await test.step('Verify workload in topology', async () => { + await topologyPage.waitForWorkload('mariadb'); + await expect(topologyPage.getWorkload('mariadb')).toBeVisible(); + }); + }); + }, +); + +test.describe( + 'Software Catalog with All Namespaces', + { tag: ['@dev-console', '@regression'] }, + () => { + let catalogPage: SoftwareCatalogPage; + + test.beforeEach(async ({ page }) => { + catalogPage = new SoftwareCatalogPage(page); + await warmupSPA(page); + }); + + test('forces project selection when all namespaces URL is accessed [A-12-TC01]', async () => { + await test.step('Navigate to catalog all namespaces', async () => { + await catalogPage.navigateToAllNamespacesCatalog(); + }); + + await test.step('Verify project selection message', async () => { + await expect(catalogPage.getPageHeading()).toBeVisible(); + await expect(catalogPage.getProjectSelectionMessage()).toBeVisible(); + }); + }); + + test('shows catalog items when specific project is selected [A-12-TC02]', async ({ + k8sClient, + cleanup, + }) => { + const ns = `aut-catalog-namespaces-${Date.now()}`; + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + + await test.step('Navigate to catalog with namespace', async () => { + await catalogPage.navigateToCatalog(ns); + }); + + await test.step('Verify catalog items are visible', async () => { + await expect(catalogPage.getPageHeading()).toBeVisible(); + const tiles = catalogPage.getCatalogTiles(); + await expect(tiles.first()).toBeVisible({ timeout: 30_000 }); + }); + }); + }, +); + +test.describe( + 'Software Catalog Page details', + { tag: ['@dev-console', '@regression'] }, + () => { + const ns = `aut-catalog-pagedetails-${Date.now()}`; + let catalogPage: SoftwareCatalogPage; + + test.beforeEach(async ({ page, k8sClient, cleanup }) => { + catalogPage = new SoftwareCatalogPage(page); + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + await warmupSPA(page); + await catalogPage.navigateToCatalog(ns); + }); + + test('Software Catalog page default view [A-09-TC01]', async () => { + await expect(catalogPage.getFilterInput()).toBeVisible(); + }); + + test('Helm Charts on Software Catalog [A-09-TC06]', async () => { + await test.step('Click on Helm Charts type', async () => { + await catalogPage.selectTypeOption('Helm Charts'); + }); + + await test.step('Verify Helm Charts are displayed', async () => { + const tiles = catalogPage.getCatalogTiles(); + await expect(tiles.first()).toBeVisible({ timeout: 30_000 }); + await expect(catalogPage.getFilterInput()).toBeVisible(); + }); + }); + }, +); diff --git a/frontend/e2e/tests/dev-console/container-image.spec.ts b/frontend/e2e/tests/dev-console/container-image.spec.ts new file mode 100644 index 00000000000..e594bd3df7e --- /dev/null +++ b/frontend/e2e/tests/dev-console/container-image.spec.ts @@ -0,0 +1,115 @@ +import { test, expect } from '../../fixtures'; +import { + AddPage, + DeployImagePage, + TopologyPage, +} from '../../pages/dev-console/add-page'; + +/** + * Migrated from: + * frontend/packages/dev-console/integration-tests/features/addFlow/create-from-container-image.feature + * + * All automatable scenarios are included. + */ + +test.describe( + 'Create Application from Container image', + { tag: ['@dev-console', '@regression'] }, + () => { + const ns = `aut-addflow-containerimg-${Date.now()}`; + let addPage: AddPage; + let deployPage: DeployImagePage; + let topologyPage: TopologyPage; + + test.beforeEach(async ({ page, k8sClient, cleanup }) => { + addPage = new AddPage(page); + deployPage = new DeployImagePage(page); + topologyPage = new TopologyPage(page); + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + await addPage.ensureDevPerspectiveAndNavigate(ns); + }); + + test('deploy image page details on entering external registry image [A-02-TC01]', async () => { + await test.step('Navigate to Deploy Image page', async () => { + await addPage.clickContainerImage(); + }); + + await test.step('Enter external registry image', async () => { + await deployPage.enterExternalRegistryImage('openshift/hello-openshift'); + }); + + await test.step('Verify auto-populated fields', async () => { + await expect(deployPage.getAppNameInput()).toHaveValue('hello-openshift-app', { + timeout: 30_000, + }); + await expect(deployPage.getNameInput()).toHaveValue('hello-openshift'); + }); + }); + + test('cancel operation on Container image form [A-02-TC04]', async () => { + await test.step('Navigate to Deploy Image page', async () => { + await addPage.clickContainerImage(); + }); + + await test.step('Select internal registry and cancel', async () => { + await deployPage.selectImageStreamTag(); + await deployPage.selectProject('openshift'); + await deployPage.selectImageStream('golang'); + await deployPage.selectTag('latest'); + await deployPage.clickCancel(); + }); + + await test.step('Verify redirect to Add page', async () => { + await expect(addPage.getPageHeading()).toBeVisible(); + }); + }); + }, +); + +test.describe( + 'Deploy image from internal registry', + { tag: ['@dev-console', '@smoke'] }, + () => { + const ns = `aut-addflow-deploy-int-${Date.now()}`; + let addPage: AddPage; + let deployPage: DeployImagePage; + let topologyPage: TopologyPage; + + test.beforeEach(async ({ page, k8sClient, cleanup }) => { + addPage = new AddPage(page); + deployPage = new DeployImagePage(page); + topologyPage = new TopologyPage(page); + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + await addPage.ensureDevPerspectiveAndNavigate(ns); + }); + + test('deploy image from internal registry with Runtime icon [A-02-TC03]', async () => { + test.slow(); + + await test.step('Navigate to Deploy Image page', async () => { + await addPage.clickContainerImage(); + }); + + await test.step('Select internal registry image stream', async () => { + await deployPage.selectImageStreamTag(); + await deployPage.selectProject('openshift'); + await deployPage.selectImageStream('golang'); + await deployPage.selectTag('latest'); + }); + + await test.step('Configure and create deployment', async () => { + await deployPage.selectRuntimeIcon('fedora'); + await deployPage.enterName('hello-internal'); + await deployPage.selectResourceType('deployment'); + await deployPage.clickCreate(); + }); + + await test.step('Verify workload in topology', async () => { + await topologyPage.waitForWorkload('hello-internal'); + await expect(topologyPage.getWorkload('hello-internal')).toBeVisible(); + }); + }); + }, +); diff --git a/frontend/e2e/tests/dev-console/create-from-yaml.spec.ts b/frontend/e2e/tests/dev-console/create-from-yaml.spec.ts new file mode 100644 index 00000000000..627d7c1a6d7 --- /dev/null +++ b/frontend/e2e/tests/dev-console/create-from-yaml.spec.ts @@ -0,0 +1,101 @@ +import { test, expect } from '../../fixtures'; +import { warmupSPA } from '../../pages/base-page'; +import { AddPage, ImportYAMLPage, TopologyPage } from '../../pages/dev-console/add-page'; + +/** + * Migrated from: + * frontend/packages/dev-console/integration-tests/features/addFlow/create-from-yaml.feature + * + * All scenarios included. + */ + +const GIT_DC_YAML = `apiVersion: apps.openshift.io/v1 +kind: DeploymentConfig +metadata: + name: shell-app +spec: + replicas: 1 + selector: + app: shell-app + template: + metadata: + labels: + app: shell-app + spec: + containers: + - name: shell-app + image: centos:latest + command: + - /bin/sh + - '-c' + - >- + while true ; do date; sleep 1; done; + securityContext: + allowPrivilegeEscalation: false + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + capabilities: + drop: + - ALL +`; + +test.describe( + 'Create Application from YAML file', + { tag: ['@dev-console', '@smoke'] }, + () => { + const ns = `aut-addflow-yaml-${Date.now()}`; + let addPage: AddPage; + let yamlPage: ImportYAMLPage; + let topologyPage: TopologyPage; + + test.beforeEach(async ({ page, k8sClient, cleanup }) => { + addPage = new AddPage(page); + yamlPage = new ImportYAMLPage(page); + topologyPage = new TopologyPage(page); + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + await warmupSPA(page); + await addPage.ensureDevPerspectiveAndNavigate(ns); + }); + + test('create a workload from YAML file [A-07-TC01]', async ({ page }) => { + test.slow(); + + await test.step('Navigate to Import YAML page', async () => { + await addPage.clickImportYAML(); + }); + + await test.step('Enter YAML content and create', async () => { + await page.waitForFunction( + () => !!(window as any).monaco?.editor?.getModels()?.[0], + { timeout: 30_000 }, + ); + await page.evaluate((yaml) => { + (window as any).monaco.editor.getModels()[0].setValue(yaml); + }, GIT_DC_YAML); + await yamlPage.getSubmitButton().click(); + }); + + await test.step('Verify workload in topology', async () => { + await topologyPage.navigateToTopology(ns); + await topologyPage.waitForWorkload('shell-app'); + await expect(topologyPage.getWorkload('shell-app')).toBeVisible(); + }); + }); + + test('cancel operation on YAML file redirects to Add page [A-07-TC02]', async () => { + await test.step('Navigate to Import YAML page', async () => { + await addPage.clickImportYAML(); + }); + + await test.step('Click cancel', async () => { + await yamlPage.getCancelButton().click(); + }); + + await test.step('Verify redirect to Add page', async () => { + await expect(addPage.getPageHeading()).toBeVisible(); + }); + }); + }, +); diff --git a/frontend/e2e/tests/dev-console/import-from-dockerfile.spec.ts b/frontend/e2e/tests/dev-console/import-from-dockerfile.spec.ts new file mode 100644 index 00000000000..82566015873 --- /dev/null +++ b/frontend/e2e/tests/dev-console/import-from-dockerfile.spec.ts @@ -0,0 +1,99 @@ +import { test, expect } from '../../fixtures'; +import { + AddPage, + ImportFromGitPage, + TopologyPage, +} from '../../pages/dev-console/add-page'; + +/** + * Migrated from: + * frontend/packages/dev-console/integration-tests/features/addFlow/create-from-docker-file.feature + * + * Skipped scenarios: + * - A-05-TC02 (@manual) - Create workload from Docker file with resource type (git rate limit) + */ + +test.describe( + 'Create Application from Docker file', + { tag: ['@dev-console', '@regression'] }, + () => { + const ns = `aut-addflow-docker-${Date.now()}`; + let addPage: AddPage; + let gitPage: ImportFromGitPage; + let topologyPage: TopologyPage; + + test.beforeEach(async ({ page, k8sClient, cleanup }) => { + addPage = new AddPage(page); + gitPage = new ImportFromGitPage(page); + topologyPage = new TopologyPage(page); + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + await addPage.ensureDevPerspectiveAndNavigate(ns); + }); + + test('Dockerfile details after entering git repo url [A-05-TC01]', async () => { + await test.step('Navigate to Import from Git', async () => { + await addPage.clickImportFromGit(); + }); + + await test.step('Enter Dockerfile git URL', async () => { + await gitPage.enterGitRepoURL( + 'https://github.com/rohitkrai03/flask-dockerfile-example', + ); + await gitPage.waitForGitValidation(); + }); + + await test.step('Verify auto-detected values', async () => { + await expect(gitPage.getAppNameInput()).toHaveValue( + 'flask-dockerfile-example-app', + { timeout: 15_000 }, + ); + await expect(gitPage.getNameInput()).toHaveValue('flask-dockerfile-example'); + }); + }); + + test('cancel Dockerfile form redirects to Add page [A-05-TC03]', async () => { + test.slow(); + + await test.step('Navigate to Import from Git', async () => { + await addPage.clickImportFromGit(); + }); + + await test.step('Enter URL and cancel', async () => { + await gitPage.enterGitRepoURL( + 'https://github.com/rohitkrai03/flask-dockerfile-example', + ); + await gitPage.waitForGitValidation(); + await gitPage.selectResourceType('Deployment'); + await gitPage.clickCancel(); + }); + + await test.step('Verify redirect to Add page', async () => { + await expect(addPage.getPageHeading()).toBeVisible(); + }); + }); + + test('create workload from Dockerfile [A-05-TC04]', async () => { + test.slow(); + + await test.step('Navigate to Import from Git', async () => { + await addPage.clickImportFromGit(); + }); + + await test.step('Fill form with Dockerfile repo and create', async () => { + await gitPage.enterGitRepoURL( + 'https://github.com/rohitkrai03/flask-dockerfile-example', + ); + await gitPage.waitForGitValidation(); + await gitPage.enterName('dockerfile-5000'); + await gitPage.selectResourceType('Deployment'); + await gitPage.clickCreate(); + }); + + await test.step('Verify workload in topology', async () => { + await topologyPage.waitForWorkload('dockerfile-5000'); + await expect(topologyPage.getWorkload('dockerfile-5000')).toBeVisible(); + }); + }); + }, +); diff --git a/frontend/packages/dev-console/integration-tests/features/addFlow/catalog-all-namespaces.feature b/frontend/packages/dev-console/integration-tests/features/addFlow/catalog-all-namespaces.feature deleted file mode 100644 index eeb7c0b0171..00000000000 --- a/frontend/packages/dev-console/integration-tests/features/addFlow/catalog-all-namespaces.feature +++ /dev/null @@ -1,21 +0,0 @@ -@add-flow @dev-console -Feature: Software Catalog with All Namespaces in Dev Perspective - As a developer, when I visit the catalog with all namespaces selected, I should see the project selection page instead of the catalog - - Background: - Given user is at developer perspective - - @regression - Scenario: Software Catalog forces project selection when all namespaces URL is accessed: A-12-TC01 - When user navigates to catalog all namespaces page - Then user will see "Software Catalog" page heading - And user will see "Select a Project to view the software catalog" help text - And user will not see catalog tiles or items - - @regression - Scenario: Software Catalog shows catalog items when specific project is selected: A-12-TC02 - Given user has created or selected namespace "aut-catalog-namespaces" - And user is at Software Catalog page - Then user will see "Software Catalog" page heading - And user will see catalog tiles or items - And user will not see "Select a Project to view the software catalog" help text diff --git a/frontend/packages/dev-console/integration-tests/features/addFlow/create-from-catalog.feature b/frontend/packages/dev-console/integration-tests/features/addFlow/create-from-catalog.feature deleted file mode 100644 index 185a339bdf4..00000000000 --- a/frontend/packages/dev-console/integration-tests/features/addFlow/create-from-catalog.feature +++ /dev/null @@ -1,43 +0,0 @@ -@add-flow @dev-console -Feature: Create Application from Catalog file - As a user, I want to create the application, component or service from Add Flow Catalog file - - Background: - Given user is at developer perspective - And user has created or selected namespace "aut-addflow-catalog" - And user is at Add page - - - @regression - Scenario: Create the workload using Builder Image: A-01-TC01 - Given user is at Software Catalog page - When user selects "Builder Image" option from Type section - And user searches and selects Builder Image card "Node.js" from catalog page - And user clicks Create Application button on side bar - And user enters Git Repo url in s2i builder image page as "https://github.com/sclorg/nodejs-ex.git" - And user selects resource type as "Deployment" - And user enters Application name as "builder-app" - And user enters workload name as "builder" - And user clicks create button - Then user will be redirected to Topology page - And user is able to see workload "builder" in topology page - - - @smoke - Scenario Outline: Deploy Application using Catalog Template "": A-01-TC02 - Given user is at Software Catalog page - And user is at Templates page - When user selects Template category "" - And user searches and selects Template card "" from catalog page - And user clicks Instantiate Template button on side bar - And user clicks create button on Instantiate Template page - Then user will be redirected to Topology page - And user is able to see workload "" in topology page - - Examples: - | template_category | card_name | workload_name | - | CI/CD | Jenkins | jenkins | - | Databases | MariaDB | mariadb | - | Languages | Node.js + PostgreSQL (Ephemeral) | nodejs-postgresql-example | - | Middleware | Apache HTTP Server | httpd-example | - | Other | Nginx HTTP server and a reverse proxy | nginx-example | diff --git a/frontend/packages/dev-console/integration-tests/features/addFlow/create-from-container-image.feature b/frontend/packages/dev-console/integration-tests/features/addFlow/create-from-container-image.feature deleted file mode 100644 index dbb0900c40b..00000000000 --- a/frontend/packages/dev-console/integration-tests/features/addFlow/create-from-container-image.feature +++ /dev/null @@ -1,80 +0,0 @@ -@add-flow @dev-console -Feature: Create Application from Container image file - As a user, I want to create the application, component or service from Add Flow Container image - - Background: - Given user is at developer perspective - And user has created or selected namespace "aut-addflow-containerimage" - And user is at Add page - - - @regression - Scenario: Deploy image page details on entering external registry image name: A-02-TC01 - Given user is at Deploy Image page - When user enters Image name from external registry as "openshift/hello-openshift" - Then user can see the image name gets Validated - And Application name displays as "hello-openshift-app" - And Name displays as "hello-openshift" - And advanced option Create a route to the application is selected - - - @regression - Scenario Outline: Deploy image with Runtime icon from external registry: A-02-TC02 - Given user is at Deploy Image page - When user enters Image name from external registry as "" - And user selects the "" from Runtime Icon dropdown - And user selects the application "sample-app" from Application dropdown - And user enters Name as "" - And user selects resource type as "deployment" - And user clicks Create button on Add page - Then user will be redirected to Topology page - And user will see the deployed image "" with "" icon - - Examples: - | image | image_name | runtime_icon | name | - | secure | openshift/hello-openshift | fedora | hello-secure | - # | insecure | | ansible | hello-insecure | - - - @smoke - Scenario Outline: Deploy image with Runtime icon from internal registry: A-02-TC03 - Given user is at Deploy Image page - When user selects Image stream tag from internal registry - And user selects Project as "openshift" from internal registry - And user selects Image Stream as "" from internal registry - And user selects tag as "latest" from internal registry - And user selects the "" from Runtime Icon dropdown - And user selects the application "sample-app" from Application dropdown - And user enters Name as "" - And user selects resource type as "deployment" - And user clicks Create button on Add page - Then user will be redirected to Topology page - And user will see the deployed image "" with "" icon - - Examples: - | image_stream | runtime_icon | name | - | golang | fedora | hello-internal | - - - @regression - Scenario: Perform cancel operation on Container image form: A-02-TC04 - Given user is at Deploy Image page - When user selects Image stream tag from internal registry - And user selects Project as "openshift" from internal registry - And user selects Image Stream as "golang" from internal registry - And user selects tag as "latest" from internal registry - And user clicks Cancel button on Deploy Image page - Then user will be redirected to Add page - - - @regression - Scenario: Edit Runtime Icon while Editing Image: A-02-TC05 - Given user has deployed container Image "openshift/hello-openshift" from external registry - And user is at Topology page - And topology page has a deployed image "hello-openshift" with Runtime Icon "fedora" - When user right clicks on the node "hello-openshift" to open context menu - And user selects Edit imagename "hello-openshift" option - And user updates the Runtime icon to "ansible" - And user clicks on Save button - Then user will be redirected to Topology page - And user will see the deployment image "hello-openshift" icon updated to "ansible" Icon diff --git a/frontend/packages/dev-console/integration-tests/features/addFlow/create-from-database.feature b/frontend/packages/dev-console/integration-tests/features/addFlow/create-from-database.feature deleted file mode 100644 index 1618c7ddc46..00000000000 --- a/frontend/packages/dev-console/integration-tests/features/addFlow/create-from-database.feature +++ /dev/null @@ -1,18 +0,0 @@ -@add-flow @dev-console -Feature: Create Application from Database - As a user, I want to create the application, component or service from Database using Add Flow - - Background: - Given user is at developer perspective - And user has created or selected namespace "aut-addflow-database" - - - @smoke - Scenario: Create the Database from Add page: A-03-TC01 - Given user is at Add page - When user clicks Database card - And user selects "MariaDB" database on Software Catalog - And user clicks Instantiate Template button on side bar - And user clicks create button on Instantiate Template page - Then user will be redirected to Topology page - And user is able to see workload "mariadb" in topology page diff --git a/frontend/packages/dev-console/integration-tests/features/addFlow/create-from-docker-file.feature b/frontend/packages/dev-console/integration-tests/features/addFlow/create-from-docker-file.feature deleted file mode 100644 index aaf3022b369..00000000000 --- a/frontend/packages/dev-console/integration-tests/features/addFlow/create-from-docker-file.feature +++ /dev/null @@ -1,58 +0,0 @@ -@add-flow @dev-console -Feature: Create Application from Docker file - As a user, I want to create the application, component or service from Add Flow Docker file - - Background: - Given user is at developer perspective - And user has created or selected namespace "aut-addflow-docker" - And user is at Add page - - - @regression - Scenario: Dockerfile details after entering git repo url: A-05-TC01 - Given user is on Import from Git form - When user enters Git Repo URL as "https://github.com/rohitkrai03/flask-dockerfile-example" - Then git url "https://github.com/rohitkrai03/flask-dockerfile-example" gets Validated - And application name displays as "flask-dockerfile-example-app" - And name field auto populates with value "flask-dockerfile-example" in Import from Git form - - - # @smoke - # Marking this scenario as @manual, because due to git-rate limit issue, below scenarios are failing - # TODO: Use Cypress HTTP mocking to solve the github rate limiting issue. See - https://docs.cypress.io/guides/guides/network-requests - @regression @manual - Scenario Outline: Create a workload from Docker file with "" as resource type: A-05-TC02 - Given user is on Import from Git form - When user enters Git Repo URL as "https://github.com/rohitkrai03/flask-dockerfile-example" - And user enters Name as "" in Docker file page - And user selects "" in Resource type section - And user clicks Create button on Add page - Then user will be redirected to Topology page - And user is able to see workload "" in topology page - And user will see sidebar in topology page with title "" - - Examples: - | resource_type | name | - | Deployment | dockerfile | - | Deployment Config | dockerfile-1 | - - - @regression - Scenario: Performing cancel operation on Dockerfile form should redirected the user to Add page: A-05-TC03 - Given user is on Import from Git form - When user enters Git Repo URL as "https://github.com/rohitkrai03/flask-dockerfile-example" - And user selects "Deployment" in Resource type section - And user clicks Cancel button on Add page - Then user will be redirected to Add page - - - @regression @ODC-7614 - Scenario: Create workload from Dockerfile and verify the Exposed Port in the Target Port section: A-05-TC04 - Given user is on Import from Git form - When user enters Git Repo URL as "https://github.com/rohitkrai03/flask-dockerfile-example" - And user enters Name as "dockerfile-5000" in Docker file page - And user selects "Deployment" in Resource type section - And user selects "5000" in Target Port section - And user clicks Create button on Add page - Then user will be redirected to Topology page - And user is able to see workload "dockerfile-5000" in topology page diff --git a/frontend/packages/dev-console/integration-tests/features/addFlow/create-from-yaml.feature b/frontend/packages/dev-console/integration-tests/features/addFlow/create-from-yaml.feature deleted file mode 100644 index 5c899ed2478..00000000000 --- a/frontend/packages/dev-console/integration-tests/features/addFlow/create-from-yaml.feature +++ /dev/null @@ -1,24 +0,0 @@ -@add-flow @dev-console -Feature: Create Application from YAML file - As a user, I want to create the application, component or service from Yaml file using Add Flow - - Background: - Given user is at developer perspective - And user is at Add page - And user has created or selected namespace "aut-addflow-yaml" - - - @smoke - Scenario: Create a workload from YAML file: A-07-TC01 - Given user is at Import YAML page - When user enters the "testData/add-flow/git-dc.yaml" file data to YAML Editor - And user clicks create button on YAML page - And user navigates to Topology page - Then user is able to see workload "shell-app" in topology page - - - @regression - Scenario: Perform cancel operation on YAML file: A-07-TC02 - Given user is at Import YAML page - When user clicks on cancel button - Then user will be redirected to Add page diff --git a/frontend/packages/dev-console/integration-tests/features/addFlow/software-catalog-details.feature b/frontend/packages/dev-console/integration-tests/features/addFlow/software-catalog-details.feature deleted file mode 100644 index 650bf87b4eb..00000000000 --- a/frontend/packages/dev-console/integration-tests/features/addFlow/software-catalog-details.feature +++ /dev/null @@ -1,99 +0,0 @@ -@add-flow @dev-console -Feature: Software Catalog Page - As a user, I should be able to use Software Catalog page to deploy application - - - Background: - Given user is at admin perspective - And user is at Software Catalog page in admin page - And user has created or selected namespace "aut-catalog-pagedetails" - - - @regression - Scenario: Software Catalog page - Default view: A-09-TC01 - Then user will see All Items already selected - And user will see CICD, Databases, Languages, Middleware, Other categories - And user will see 'BuilderImage', 'Devfile', 'HelmChart', 'Template' types - And user will see Filter by Keyword field - And user will see A-Z, Z-A sort by dropdown - - - @regression - Scenario Outline: Filter the cards with type: A-09-TC02 - When user selects "" option from Type section - Then user is able to see cards related to "" - - Examples: - | type | - | Helm Charts | - | Builder Image | - | Template | - - - @smoke - Scenario: Helm Charts on default Software Catalog: A-09-TC06 - When user clicks on Helm Charts type - Then user will see the cards of Helm Charts - And user will see Filter by Keyword field - And user will see A-Z, Z-A sort by dropdown - - - @smoke @manual - Scenario: Helm Charts Repositories on Software Catalog: A-09-TC07 - Given user has added multiple helm charts repositories - When user clicks on Helm Charts type - Then user will see the list of Chart Repositories - And user will see the cards of Helm Charts - And user will see Filter by Keyword field - And user will see A-Z, Z-A sort by dropdown - - - @smoke @manual - Scenario: Software Catalog Customization - Add empty Categories: A-09-TC08 - Given user is at Search page - When user clicks on Resources dropdown - And user searches for Console - And user clicks on Console checkbox with "operator.openshift.io/v1" - And user clicks on cluster link - And user navigates to YAML tab - And user removes everything from spec.customization.developerCatalog.categories - And user enters "[]" in front of spec.customization.developerCatalog.categories - And user clicks on Save button - And user clicks on Reload button - And user navigates to Software Catalog page - Then user will not see Categories - - - @smoke @manual - Scenario: Software Catalog - Categories under Schema tab: A-09-TC09 - Given user is at cluster YAML tab - When user clicks on View sidebar - And user clicks on View Details under spec on the sidebar - And user clicks on View Details under customization on the sidebar - And user clicks on View Details under developerCatalog on the sidebar - And user clicks on View Details under categories on the sidebar - Then user will see categories which are shown in the software catalog - - - @smoke @manual - Scenario: Software Catalog Customization - Edit Categories: A-09-TC10 - Given user is at cluster YAML tab - And user has removed all the categories from Software Catalog page - And user has entered "[]" in front of spec.customization.developerCatalog.categories - When user clicks on View sidebar - And user clicks on Snippets on the sidebar - And user removes "[]" in front of spec.customization.developerCatalog.categories - And user clicks on Insert Snippet link on the sidebar - And user removes Languages Category - And user clicks on Save button - And user clicks on Reload button - Then user will see all the categories except Languages added under spec.customization.developerCatalog.categories - And user will see all the categories except Languages on Software Catalog page - - - @regression @manual - Scenario: Devfiles on Software Catalog: A-09-TC011 - When user clicks on Devfiles type - Then user will see the cards of Devfiles - And user will see Filter by Keyword field - And user will see A-Z, Z-A sort by dropdown