diff --git a/frontend/e2e/clients/kubernetes-client.ts b/frontend/e2e/clients/kubernetes-client.ts index a9255d43cf4..f4cfb2d807f 100644 --- a/frontend/e2e/clients/kubernetes-client.ts +++ b/frontend/e2e/clients/kubernetes-client.ts @@ -448,6 +448,101 @@ export default class KubernetesClient { return response; } + async createClusterCustomResource( + group: string, + version: string, + plural: string, + body: Record, + ): Promise { + return this.coApi.createClusterCustomObject({ body, group, plural, version }); + } + + async deleteClusterCustomResource( + group: string, + version: string, + plural: string, + name: string, + ): Promise { + try { + await this.coApi.deleteClusterCustomObject({ group, name, plural, version }); + } catch (err) { + if (!isNotFound(err)) { + throw err; + } + } + } + + async getClusterCustomResource( + group: string, + version: string, + plural: string, + name: string, + ): Promise { + return this.coApi.getClusterCustomObject({ group, name, plural, version }); + } + + async patchClusterCustomResource( + group: string, + version: string, + plural: string, + name: string, + patch: object, + ): Promise { + return this.coApi.patchClusterCustomObject({ + body: patch, + group, + name, + plural, + version, + contentType: k8s.PatchStrategy.MergePatch, + } as any); + } + + async createPVC(namespace: string, body: k8s.V1PersistentVolumeClaim): Promise { + return this.k8sApi.createNamespacedPersistentVolumeClaim({ namespace, body }); + } + + async deletePVC(name: string, namespace: string): Promise { + try { + await this.k8sApi.deleteNamespacedPersistentVolumeClaim({ name, namespace }); + } catch (err) { + if (!isNotFound(err)) { + throw err; + } + } + } + + async getPVC(name: string, namespace: string): Promise { + return this.k8sApi.readNamespacedPersistentVolumeClaim({ name, namespace }); + } + + async createDeployment(namespace: string, body: k8s.V1Deployment): Promise { + return this.appsApi.createNamespacedDeployment({ namespace, body }); + } + + async deleteDeployment(name: string, namespace: string): Promise { + try { + await this.appsApi.deleteNamespacedDeployment({ name, namespace }); + } catch (err) { + if (!isNotFound(err)) { + throw err; + } + } + } + + async patchDeployment( + name: string, + namespace: string, + patch: object, + ): Promise { + return this.appsApi.patchNamespacedDeployment({ + name, + namespace, + body: patch, + contentType: k8s.PatchStrategy.JsonPatch, + } as any); + } + async listCustomResources( group: string, version: string, diff --git a/frontend/e2e/mocks/storage.ts b/frontend/e2e/mocks/storage.ts new file mode 100644 index 00000000000..a8badf00a1f --- /dev/null +++ b/frontend/e2e/mocks/storage.ts @@ -0,0 +1,223 @@ +export const testerDeployment = { + apiVersion: 'apps/v1', + kind: 'Deployment', + metadata: { + name: 'busybox-deployment', + labels: { app: 'busybox' }, + }, + spec: { + replicas: 1, + strategy: { type: 'RollingUpdate' }, + selector: { matchLabels: { app: 'busybox' } }, + template: { + metadata: { labels: { app: 'busybox' } }, + spec: { + volumes: [ + { + name: 'testpvc', + persistentVolumeClaim: { claimName: 'testpvc' }, + }, + ], + containers: [ + { + name: 'busybox', + image: 'busybox', + imagePullPolicy: 'IfNotPresent', + volumeDevices: [{ name: 'testpvc', devicePath: '/data' }], + command: ['sh', '-c', 'echo Container 1 is Running ; sleep 3600'], + }, + ], + }, + }, + }, +}; + +export const PVC = { + apiVersion: 'v1', + kind: 'PersistentVolumeClaim', + metadata: { name: 'testpvc' }, + spec: { + storageClassName: 'gp2-csi', + accessModes: ['ReadWriteOnce'], + resources: { requests: { storage: '1Gi' } }, + }, +}; + +export const PVCGP3 = { + apiVersion: 'v1', + kind: 'PersistentVolumeClaim', + metadata: { name: 'testpvcgp3' }, + spec: { + storageClassName: 'gp3-csi', + accessModes: ['ReadWriteOnce'], + resources: { requests: { storage: '1Gi' } }, + }, +}; + +export const SnapshotClass = { + apiVersion: 'snapshot.storage.k8s.io/v1', + kind: 'VolumeSnapshotClass', + metadata: { name: 'csi-hostpath-snapclass' }, + driver: 'ebs.csi.aws.com', + deletionPolicy: 'Delete', +}; + +export const patchForVolume = { + op: 'add' as const, + path: '/spec/template/spec/volumes/-', + value: { + name: 'testpvc-snapshot-restore', + persistentVolumeClaim: { claimName: 'testpvc-snapshot-restore' }, + }, +}; + +// --- Storage Class provisioner fixtures --- + +export type Parameter = { + name: string; + id?: string; + values?: string | string[]; + hintText?: string; + nestedParameter?: Parameter; +}; + +export const provisionersMap: Record = { + 'kubernetes.io/aws-ebs': [ + { name: 'Type', id: 'type', values: ['io1', 'gp2', 'sc1', 'st1'] }, + { name: 'IOPS per GiB', id: 'iopsPerGB', values: '10' }, + { name: 'Filesystem type', id: 'fsType', values: 'ext4' }, + { + name: 'Encrypted', + id: 'encrypted', + nestedParameter: { name: 'KMS key ID', values: 'sample-kms-id' }, + }, + ], + 'kubernetes.io/cinder': [ + { name: 'Volume type', id: 'type', values: 'ext4' }, + { name: 'Availability zone', id: 'availability', values: 'lalitpur' }, + ], + 'kubernetes.io/azure-file': [ + { name: 'SKU name', id: 'skuName', hintText: 'Azure storage account SKU tier', values: 'sample-name' }, + { name: 'Location', id: 'location', hintText: 'Azure storage account name', values: 'bhaktapur' }, + { + name: 'Azure storage account name', + id: 'storageAccount', + hintText: 'Azure storage account name', + values: 'test-account', + }, + ], + 'kubernetes.io/azure-disk': [ + { name: 'Storage account type', id: 'storageaccounttype', hintText: 'Storage account type', values: 'tester' }, + { name: 'Account kind', id: 'kind', values: ['shared', 'dedicated', 'managed'] }, + ], + 'kubernetes.io/quobyte': [ + { name: 'Quobyte API server', id: 'quobyteAPIServer', values: 'test.xyzab' }, + { name: 'Registry address(es)', id: 'registry', values: 'xyz.abc' }, + { name: 'Admin secret name', id: 'adminSecretName', values: 'secret-admin' }, + { name: 'Admin secret namespace', id: 'adminSecretNamespace', values: 'secret-ns' }, + { name: 'User', id: 'user', values: 'admin' }, + { name: 'Quobyte configuration', id: 'quobyteConfig', values: 'config' }, + { name: 'Quobyte tenant', id: 'quobyteTenant', values: 'tester' }, + ], + 'kubernetes.io/vsphere-volume': [ + { name: 'Disk format', id: 'diskformat', values: ['thin', 'zeroed thick', 'eager zeroed thick'] }, + { name: 'Datastore', id: 'datastore', values: 'store-thin' }, + ], + 'kubernetes.io/portworx-volume': [ + { name: 'Filesystem', id: 'fs', values: ['none', 'xfs', 'ext4'] }, + { name: 'Block size', id: 'block_size', values: '1024' }, + { + name: 'Number of synchronous replicas to be provided in the form of replication factor', + id: 'repl', + values: '2', + }, + { name: 'I/O priority', id: 'io_priority', values: ['high', 'medium', 'low'] }, + { name: 'Snapshot interval', id: 'snap_interval', values: '5' }, + { name: 'Aggregation level', id: 'aggregation_level', values: '2024' }, + { name: 'Ephemeral', id: 'ephemeral' }, + ], + 'kubernetes.io/scaleio': [ + { name: 'API gateway', id: 'gateway', values: 'abc.xyz' }, + { name: 'System name', id: 'system', values: 'test-sys' }, + { name: 'Protection domain', id: 'protectionDomain', values: 'local' }, + { name: 'Storage pool', id: 'storagePool', values: 'simple-pool' }, + { name: 'Storage mode', id: 'storageMode', values: ['ThinProvisioned', 'ThickProvisioned'] }, + { name: 'Secret reference', id: 'secretRef', values: 'simpleSecret' }, + { name: 'Read Only', id: 'readOnly' }, + { name: 'Filesystem Type', id: 'fsType', values: 'ext2' }, + ], + 'kubernetes.io/storageos': [ + { name: 'Pool', id: 'pool', values: 'test-pool' }, + { name: 'Description', id: 'description', values: 'storage drive' }, + { name: 'Filesystem type', id: 'fsType', values: 'ext3' }, + { name: 'Admin secret name', id: 'adminSecretName', values: 'admin-secret-name' }, + { name: 'Admin secret namespace', id: 'adminSecretNamespace', values: 'secret-ns' }, + ], +}; + +// --- VolumeAttributesClass fixtures --- + +export const getVACFixtures = (suffix: string) => { + const names = { + TEST_VAC_LOW_IOPS: `test-vac-low-iops-${suffix}`, + TEST_VAC_HIGH_IOPS: `test-vac-high-iops-${suffix}`, + TEST_VAC_INVALID: `test-vac-invalid-${suffix}`, + TEST_STORAGECLASS: `test-storageclass-${suffix}`, + TEST_PVC: 'test-pvc', + TEST_DEPLOYMENT: 'test-deployment', + }; + + return { + ...names, + VAC_LOW_IOPS: { + apiVersion: 'storage.k8s.io/v1', + kind: 'VolumeAttributesClass', + metadata: { name: names.TEST_VAC_LOW_IOPS }, + driverName: 'ebs.csi.aws.com', + parameters: { iops: '3000', throughput: '125', type: 'gp3' }, + }, + VAC_HIGH_IOPS: { + apiVersion: 'storage.k8s.io/v1', + kind: 'VolumeAttributesClass', + metadata: { name: names.TEST_VAC_HIGH_IOPS }, + driverName: 'ebs.csi.aws.com', + parameters: { iops: '3000', throughput: '125', type: 'gp3' }, + }, + VAC_INVALID: { + apiVersion: 'storage.k8s.io/v1', + kind: 'VolumeAttributesClass', + metadata: { name: names.TEST_VAC_INVALID }, + driverName: 'ebs.csi.aws.com', + parameters: { iops: '999999', throughput: '999999', type: 'gp3' }, + }, + STORAGE_CLASS: { + apiVersion: 'storage.k8s.io/v1', + kind: 'StorageClass', + metadata: { name: names.TEST_STORAGECLASS }, + provisioner: 'ebs.csi.aws.com', + allowVolumeExpansion: true, + }, + getDeployment: (namespace: string, pvcName: string) => ({ + apiVersion: 'apps/v1', + kind: 'Deployment', + metadata: { name: names.TEST_DEPLOYMENT, namespace }, + spec: { + replicas: 1, + selector: { matchLabels: { app: 'test-app' } }, + template: { + metadata: { labels: { app: 'test-app' } }, + spec: { + containers: [ + { + name: 'container', + image: 'image-registry.openshift-image-registry.svc:5000/openshift/httpd:latest', + volumeMounts: [{ name: 'storage', mountPath: '/data' }], + }, + ], + volumes: [{ name: 'storage', persistentVolumeClaim: { claimName: pvcName } }], + }, + }, + }, + }), + }; +}; diff --git a/frontend/e2e/pages/base-page.ts b/frontend/e2e/pages/base-page.ts index e8bcc9eb325..6a05e8f4d26 100644 --- a/frontend/e2e/pages/base-page.ts +++ b/frontend/e2e/pages/base-page.ts @@ -31,6 +31,18 @@ export default abstract class BasePage { protected async goTo(url: string): Promise { await this.page.goto(url, { timeout: 90_000 }); await this.waitForLoadingComplete(); + await this.dismissTourIfPresent(); + } + + private async dismissTourIfPresent(): Promise { + const skipButton = this.page.getByRole('button', { name: 'Skip tour' }); + try { + if ((await skipButton.count()) > 0) { + await skipButton.click({ timeout: 3_000 }); + } + } catch { + // Tour not present or already dismissed + } } protected locator( diff --git a/frontend/e2e/pages/list-page.ts b/frontend/e2e/pages/list-page.ts new file mode 100644 index 00000000000..bd7354759b4 --- /dev/null +++ b/frontend/e2e/pages/list-page.ts @@ -0,0 +1,65 @@ +import type { Locator } from '@playwright/test'; + +import BasePage from './base-page'; + +export class ListPage extends BasePage { + private readonly nameFilterInput = this.page.locator('[data-test-id="item-filter"]'); + private readonly createButton = this.page.getByTestId('item-create'); + private readonly resourceRows = this.page.locator('[data-test-rows="resource-row"]'); + + async filterByName(name: string): Promise { + await this.nameFilterInput.fill(name); + } + + async navigateTo(url: string): Promise { + await this.goTo(url); + } + + async clickCreateButton(): Promise { + await this.robustClick(this.createButton); + } + + async rowsShouldBeLoaded(): Promise { + await this.resourceRows.first().waitFor({ state: 'visible', timeout: 30_000 }); + } + + getRow(resourceName: string): Locator { + return this.resourceRows.filter({ hasText: resourceName }); + } + + async rowShouldExist(resourceName: string): Promise { + const row = this.resourceRows.filter({ hasText: resourceName }); + await row.first().waitFor({ state: 'visible', timeout: 30_000 }); + return row; + } + + async rowShouldNotExist(resourceName: string): Promise { + await this.page + .locator(`[data-test-id="${resourceName}"]`) + .waitFor({ state: 'detached', timeout: 90_000 }); + } + + async clickRowKebabAction(resourceName: string, actionName: string): Promise { + const row = this.resourceRows.filter({ hasText: resourceName }).first(); + await row.waitFor({ state: 'visible', timeout: 30_000 }); + const kebab = row.locator('[data-test-id="kebab-button"]'); + const action = this.page.locator(`[data-test-action="${actionName}"]`); + + for (let attempt = 0; attempt < 3; attempt++) { + await this.robustClick(kebab); + try { + await action.waitFor({ state: 'visible', timeout: 5_000 }); + await action.click(); + return; + } catch { + // Menu may have closed — retry + } + } + throw new Error(`Kebab action "${actionName}" not found for row "${resourceName}"`); + } + + async clickRowByName(resourceName: string): Promise { + const link = this.page.locator(`a[data-test-id="${resourceName}"]`); + await this.robustClick(link); + } +} diff --git a/frontend/e2e/pages/modal-page.ts b/frontend/e2e/pages/modal-page.ts new file mode 100644 index 00000000000..6ba094c26cc --- /dev/null +++ b/frontend/e2e/pages/modal-page.ts @@ -0,0 +1,26 @@ +import BasePage from './base-page'; + +export class ModalPage extends BasePage { + private readonly cancelButton = this.page.locator('[data-test-id="modal-cancel-action"]'); + private readonly submitButton = this.page.locator('button[type=submit]'); + + async shouldBeOpened(): Promise { + await this.cancelButton.waitFor({ state: 'visible', timeout: 20_000 }); + } + + async shouldBeClosed(): Promise { + await this.cancelButton.waitFor({ state: 'detached', timeout: 30_000 }); + } + + async submit(): Promise { + await this.robustClick(this.submitButton); + } + + async submitShouldBeEnabled(): Promise { + await this.submitButton.waitFor({ state: 'visible', timeout: 10_000 }); + } + + async cancel(): Promise { + await this.robustClick(this.cancelButton); + } +} diff --git a/frontend/e2e/tests/console/storage/clone.spec.ts b/frontend/e2e/tests/console/storage/clone.spec.ts new file mode 100644 index 00000000000..4dedd26fde2 --- /dev/null +++ b/frontend/e2e/tests/console/storage/clone.spec.ts @@ -0,0 +1,142 @@ +import { test, expect } from '../../../fixtures'; +import { ListPage } from '../../../pages/list-page'; +import { ModalPage } from '../../../pages/modal-page'; +import { PVC, PVCGP3, testerDeployment } from '../../../mocks/storage'; + +const isAws = String(process.env.BRIDGE_AWS).toLowerCase() === 'true'; +const cloneSize = '2'; + +test.describe('Clone Tests', { tag: ['@admin', '@storage'] }, () => { + test.skip(!isAws, 'No CSI based storage classes are available on this platform'); + + test('creates and deletes a PVC clone', async ({ page, k8sClient, cleanup }) => { + const ns = `test-clone-${Date.now()}`; + const pvcName = PVC.metadata.name; + const cloneName = `${pvcName}-clone`; + const listPage = new ListPage(page); + const modal = new ModalPage(page); + + await test.step('Set up namespace and resources', async () => { + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + await k8sClient.createPVC(ns, { ...PVC, metadata: { ...PVC.metadata, namespace: ns } } as any); + await k8sClient.createPVC(ns, { + ...PVCGP3, + metadata: { ...PVCGP3.metadata, namespace: ns }, + } as any); + await k8sClient.createDeployment(ns, { + ...testerDeployment, + metadata: { ...testerDeployment.metadata, namespace: ns }, + } as any); + }); + + await test.step('Wait for PVC to be Bound', async () => { + await listPage.navigateTo(`/k8s/ns/${ns}/persistentvolumeclaims`); + await listPage.filterByName(pvcName); + const pvcRow = listPage.getRow(pvcName).first(); + await expect(pvcRow.locator('[data-test="status-text"]')).toContainText('Bound', { + timeout: 120_000, + }); + }); + + await test.step('Clone PVC', async () => { + await listPage.clickRowKebabAction(pvcName, 'Clone PVC'); + await modal.shouldBeOpened(); + await modal.submitShouldBeEnabled(); + await page.getByTestId('input-request-size').fill(cloneSize); + await modal.submit(); + await modal.shouldBeClosed(); + }); + + await test.step('Verify clone details', async () => { + await expect(page).toHaveURL(new RegExp(`persistentvolumeclaims/${cloneName}`)); + await expect(page.locator('[data-test="page-heading"]')).toContainText(cloneName); + const pvc = await k8sClient.getPVC(cloneName, ns); + expect((pvc as any).metadata.name).toBe(cloneName); + expect((pvc as any).metadata.namespace).toBe(ns); + await expect(page.getByTestId('pvc-requested-capacity')).toContainText(`${cloneSize} GiB`); + }); + + await test.step('Verify clone appears in list', async () => { + await listPage.navigateTo(`/k8s/ns/${ns}/persistentvolumeclaims`); + await listPage.rowsShouldBeLoaded(); + await listPage.rowShouldExist(cloneName); + }); + + await test.step('Delete clone', async () => { + await listPage.filterByName(cloneName); + await listPage.clickRowKebabAction(cloneName, 'Delete PersistentVolumeClaim'); + await modal.shouldBeOpened(); + await modal.submitShouldBeEnabled(); + await modal.submit(); + await modal.shouldBeClosed(); + await listPage.rowShouldNotExist(cloneName); + }); + }); + + test('creates PVC clone with different storage class', async ({ + page, + k8sClient, + cleanup, + }) => { + const ns = `test-clone-sc-${Date.now()}`; + const pvcName = PVC.metadata.name; + const cloneName = `${pvcName}-clone`; + const listPage = new ListPage(page); + const modal = new ModalPage(page); + + await test.step('Set up namespace and resources', async () => { + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + await k8sClient.createPVC(ns, { ...PVC, metadata: { ...PVC.metadata, namespace: ns } } as any); + await k8sClient.createPVC(ns, { + ...PVCGP3, + metadata: { ...PVCGP3.metadata, namespace: ns }, + } as any); + await k8sClient.createDeployment(ns, { + ...testerDeployment, + metadata: { ...testerDeployment.metadata, namespace: ns }, + } as any); + }); + + await test.step('Wait for PVC to be Bound', async () => { + await listPage.navigateTo(`/k8s/ns/${ns}/persistentvolumeclaims`); + await listPage.filterByName(pvcName); + const pvcRow = listPage.getRow(pvcName).first(); + await expect(pvcRow.locator('[data-test="status-text"]')).toContainText('Bound', { + timeout: 120_000, + }); + }); + + await test.step('Clone PVC with gp3-csi storage class', async () => { + await listPage.clickRowKebabAction(pvcName, 'Clone PVC'); + await modal.shouldBeOpened(); + await modal.submitShouldBeEnabled(); + await page.getByTestId('input-request-size').fill(cloneSize); + await page.getByTestId('storage-class-dropdown').click(); + await page.getByRole('option', { name: /gp3-csi/ }).click(); + await modal.submit(); + await modal.shouldBeClosed(); + }); + + await test.step('Verify clone details', async () => { + await expect(page).toHaveURL(new RegExp(`persistentvolumeclaims/${cloneName}`)); + await expect(page.locator('[data-test="page-heading"]')).toContainText(cloneName); + const pvc = await k8sClient.getPVC(cloneName, ns); + expect((pvc as any).metadata.name).toBe(cloneName); + expect((pvc as any).metadata.namespace).toBe(ns); + await expect(page.getByTestId('pvc-requested-capacity')).toContainText(`${cloneSize} GiB`); + }); + + await test.step('Delete clone', async () => { + await listPage.navigateTo(`/k8s/ns/${ns}/persistentvolumeclaims`); + await listPage.filterByName(cloneName); + await listPage.clickRowKebabAction(cloneName, 'Delete PersistentVolumeClaim'); + await modal.shouldBeOpened(); + await modal.submitShouldBeEnabled(); + await modal.submit(); + await modal.shouldBeClosed(); + await listPage.rowShouldNotExist(cloneName); + }); + }); +}); diff --git a/frontend/e2e/tests/console/storage/create-storage-class.spec.ts b/frontend/e2e/tests/console/storage/create-storage-class.spec.ts new file mode 100644 index 00000000000..594ef67bda7 --- /dev/null +++ b/frontend/e2e/tests/console/storage/create-storage-class.spec.ts @@ -0,0 +1,102 @@ +import kebabCase from 'lodash/kebabCase.js'; +import { test, expect } from '../../../fixtures'; +import { ListPage } from '../../../pages/list-page'; +import { ModalPage } from '../../../pages/modal-page'; +import { provisionersMap, type Parameter } from '../../../mocks/storage'; + +function getSCNameFromProvisioner(provisionerName: string): string { + return `${kebabCase(provisionerName)}-sc`; +} + +function getParameterTestId(name: string): string { + return `storage-class-provisioner-${kebabCase(name)}`; +} + +function getParameterType(parameter: Parameter): 'dropdown' | 'checkbox' | 'text' { + if (Array.isArray(parameter.values)) return 'dropdown'; + if (typeof parameter.values === 'string') return 'text'; + return 'checkbox'; +} + +test.describe( + 'Storage Class creation with various provisioners', + { tag: ['@admin', '@storage'] }, + () => { + for (const [provisionerName, parameters] of Object.entries(provisionersMap)) { + test(`creates ${provisionerName} based storage class`, async ({ page, k8sClient }) => { + const listPage = new ListPage(page); + const modal = new ModalPage(page); + const scName = getSCNameFromProvisioner(provisionerName); + + await test.step('Clean up leftover StorageClass if exists', async () => { + await k8sClient + .deleteClusterCustomResource('storage.k8s.io', 'v1', 'storageclasses', scName) + .catch(() => {}); + }); + + await test.step('Navigate to StorageClasses and click Create', async () => { + await listPage.navigateTo('/k8s/cluster/storageclasses'); + await listPage.clickCreateButton(); + }); + + await test.step('Fill storage class information', async () => { + await page.getByTestId('storage-class-name').fill(scName); + await page + .getByTestId('storage-class-description') + .fill('Storage class to be used for E2E tests only.'); + await page.getByTestId('storage-class-provisioner-dropdown').click(); + await page.getByTestId('console-select-search-input').locator('input').fill(provisionerName); + await page.getByRole('option', { name: provisionerName }).click(); + }); + + await test.step('Validate and fill provisioner parameters', async () => { + for (const parameter of parameters) { + await fillParameter(page, parameter); + } + }); + + await test.step('Create storage class and verify details page', async () => { + await page.locator('#save-changes').click(); + await expect(page.locator('[data-test-id="resource-title"]')).toBeVisible({ + timeout: 30_000, + }); + await expect(page.locator('[data-test-id="resource-title"]')).not.toBeEmpty(); + }); + + await test.step('Delete storage class via UI', async () => { + await page.locator('[data-test-id="actions-menu-button"]').click(); + await page.locator('[data-test-action="Delete StorageClass"]').click(); + await modal.shouldBeOpened(); + await modal.submit(); + await modal.shouldBeClosed(); + }); + }); + } + }, +); + +async function fillParameter(page: import('@playwright/test').Page, parameter: Parameter): Promise { + const testId = getParameterTestId(parameter.name); + const paramType = getParameterType(parameter); + + if (paramType === 'dropdown') { + await page.getByTestId(testId).click(); + const values = parameter.values as string[]; + for (const val of values) { + await expect(page.getByRole('option', { name: val, exact: true })).toBeVisible(); + } + await page.getByRole('option', { name: values[0], exact: true }).click(); + } else if (paramType === 'checkbox') { + await page.getByTestId(testId).click(); + } else { + await page.getByTestId(testId).fill(parameter.values as string); + } + + if (parameter.hintText) { + await expect(page.locator('.help-block', { hasText: parameter.hintText })).toBeVisible(); + } + + if (parameter.nestedParameter) { + await fillParameter(page, parameter.nestedParameter); + } +} diff --git a/frontend/e2e/tests/console/storage/snapshot.spec.ts b/frontend/e2e/tests/console/storage/snapshot.spec.ts new file mode 100644 index 00000000000..fb8f1e056ce --- /dev/null +++ b/frontend/e2e/tests/console/storage/snapshot.spec.ts @@ -0,0 +1,197 @@ +import { test, expect } from '../../../fixtures'; +import { ListPage } from '../../../pages/list-page'; +import { ModalPage } from '../../../pages/modal-page'; +import { PVC, testerDeployment, SnapshotClass, patchForVolume } from '../../../mocks/storage'; + +const isAws = String(process.env.BRIDGE_AWS).toLowerCase() === 'true'; +const dropdownFirstOption = '[role="option"]'; + +test.describe('Snapshot Tests', { tag: ['@admin', '@storage'] }, () => { + test.skip(!isAws, 'No CSI based storage classes are available on this platform'); + + test('creates, lists, and deletes a VolumeSnapshot', async ({ page, k8sClient, cleanup }) => { + const ns = `test-snap-${Date.now()}`; + const pvcName = PVC.metadata.name; + const snapshotName = `${pvcName}-snapshot`; + const listPage = new ListPage(page); + const modal = new ModalPage(page); + + await test.step('Set up namespace and resources', async () => { + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + await k8sClient.createPVC(ns, { ...PVC, metadata: { ...PVC.metadata, namespace: ns } } as any); + await k8sClient.createDeployment(ns, { + ...testerDeployment, + metadata: { ...testerDeployment.metadata, namespace: ns }, + } as any); + await k8sClient + .createClusterCustomResource( + 'snapshot.storage.k8s.io', + 'v1', + 'volumesnapshotclasses', + SnapshotClass as any, + ) + .catch((e) => { + if (!String(e).includes('409')) throw e; + }); + }); + + await test.step('Wait for PVC to be Bound', async () => { + await listPage.navigateTo(`/k8s/ns/${ns}/persistentvolumeclaims`); + await listPage.filterByName(pvcName); + const pvcRow = listPage.getRow(pvcName).first(); + await expect(pvcRow.locator('[data-test="status-text"]')).toContainText('Bound', { + timeout: 120_000, + }); + }); + + await test.step('Create snapshot', async () => { + await listPage.navigateTo(`/k8s/ns/${ns}/snapshot.storage.k8s.io~v1~VolumeSnapshot`); + await listPage.clickCreateButton(); + await page.getByTestId('pvc-dropdown').click(); + await page.locator(dropdownFirstOption).first().click(); + await page.getByTestId('snapshot-dropdown').click(); + await page.locator(dropdownFirstOption).first().click(); + await modal.submit(); + }); + + await test.step('Verify snapshot details', async () => { + await expect(page).toHaveURL( + new RegExp(`snapshot.storage.k8s.io~v1~VolumeSnapshot/${snapshotName}`), + ); + await expect(page.locator('[data-test="page-heading"]')).toContainText(pvcName); + await expect( + page.locator('[data-test-id="resource-summary"] [data-test="status-text"]'), + ).toContainText('Ready', { + timeout: 120_000, + }); + + const vs = (await k8sClient.getCustomResource( + 'snapshot.storage.k8s.io', + 'v1', + ns, + 'volumesnapshots', + snapshotName, + )) as any; + + expect(vs.metadata.name).toBe(snapshotName); + expect(vs.metadata.namespace).toBe(ns); + expect(vs.spec.source.persistentVolumeClaimName).toBe(pvcName); + + await expect(page.locator('[data-test="details-item-value__VSC"] a')).toContainText( + vs.status.boundVolumeSnapshotContentName, + ); + await expect(page.locator('[data-test="details-item-value__SC"] a')).toContainText( + vs.spec.volumeSnapshotClassName, + ); + }); + + await test.step('Verify snapshot in list', async () => { + await listPage.navigateTo(`/k8s/ns/${ns}/snapshot.storage.k8s.io~v1~VolumeSnapshot`); + await listPage.rowsShouldBeLoaded(); + await listPage.rowShouldExist(snapshotName); + }); + + await test.step('Delete snapshot', async () => { + await listPage.clickRowKebabAction(snapshotName, 'Delete VolumeSnapshot'); + await modal.shouldBeOpened(); + await modal.submitShouldBeEnabled(); + await modal.submit(); + await modal.shouldBeClosed(); + await listPage.rowShouldNotExist(snapshotName); + }); + + await test.step('Clean up cluster-scoped resources', async () => { + await k8sClient.deleteClusterCustomResource( + 'snapshot.storage.k8s.io', + 'v1', + 'volumesnapshotclasses', + SnapshotClass.metadata.name, + ); + }); + }); + + test('restores a snapshot to create a new PVC', async ({ page, k8sClient, cleanup }) => { + const ns = `test-snap-restore-${Date.now()}`; + const pvcName = PVC.metadata.name; + const snapshotName = `${pvcName}-snapshot`; + const restoreName = `${snapshotName}-restore`; + const listPage = new ListPage(page); + const modal = new ModalPage(page); + + await test.step('Set up namespace and resources', async () => { + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + await k8sClient.createPVC(ns, { ...PVC, metadata: { ...PVC.metadata, namespace: ns } } as any); + await k8sClient.createDeployment(ns, { + ...testerDeployment, + metadata: { ...testerDeployment.metadata, namespace: ns }, + } as any); + await k8sClient + .createClusterCustomResource( + 'snapshot.storage.k8s.io', + 'v1', + 'volumesnapshotclasses', + SnapshotClass as any, + ) + .catch((e) => { + if (!String(e).includes('409')) throw e; + }); + }); + + await test.step('Wait for PVC to be Bound', async () => { + await listPage.navigateTo(`/k8s/ns/${ns}/persistentvolumeclaims`); + await listPage.filterByName(pvcName); + const pvcRow = listPage.getRow(pvcName).first(); + await expect(pvcRow.locator('[data-test="status-text"]')).toContainText('Bound', { + timeout: 120_000, + }); + }); + + await test.step('Create snapshot', async () => { + await listPage.navigateTo(`/k8s/ns/${ns}/snapshot.storage.k8s.io~v1~VolumeSnapshot`); + await listPage.clickCreateButton(); + await page.getByTestId('pvc-dropdown').click(); + await page.locator(dropdownFirstOption).first().click(); + await page.getByTestId('snapshot-dropdown').click(); + await page.locator(dropdownFirstOption).first().click(); + await modal.submit(); + await expect( + page.locator('[data-test-id="resource-summary"] [data-test="status-text"]'), + ).toContainText('Ready', { + timeout: 120_000, + }); + }); + + await test.step('Restore snapshot as new PVC', async () => { + await listPage.navigateTo(`/k8s/ns/${ns}/snapshot.storage.k8s.io~v1~VolumeSnapshot`); + await listPage.rowsShouldBeLoaded(); + await listPage.clickRowKebabAction(snapshotName, 'Restore as new PVC'); + await modal.shouldBeOpened(); + await expect(page.getByTestId('pvc-name')).toHaveValue(restoreName); + await page.locator('#restore-storage-class').click(); + await page.locator(dropdownFirstOption).nth(1).click(); + await modal.submit(); + await modal.shouldBeClosed(); + }); + + await test.step('Patch deployment to use restored PVC and verify Bound', async () => { + await k8sClient.patchDeployment(testerDeployment.metadata.name, ns, [patchForVolume]); + await listPage.navigateTo(`/k8s/ns/${ns}/persistentvolumeclaims/${restoreName}`); + await expect( + page.locator('[data-test-id="pvc-status"] [data-test="status-text"]'), + ).toContainText('Bound', { + timeout: 120_000, + }); + }); + + await test.step('Clean up cluster-scoped resources', async () => { + await k8sClient.deleteClusterCustomResource( + 'snapshot.storage.k8s.io', + 'v1', + 'volumesnapshotclasses', + SnapshotClass.metadata.name, + ); + }); + }); +}); diff --git a/frontend/e2e/tests/console/storage/volume-attributes-class.spec.ts b/frontend/e2e/tests/console/storage/volume-attributes-class.spec.ts new file mode 100644 index 00000000000..a1d0f61043c --- /dev/null +++ b/frontend/e2e/tests/console/storage/volume-attributes-class.spec.ts @@ -0,0 +1,155 @@ +import { test, expect } from '../../../fixtures'; +import { ModalPage } from '../../../pages/modal-page'; +import { getVACFixtures } from '../../../mocks/storage'; + +const isAws = String(process.env.BRIDGE_AWS).toLowerCase() === 'true'; + +test.describe('VolumeAttributesClass E2E tests', { tag: ['@admin', '@storage'] }, () => { + test.skip(!isAws, 'Requires AWS platform with EBS CSI driver'); + + test('creates PVC with VAC, modifies VAC, and handles invalid VAC', async ({ + page, + k8sClient, + cleanup, + }) => { + const ns = `test-vac-${Date.now()}`; + const fixtures = getVACFixtures(ns); + const { + VAC_LOW_IOPS, + VAC_HIGH_IOPS, + VAC_INVALID, + STORAGE_CLASS, + TEST_VAC_LOW_IOPS, + TEST_VAC_HIGH_IOPS, + TEST_VAC_INVALID, + TEST_PVC, + TEST_STORAGECLASS, + getDeployment, + } = fixtures; + const modal = new ModalPage(page); + + await test.step('Set up namespace and cluster-scoped resources', async () => { + await k8sClient.createNamespace(ns); + cleanup.trackNamespace(ns); + await k8sClient.createClusterCustomResource( + 'storage.k8s.io', + 'v1', + 'storageclasses', + STORAGE_CLASS as any, + ); + await k8sClient.createClusterCustomResource( + 'storage.k8s.io', + 'v1', + 'volumeattributesclasses', + VAC_LOW_IOPS as any, + ); + await k8sClient.createClusterCustomResource( + 'storage.k8s.io', + 'v1', + 'volumeattributesclasses', + VAC_HIGH_IOPS as any, + ); + await k8sClient.createClusterCustomResource( + 'storage.k8s.io', + 'v1', + 'volumeattributesclasses', + VAC_INVALID as any, + ); + await k8sClient.createDeployment(ns, getDeployment(ns, TEST_PVC) as any); + }); + + await test.step('Create PVC with VolumeAttributesClass', async () => { + await page.goto(`/k8s/ns/${ns}/persistentvolumeclaims/~new/form`); + await page.getByTestId('pvc-name').fill(TEST_PVC); + await page.getByTestId('pvc-size').fill('1'); + + await page.getByTestId('storageclass-dropdown').click(); + await page.getByRole('option', { name: TEST_STORAGECLASS }).click(); + + await page.getByTestId('volumeattributesclass-dropdown').click(); + await page.getByRole('option', { name: TEST_VAC_LOW_IOPS }).click(); + + await page.getByTestId('create-pvc').click(); + }); + + await test.step('Verify PVC details with requested VAC', async () => { + await expect(page.locator('[data-test="page-heading"]')).toContainText(TEST_PVC); + await expect(page.locator('[data-test-id="pvc-requested-vac"]')).toContainText( + TEST_VAC_LOW_IOPS, + { timeout: 30_000 }, + ); + await expect(page.locator('[data-test="status-text"]')).toContainText('Bound', { + timeout: 120_000, + }); + await expect(page.locator('[data-test-id="pvc-current-vac"]')).toContainText( + TEST_VAC_LOW_IOPS, + { timeout: 30_000 }, + ); + }); + + await test.step('Modify VolumeAttributesClass to high IOPS', async () => { + await page.locator('[data-test-id="actions-menu-button"]').click(); + await page.locator('[data-test-action="Modify VolumeAttributesClass"]').click(); + await modal.shouldBeOpened(); + + await page.getByTestId('modify-vac-dropdown').click(); + await page.getByRole('option', { name: TEST_VAC_HIGH_IOPS }).click(); + await modal.submit(); + await modal.shouldBeClosed(); + + await expect(page.locator('[data-test-id="pvc-requested-vac"]')).toContainText( + TEST_VAC_HIGH_IOPS, + { timeout: 30_000 }, + ); + await expect(page.locator('[data-test-id="pvc-current-vac"]')).toContainText( + TEST_VAC_HIGH_IOPS, + { timeout: 30_000 }, + ); + }); + + await test.step('Attempt invalid VAC modification and verify error', async () => { + await page.locator('[data-test-id="actions-menu-button"]').click(); + await page.locator('[data-test-action="Modify VolumeAttributesClass"]').click(); + await modal.shouldBeOpened(); + + await page.getByTestId('modify-vac-dropdown').click(); + await page.getByRole('option', { name: TEST_VAC_INVALID }).click(); + await modal.submit(); + await modal.shouldBeClosed(); + + await expect(page.locator('[data-test-id="pvc-requested-vac"]')).toContainText( + TEST_VAC_INVALID, + { timeout: 30_000 }, + ); + await expect(page.locator('[data-test-id="pvc-current-vac"]')).toContainText( + TEST_VAC_HIGH_IOPS, + { timeout: 30_000 }, + ); + + await expect(page.locator('[data-test-id="vac-error-alert"]')).toBeVisible({ + timeout: 60_000, + }); + await expect(page.locator('[data-test-id="vac-error-alert"]')).toContainText( + 'VolumeAttributesClass modification failed', + ); + }); + + await test.step('Clean up cluster-scoped resources', async () => { + for (const vacName of [TEST_VAC_LOW_IOPS, TEST_VAC_HIGH_IOPS, TEST_VAC_INVALID]) { + await k8sClient + .patchClusterCustomResource('storage.k8s.io', 'v1', 'volumeattributesclasses', vacName, { + metadata: { finalizers: [] }, + }) + .catch(() => {}); + } + for (const vacName of [TEST_VAC_LOW_IOPS, TEST_VAC_HIGH_IOPS, TEST_VAC_INVALID]) { + await k8sClient + .deleteClusterCustomResource('storage.k8s.io', 'v1', 'volumeattributesclasses', vacName) + .catch(() => {}); + } + await k8sClient + .deleteClusterCustomResource('storage.k8s.io', 'v1', 'storageclasses', TEST_STORAGECLASS) + .catch(() => {}); + }); + }); +}); diff --git a/frontend/packages/integration-tests/mocks/snapshot.ts b/frontend/packages/integration-tests/mocks/snapshot.ts deleted file mode 100644 index 397368d2fa1..00000000000 --- a/frontend/packages/integration-tests/mocks/snapshot.ts +++ /dev/null @@ -1,112 +0,0 @@ -import type { Patch } from '@console/internal/module/k8s'; - -export const testerDeployment = { - apiVersion: 'apps/v1', - kind: 'Deployment', - metadata: { - name: 'busybox-deployment', - labels: { - app: 'busybox', - }, - }, - spec: { - replicas: 1, - strategy: { - type: 'RollingUpdate', - }, - selector: { - matchLabels: { - app: 'busybox', - }, - }, - template: { - metadata: { - labels: { - app: 'busybox', - }, - }, - spec: { - volumes: [ - { - name: 'testpvc', - persistentVolumeClaim: { - claimName: 'testpvc', - }, - }, - ], - containers: [ - { - name: 'busybox', - image: 'busybox', - imagePullPolicy: 'IfNotPresent', - volumeDevices: [ - { - name: 'testpvc', - devicePath: '/data', - }, - ], - command: ['sh', '-c', 'echo Container 1 is Running ; sleep 3600'], - }, - ], - }, - nodeSelector: { - overload: 'true', - }, - }, - }, -}; - -export const PVC = { - apiVersion: 'v1', - kind: 'PersistentVolumeClaim', - metadata: { - name: 'testpvc', - }, - spec: { - storageClassName: 'gp2-csi', - accessModes: ['ReadWriteOnce'], - resources: { - requests: { - storage: '1Gi', - }, - }, - }, -}; - -export const PVCGP3 = { - apiVersion: PVC.apiVersion, - kind: PVC.kind, - metadata: { - name: 'testpvcgp3', - }, - spec: { - storageClassName: 'gp3-csi', - accessModes: PVC.spec.accessModes, - resources: { - requests: { - storage: PVC.spec.resources.requests.storage, - }, - }, - }, -}; - -export const SnapshotClass = { - apiVersion: 'snapshot.storage.k8s.io/v1', - kind: 'VolumeSnapshotClass', - metadata: { - name: 'csi-hostpath-snapclass', - }, - driver: 'ebs.csi.aws.com', - deletionPolicy: 'Delete', -}; - -export const patchForVolume: Patch = { - op: 'add', - path: '/spec/template/spec/volumes/-', - value: { - name: 'testpvc-snapshot-restore', - persistentVolumeClaim: { - claimName: 'testpvc-snapshot-restore', - }, - }, -}; diff --git a/frontend/packages/integration-tests/mocks/storage-class.ts b/frontend/packages/integration-tests/mocks/storage-class.ts deleted file mode 100644 index 4fb07fcfe58..00000000000 --- a/frontend/packages/integration-tests/mocks/storage-class.ts +++ /dev/null @@ -1,316 +0,0 @@ -import type { Parameter } from '../views/storage/create-storage-class'; - -type ProvisionerAndParameters = { - [provisionerName: string]: Parameter[]; -}; - -export const provisionersMap: ProvisionerAndParameters = { - 'kubernetes.io/aws-ebs': [ - { - name: 'Type', - id: 'type', - values: ['io1', 'gp2', 'sc1', 'st1'], - }, - { - name: 'IOPS per GiB', - id: 'iopsPerGB', - values: '10', - }, - { - name: 'Filesystem type', - id: 'fsType', - values: 'ext4', - }, - { - name: 'Encrypted', - id: 'encrypted', - nestedParameter: { - name: 'KMS key ID', - values: 'sample-kms-id', - }, - }, - ], - // not showing up on GCP cluster - /* 'ebs.csi.aws.com': [ - { - name: 'Type', - id: 'type', - values: ['gp3', 'gp2', 'io1', 'sc1', 'st1', 'standard'], - }, - { - name: 'IOPS per GiB', - id: 'iopsPerGB', - values: '10', - }, - { - name: 'Filesystem Type', - id: 'fsType', - values: ['ext4', 'xfs', 'ext2', 'ext3'], - }, - { - name: 'Encrypted', - id: 'encrypted', - nestedParameter: { - name: 'KMS key ID', - values: 'sample-kms-id', - }, - }, - ], - 'kubernetes.io/gce-pd': [ - { - name: 'Type', - id: 'type', - values: ['pd-standard', 'pd-ssd'], - }, - { - name: 'Zone', - id: 'zone', - values: 'kathmandu', - }, - { - name: 'Replication type', - id: 'replication-type', - values: ['none', 'regional-pd'], - }, - ], - 'kubernetes.io/glusterfs': [ - { - name: 'Gluster REST/Heketi URL', - id: 'resturl', - values: 'abcd.xyz', - }, - { - name: 'Gluster REST/Heketi user', - id: 'restuser', - values: 'user', - }, - { - name: 'Secret Namespace', - id: 'secretNamespace', - values: 'secret-ns', - }, - { - name: 'Secret Name', - id: 'secretName', - values: 'secret-name', - }, - { - name: 'Cluster ID', - id: 'clusterid', - values: 'drogo', - }, - { - name: 'GID min', - id: 'gidMin', - values: '50', - }, - { - name: 'GID max', - id: 'gidMax', - values: '100', - }, - { - name: 'Volume type', - id: 'volumetype', - values: 'ext4', - }, - ], */ - 'kubernetes.io/cinder': [ - { - name: 'Volume type', - id: 'type', - values: 'ext4', - }, - { - name: 'Availability zone', - id: 'availability', - values: 'lalitpur', - }, - ], - 'kubernetes.io/azure-file': [ - { - name: 'SKU name', - id: 'skuName', - hintText: 'Azure storage account SKU tier', - values: 'sample-name', - }, - { - name: 'Location', - id: 'location', - hintText: 'Azure storage account name', - values: 'bhaktapur', - }, - { - name: 'Azure storage account name', - id: 'storageAccount', - hintText: 'Azure storage account name', - values: 'test-account', - }, - ], - 'kubernetes.io/azure-disk': [ - { - name: 'Storage account type', - id: 'storageaccounttype', - hintText: 'Storage account type', - values: 'tester', - }, - { - name: 'Account kind', - id: 'kind', - values: ['shared', 'dedicated', 'managed'], - }, - ], - 'kubernetes.io/quobyte': [ - { - name: 'Quobyte API server', - id: 'quobyteAPIServer', - values: 'test.xyzab', - }, - { - name: 'Registry address(es)', - id: 'registry', - values: 'xyz.abc', - }, - { - name: 'Admin secret name', - id: 'adminSecretName', - values: 'secret-admin', - }, - { - name: 'Admin secret namespace', - id: 'adminSecretNamespace', - values: 'secret-ns', - }, - { - name: 'User', - id: 'user', - values: 'admin', - }, - { - name: 'Quobyte configuration', - id: 'quobyteConfig', - values: 'config', - }, - { - name: 'Quobyte tenant', - id: 'quobyteTenant', - values: 'tester', - }, - ], - 'kubernetes.io/vsphere-volume': [ - { - name: 'Disk format', - id: 'diskformat', - values: ['thin', 'zeroedthick', 'eagerzeroedthick'], - }, - { - name: 'Datastore', - id: 'datastore', - values: 'store-thin', - }, - ], - 'kubernetes.io/portworx-volume': [ - { - name: 'Filesystem', - id: 'fs', - values: ['none', 'xfs', 'ext4'], - }, - { - name: 'Block size', - id: 'block_size', - values: '1024', - }, - { - name: 'Number of synchronous replicas to be provided in the form of replication factor', - id: 'repl', - values: '2', - }, - { - name: 'I/O priority', - id: 'io_priority', - values: ['high', 'medium', 'low'], - }, - { - name: 'Snapshot interval', - id: 'snap_interval', - values: '5', - }, - { - name: 'Aggregation level', - id: 'aggregation_level', - values: '2024', - }, - { - name: 'Ephemeral', - id: 'ephemeral', - }, - ], - 'kubernetes.io/scaleio': [ - { - name: 'API gateway', - id: 'gateway', - values: 'abc.xyz', - }, - { - name: 'System name', - id: 'system', - values: 'test-sys', - }, - { - name: 'Protection domain', - id: 'protectionDomain', - values: 'local', - }, - { - name: 'Storage pool', - id: 'storagePool', - values: 'simple-pool', - }, - { - name: 'Storage mode', - id: 'storageMode', - values: ['thinProvisioned', 'thickProvisioned'], - }, - { - name: 'Secret reference', - id: 'secretRef', - values: 'simpleSecret', - }, - { - name: 'Read Only', - id: 'readOnly', - }, - { - name: 'Filesystem Type', - id: 'fsType', - values: 'ext2', - }, - ], - 'kubernetes.io/storageos': [ - { - name: 'Pool', - id: 'pool', - values: 'test-pool', - }, - { - name: 'Description', - id: 'description', - values: 'storage drive', - }, - { - name: 'Filesystem type', - id: 'fsType', - values: 'ext3', - }, - { - name: 'Admin secret name', - id: 'adminSecretName', - values: 'admin-secret-name', - }, - { - name: 'Admin secret namespace', - id: 'adminSecretNamespace', - values: 'secret-ns', - }, - ], -}; diff --git a/frontend/packages/integration-tests/mocks/volume-attributes-class.ts b/frontend/packages/integration-tests/mocks/volume-attributes-class.ts deleted file mode 100644 index 6fcd77af8e7..00000000000 --- a/frontend/packages/integration-tests/mocks/volume-attributes-class.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { DeploymentKind } from '@console/internal/module/k8s'; - -// Factory function to generate unique fixture names and objects per test run -// This prevents collisions when tests run concurrently on shared clusters -export const getVACFixtures = (suffix: string) => { - const names = { - TEST_VAC_LOW_IOPS: `test-vac-low-iops-${suffix}`, - TEST_VAC_HIGH_IOPS: `test-vac-high-iops-${suffix}`, - TEST_VAC_INVALID: `test-vac-invalid-${suffix}`, - TEST_STORAGECLASS: `test-storageclass-${suffix}`, - // Namespace-scoped, no suffix needed - TEST_PVC: 'test-pvc', - TEST_DEPLOYMENT: 'test-deployment', - }; - - return { - ...names, - VAC_LOW_IOPS: { - apiVersion: 'storage.k8s.io/v1', - kind: 'VolumeAttributesClass', - metadata: { name: names.TEST_VAC_LOW_IOPS }, - driverName: 'ebs.csi.aws.com', - parameters: { iops: '3000', throughput: '125', type: 'gp3' }, - }, - VAC_HIGH_IOPS: { - apiVersion: 'storage.k8s.io/v1', - kind: 'VolumeAttributesClass', - metadata: { name: names.TEST_VAC_HIGH_IOPS }, - driverName: 'ebs.csi.aws.com', - // Uses identical parameters to VAC_LOW_IOPS to minimize CSI driver modification time and reduce test flakiness. - // This allows verification of VAC name fields on the PVC details page without long waits for actual volume operations. - parameters: { iops: '3000', throughput: '125', type: 'gp3' }, - }, - VAC_INVALID: { - apiVersion: 'storage.k8s.io/v1', - kind: 'VolumeAttributesClass', - metadata: { name: names.TEST_VAC_INVALID }, - driverName: 'ebs.csi.aws.com', - parameters: { iops: '999999', throughput: '999999', type: 'gp3' }, - }, - STORAGE_CLASS: { - apiVersion: 'storage.k8s.io/v1', - kind: 'StorageClass', - metadata: { name: names.TEST_STORAGECLASS }, - provisioner: 'ebs.csi.aws.com', - allowVolumeExpansion: true, - }, - getDeployment: (namespace: string, pvcName: string): DeploymentKind => ({ - apiVersion: 'apps/v1', - kind: 'Deployment', - metadata: { name: names.TEST_DEPLOYMENT, namespace }, - spec: { - replicas: 1, - selector: { matchLabels: { app: 'test-app' } }, - template: { - metadata: { labels: { app: 'test-app' } }, - spec: { - containers: [ - { - name: 'container', - image: 'image-registry.openshift-image-registry.svc:5000/openshift/httpd:latest', - volumeMounts: [{ name: 'storage', mountPath: '/data' }], - }, - ], - volumes: [{ name: 'storage', persistentVolumeClaim: { claimName: pvcName } }], - }, - }, - }, - }), - }; -}; diff --git a/frontend/packages/integration-tests/tests/storage/clone.cy.ts b/frontend/packages/integration-tests/tests/storage/clone.cy.ts deleted file mode 100644 index a025f604f80..00000000000 --- a/frontend/packages/integration-tests/tests/storage/clone.cy.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { PVC, PVCGP3, testerDeployment } from '../../mocks/snapshot'; -import { testName, checkErrors } from '../../support'; -import { resourceStatusShouldContain } from '../../views/common'; -import { detailsPage, DetailsPageSelector } from '../../views/details-page'; -import { listPage } from '../../views/list-page'; -import { modal } from '../../views/modal'; -import { nav } from '../../views/nav'; - -const cloneName = `${PVC.metadata.name}-clone`; -const cloneSize = '2'; -const deletePVCClone = (pvcName: string) => { - nav.sidenav.clickNavLink(['PersistentVolumeClaims']); - listPage.filter.byName(pvcName); - listPage.rows.clickKebabAction(pvcName, 'Delete PersistentVolumeClaim'); - modal.shouldBeOpened(); - modal.submitShouldBeEnabled(); - modal.submit(); - modal.shouldBeClosed(); - listPage.rows.shouldNotExist(pvcName); -}; - -// Normalize env check: CI env vars are strings, so "false" would be truthy without explicit comparison. -const isAws = String(Cypress.expose('BRIDGE_AWS')).toLowerCase() === 'true'; -if (isAws) { - describe('Clone Tests', () => { - before(() => { - cy.login(); - cy.createProjectWithCLI(testName); - cy.exec(`echo '${JSON.stringify(PVC)}' | oc apply -n ${testName} -f -`); - cy.exec(`echo '${JSON.stringify(PVCGP3)}' | oc apply -n ${testName} -f -`); - cy.exec(`echo '${JSON.stringify(testerDeployment)}' | oc apply -n ${testName} -f -`); - nav.sidenav.clickNavLink(['Storage', 'PersistentVolumeClaims']); - listPage.filter.byName(PVC.metadata.name); - resourceStatusShouldContain('Bound'); - }); - - afterEach(() => { - checkErrors(); - }); - - after(() => { - cy.exec(`echo '${JSON.stringify(testerDeployment)}' | oc delete -n ${testName} -f -`); - cy.exec(`echo '${JSON.stringify(PVC)}' | oc delete -n ${testName} -f -`); - cy.exec(`echo '${JSON.stringify(PVCGP3)}' | oc delete -n ${testName} -f -`); - cy.deleteProjectWithCLI(testName); - }); - - it('Creates PVC Clone', () => { - listPage.rows.clickKebabAction(PVC.metadata.name, 'Clone PVC'); - modal.shouldBeOpened(); - modal.submitShouldBeEnabled(); - cy.byTestID('input-request-size').clear().type(cloneSize); - modal.submit(); - modal.shouldBeClosed(); - cy.location('pathname').should( - 'include', - `persistentvolumeclaims/${PVC.metadata.name}-clone`, - ); - detailsPage.titleShouldContain(`${PVC.metadata.name}-clone`); - cy.exec(`oc get pvc ${PVC.metadata.name}-clone -n ${testName} -o json`) - .its('stdout') - .then((res) => { - const pvc = JSON.parse(res); - cy.get(DetailsPageSelector.name).contains(pvc.metadata.name); - cy.get(DetailsPageSelector.namespace).contains(pvc.metadata.namespace); - cy.byTestID('pvc-requested-capacity').contains(`${cloneSize} GiB`); - }); - }); - - it('Lists Clone', () => { - nav.sidenav.clickNavLink(['PersistentVolumeClaims']); - listPage.rows.shouldBeLoaded(); - listPage.rows.shouldExist(cloneName); - }); - - it('Deletes PVC Clone', () => { - deletePVCClone(cloneName); - }); - - it('Creates PVC Clone with different storage cluster', () => { - listPage.filter.byName(PVC.metadata.name); - listPage.rows.clickKebabAction(PVC.metadata.name, 'Clone PVC'); - modal.shouldBeOpened(); - modal.submitShouldBeEnabled(); - cy.byTestID('input-request-size').clear().type(cloneSize); - cy.byTestID('storage-class-dropdown').click(); - cy.byTestID('console-select-item').contains('gp3-csi').click(); - modal.submit(); - modal.shouldBeClosed(); - cy.location('pathname').should( - 'include', - `persistentvolumeclaims/${PVC.metadata.name}-clone`, - ); - detailsPage.titleShouldContain(`${PVC.metadata.name}-clone`); - cy.exec(`oc get pvc ${PVC.metadata.name}-clone -n ${testName} -o json`) - .its('stdout') - .then((res) => { - const pvc = JSON.parse(res); - cy.get(DetailsPageSelector.name).contains(pvc.metadata.name); - cy.get(DetailsPageSelector.namespace).contains(pvc.metadata.namespace); - cy.byTestID('pvc-requested-capacity').contains(`${cloneSize} GiB`); - }); - }); - - it('Deletes PVC Clone', () => { - deletePVCClone(cloneName); - }); - }); -} else { - describe('Skipping Clone Tests', () => { - it('No CSI based storage classes are available in this platform', () => {}); - }); -} diff --git a/frontend/packages/integration-tests/tests/storage/create-storage-class.cy.ts b/frontend/packages/integration-tests/tests/storage/create-storage-class.cy.ts deleted file mode 100644 index e1423e88ed7..00000000000 --- a/frontend/packages/integration-tests/tests/storage/create-storage-class.cy.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { provisionersMap } from '../../mocks/storage-class'; -import { nav } from '../../views/nav'; -import { - createStorageClassValidateAndCleanUp, - fillStorageClassInformation, - validatePresenceOfParameterAndFeedData, -} from '../../views/storage/create-storage-class'; - -describe('Test creation of Storage classes using various provisioners', () => { - before(() => { - cy.login(); - nav.sidenav.clickNavLink(['Storage', 'StorageClasses']); - }); - - beforeEach(() => { - cy.byTestID('item-create').click(); - }); - - Object.entries(provisionersMap).forEach(([provisionerName, parameters]) => { - it(`Create ${provisionerName} based storage class`, () => { - fillStorageClassInformation(provisionerName); - parameters.forEach((parameter) => validatePresenceOfParameterAndFeedData(parameter)); - createStorageClassValidateAndCleanUp(provisionerName, parameters); - }); - }); -}); diff --git a/frontend/packages/integration-tests/tests/storage/snapshot.cy.ts b/frontend/packages/integration-tests/tests/storage/snapshot.cy.ts deleted file mode 100644 index 21bf43207a0..00000000000 --- a/frontend/packages/integration-tests/tests/storage/snapshot.cy.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { PVC, testerDeployment, SnapshotClass, patchForVolume } from '../../mocks/snapshot'; -import { testName, checkErrors } from '../../support'; -import { resourceStatusShouldContain } from '../../views/common'; -import { detailsPage, DetailsPageSelector } from '../../views/details-page'; -import { listPage } from '../../views/list-page'; -import { modal } from '../../views/modal'; -import { nav } from '../../views/nav'; -import { SnapshotDetails, dropdownFirstItem } from '../../views/storage/snapshot'; - -const snapshotName = `${PVC.metadata.name}-snapshot`; - -// These tests are meant to be run on AWS as only AWS supports CSI storage classes(gp2-csi) -// Normalize env check: CI env vars are strings, so "false" would be truthy without explicit comparison. -const isAws = String(Cypress.expose('BRIDGE_AWS')).toLowerCase() === 'true'; -if (isAws) { - describe('Snapshot Tests', () => { - before(() => { - cy.login(); - cy.createProjectWithCLI(testName); - cy.exec(`echo '${JSON.stringify(PVC)}' | oc apply -n ${testName} -f -`); - cy.exec(`echo '${JSON.stringify(testerDeployment)}' | oc apply -n ${testName} -f -`); - cy.exec(`echo '${JSON.stringify(SnapshotClass)}' | oc apply -f -`); - nav.sidenav.clickNavLink(['Storage', 'Persistent Volume Claims']); - listPage.filter.byName(PVC.metadata.name); - resourceStatusShouldContain('Bound'); - }); - - afterEach(() => { - checkErrors(); - }); - - after(() => { - cy.exec(`echo '${JSON.stringify(testerDeployment)}' | oc delete -n ${testName} -f -`); - cy.exec(`echo '${JSON.stringify(PVC)}' | oc delete -n ${testName} -f -`); - cy.exec(`oc delete pvc ${snapshotName}-restore -n ${testName}`); - cy.exec(`echo '${JSON.stringify(SnapshotClass)}' | oc delete -f -`); - cy.deleteProjectWithCLI(testName); - }); - - it('Creates Snapshot', () => { - nav.sidenav.clickNavLink(['Volume Snapshots']); - listPage.clickCreateYAMLbutton(); - cy.byTestID('pvc-dropdown').click(); - cy.get(dropdownFirstItem).first().click(); - cy.byTestID('snapshot-dropdown').click(); - cy.get(dropdownFirstItem).first().click(); - modal.submit(); - cy.location('pathname').should( - 'include', - `snapshot.storage.k8s.io~v1~VolumeSnapshot/${PVC.metadata.name}-snapshot`, - ); - detailsPage.titleShouldContain(PVC.metadata.name); - resourceStatusShouldContain('Ready', { timeout: 40000 }); - cy.exec(`oc get VolumeSnapshot ${PVC.metadata.name}-snapshot -n ${testName} -o json`) - .its('stdout') - .then((res) => { - const volumeSnapshot = JSON.parse(res); - cy.get(DetailsPageSelector.name).contains(volumeSnapshot.metadata.name); - cy.get(DetailsPageSelector.namespace).contains(volumeSnapshot.metadata.namespace); - cy.get(SnapshotDetails.vsc).contains( - volumeSnapshot.status.boundVolumeSnapshotContentName, - ); - cy.get(SnapshotDetails.sc).contains(volumeSnapshot.spec.volumeSnapshotClassName); - cy.get(SnapshotDetails.pvc).contains( - volumeSnapshot.spec.source.persistentVolumeClaimName, - ); - }); - }); - - it('Lists Snapshot', () => { - nav.sidenav.clickNavLink(['VolumeSnapshots']); - listPage.rows.shouldBeLoaded(); - listPage.rows.shouldExist(snapshotName); - listPage.rows.shouldNotExist(`${snapshotName}dup`); - }); - - it('Restore a Snapshot to create a new claim from it', () => { - cy.clickNavLink(['Volume Snapshots']); - listPage.rows.clickKebabAction(snapshotName, 'Restore as new PVC'); - modal.shouldBeOpened(); - cy.byTestID('pvc-name').should('have.value', `${snapshotName}-restore`); - cy.get(SnapshotDetails.scDropdown).click(); - cy.get(dropdownFirstItem).eq(1).click(); - modal.submit(); - modal.shouldBeClosed(); - cy.exec( - `oc patch Deployment ${ - testerDeployment.metadata.name - } --type='json' -n ${testName} -p '[${JSON.stringify(patchForVolume)}]'`, - ) - .its('stdout') - .then(() => resourceStatusShouldContain('Bound', { timeout: 40000 })); - }); - - it('Deletes Snapshot', () => { - cy.clickNavLink(['VolumeSnapshots']); - listPage.rows.clickKebabAction(snapshotName, 'Delete VolumeSnapshot'); - modal.shouldBeOpened(); - modal.submitShouldBeEnabled(); - modal.submit(); - modal.shouldBeClosed(); - listPage.rows.shouldNotExist(snapshotName); - }); - }); -} else { - describe('Skipping Snapshot Tests', () => { - it('No CSI based storage classes are available in this platform', () => {}); - }); -} diff --git a/frontend/packages/integration-tests/tests/storage/volume-attributes-class.cy.ts b/frontend/packages/integration-tests/tests/storage/volume-attributes-class.cy.ts deleted file mode 100644 index db941412c2f..00000000000 --- a/frontend/packages/integration-tests/tests/storage/volume-attributes-class.cy.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { getVACFixtures } from '../../mocks/volume-attributes-class'; -import { testName, checkErrors } from '../../support'; -import { resourceStatusShouldContain } from '../../views/common'; -import { detailsPage } from '../../views/details-page'; -import { listPage } from '../../views/list-page'; -import { modal } from '../../views/modal'; - -// These tests require AWS platform with EBS CSI driver for modifyVolume support -const isAws = String(Cypress.expose('BRIDGE_AWS')).toLowerCase() === 'true'; - -if (isAws) { - describe('VolumeAttributesClass E2E tests', () => { - // Generate unique fixtures per test run to avoid collisions on shared clusters - const fixtures = getVACFixtures(testName); - const { - VAC_LOW_IOPS, - VAC_HIGH_IOPS, - VAC_INVALID, - STORAGE_CLASS, - TEST_VAC_LOW_IOPS, - TEST_VAC_HIGH_IOPS, - TEST_VAC_INVALID, - TEST_PVC, - TEST_DEPLOYMENT, - TEST_STORAGECLASS, - getDeployment, - } = fixtures; - - before(() => { - cy.login(); - cy.createProjectWithCLI(testName); - // Create StorageClass for PVC provisioning - cy.exec(`echo '${JSON.stringify(STORAGE_CLASS)}' | oc apply -f -`); - // Create VolumeAttributesClasses for testing - cy.exec(`echo '${JSON.stringify(VAC_LOW_IOPS)}' | oc apply -f -`); - cy.exec(`echo '${JSON.stringify(VAC_HIGH_IOPS)}' | oc apply -f -`); - cy.exec(`echo '${JSON.stringify(VAC_INVALID)}' | oc apply -f -`); - // Create Deployment that will consume the PVC - cy.exec(`echo '${JSON.stringify(getDeployment(testName, TEST_PVC))}' | oc apply -f -`); - }); - - afterEach(() => { - checkErrors(); - }); - - after(() => { - // Navigate to VAC list page to avoid 404 during cleanup - cy.visit('/k8s/cluster/storage.k8s.io~v1~VolumeAttributesClass'); - listPage.dvRows.shouldBeLoaded(); - - // Delete Deployment first to release PVC - cy.exec( - `oc delete deployment ${TEST_DEPLOYMENT} -n ${testName} --ignore-not-found=true --wait=true`, - { - failOnNonZeroExit: false, - timeout: 120000, - }, - ); - - // Delete PVC to release VAC reference - cy.exec(`oc delete pvc ${TEST_PVC} -n ${testName} --ignore-not-found=true --wait=true`, { - failOnNonZeroExit: false, - timeout: 120000, - }); - - // Remove finalizers from VACs to allow deletion - [TEST_VAC_LOW_IOPS, TEST_VAC_HIGH_IOPS, TEST_VAC_INVALID].forEach((vacName) => { - cy.exec( - `oc patch volumeattributesclass ${vacName} -p '{"metadata":{"finalizers":[]}}' --type=merge`, - { failOnNonZeroExit: false, timeout: 30000 }, - ); - }); - - // Delete VACs without waiting (cluster-scoped resources can be slow) - cy.exec( - `oc delete volumeattributesclass ${TEST_VAC_LOW_IOPS} ${TEST_VAC_HIGH_IOPS} ${TEST_VAC_INVALID} --ignore-not-found=true --wait=false`, - { - failOnNonZeroExit: false, - timeout: 30000, - }, - ); - - // Delete StorageClass - cy.exec(`oc delete storageclass ${TEST_STORAGECLASS} --ignore-not-found=true --wait=false`, { - failOnNonZeroExit: false, - timeout: 30000, - }); - - cy.deleteProjectWithCLI(testName); - }); - - it('creates PVC with VolumeAttributesClass and verifies it appears on details page', () => { - cy.visit(`/k8s/ns/${testName}/persistentvolumeclaims/~new/form`); - cy.byTestID('pvc-name').should('exist').clear().type(TEST_PVC); - cy.byTestID('pvc-size').clear().type('1'); - - // Select StorageClass from dropdown - cy.byTestID('storageclass-dropdown').click(); - cy.byTestID('console-select-item').contains(TEST_STORAGECLASS).click(); - - // Select VolumeAttributesClass from dropdown - cy.byTestID('volumeattributesclass-dropdown').click(); - cy.byTestID('console-select-item').contains(TEST_VAC_LOW_IOPS).click(); - - // Create PVC and navigate to details page - cy.byTestID('create-pvc').click(); - detailsPage.titleShouldContain(TEST_PVC); - - // Verify requested VAC is displayed - cy.byLegacyTestID('pvc-requested-vac', { timeout: 30000 }).should( - 'contain.text', - TEST_VAC_LOW_IOPS, - ); - - // Wait for PVC to reach Bound status - resourceStatusShouldContain('Bound', { timeout: 120000 }); - - // Verify current VAC matches requested VAC - cy.byLegacyTestID('pvc-current-vac', { timeout: 30000 }).should('exist'); - cy.byLegacyTestID('pvc-current-vac').should('contain.text', TEST_VAC_LOW_IOPS); - }); - - it('modifies VolumeAttributesClass via modal and verifies update', () => { - cy.visit(`/k8s/ns/${testName}/persistentvolumeclaims/${TEST_PVC}`); - detailsPage.isLoaded(); - - // Open Modify VolumeAttributesClass modal - detailsPage.clickPageActionFromDropdown('Modify VolumeAttributesClass'); - modal.shouldBeOpened(); - - // Select new VolumeAttributesClass - cy.byTestID('modify-vac-dropdown').click(); - cy.byTestID('console-select-item').contains(TEST_VAC_HIGH_IOPS).click(); - modal.submit(); - modal.shouldBeClosed(); - - // Verify requested VAC updated to new value - cy.byLegacyTestID('pvc-requested-vac', { timeout: 30000 }).should( - 'contain.text', - TEST_VAC_HIGH_IOPS, - ); - - // Verify current VAC updated to new value - cy.byLegacyTestID('pvc-current-vac', { timeout: 30000 }).should( - 'contain.text', - TEST_VAC_HIGH_IOPS, - ); - }); - - it('attempts invalid VAC modification and verifies error alert', () => { - cy.visit(`/k8s/ns/${testName}/persistentvolumeclaims/${TEST_PVC}`); - detailsPage.isLoaded(); - - // Open Modify VolumeAttributesClass modal and select invalid VAC - detailsPage.clickPageActionFromDropdown('Modify VolumeAttributesClass'); - modal.shouldBeOpened(); - cy.byTestID('modify-vac-dropdown').click(); - cy.byTestID('console-select-item').contains(TEST_VAC_INVALID).click(); - modal.submit(); - modal.shouldBeClosed(); - - // Verify requested VAC updated to invalid value - cy.byLegacyTestID('pvc-requested-vac', { timeout: 30000 }).should( - 'contain.text', - TEST_VAC_INVALID, - ); - - // Verify current VAC remains at previous valid value - cy.byLegacyTestID('pvc-current-vac').should('exist'); - cy.byLegacyTestID('pvc-current-vac').should('contain.text', TEST_VAC_HIGH_IOPS); - - // Verify error alert appears after CSI driver rejects modification - cy.byLegacyTestID('vac-error-alert', { timeout: 60000 }).should('be.visible'); - cy.byLegacyTestID('vac-error-alert').should( - 'contain.text', - 'VolumeAttributesClass modification failed', - ); - }); - }); -} else { - describe('Skipping VolumeAttributesClass Tests', () => { - it('requires AWS platform with EBS CSI driver', () => {}); - }); -} diff --git a/frontend/packages/integration-tests/views/storage/create-storage-class.ts b/frontend/packages/integration-tests/views/storage/create-storage-class.ts deleted file mode 100644 index f546f7bf5c8..00000000000 --- a/frontend/packages/integration-tests/views/storage/create-storage-class.ts +++ /dev/null @@ -1,108 +0,0 @@ -import * as _ from 'lodash'; -import type { StorageClass } from '@console/internal/components/storage-class-form'; -import { detailsPage } from '../details-page'; -import { modal } from '../modal'; - -export type Parameter = { - name: string; - id?: string; - values?: string | string[]; - hintText?: string; - nestedParameter?: Parameter; -}; - -enum ParameterType { - DROPDOWN = 'DROPDOWN', - CHECKBOX = 'CHECKBOX', - TEXT = 'TEXT', -} - -const getParameterType = (parameter: Parameter): ParameterType => { - if (_.isArray(parameter.values)) return ParameterType.DROPDOWN; - if (_.isString(parameter.values)) return ParameterType.TEXT; - return ParameterType.CHECKBOX; -}; - -const COMMON_DESCRIPTION = 'Storage class to be used for E2E tests only.'; - -const getSCNameFromProvisioner = (provisionerName: string) => `${_.kebabCase(provisionerName)}-sc`; - -const getParameterIdFromName = (name: string) => `storage-class-provisioner-${_.kebabCase(name)}`; - -const validateDropdownElementsPresence = (elements: string[]) => { - elements.forEach((element) => { - cy.byTestDropDownMenu(element).should('be.visible'); - }); -}; - -const getStorageClassParameterValue = (storageClass: StorageClass, key: string) => - storageClass.parameters[key]; - -const validateAttributes = (storageClass: StorageClass, parameters: Parameter[]) => { - expect(storageClass.metadata.annotations.description).toEqual(COMMON_DESCRIPTION); - parameters.forEach((parameter) => { - const parameterType = getParameterType(parameter); - if (parameterType === ParameterType.DROPDOWN) { - expect(getStorageClassParameterValue(storageClass, parameter.id)).toEqual( - parameter.values[0], - ); - } else if (parameterType === ParameterType.CHECKBOX) { - expect(getStorageClassParameterValue(storageClass, parameter.id)).toEqual('true'); - } else { - expect(getStorageClassParameterValue(storageClass, parameter.id)).toEqual(parameter.values); - } - }); -}; - -export const fillStorageClassInformation = (provisioner: string) => { - const name = getSCNameFromProvisioner(provisioner); - cy.byTestID('storage-class-name').type(name); - cy.byTestID('storage-class-description').type(COMMON_DESCRIPTION); - cy.byTestID('storage-class-provisioner-dropdown').click(); - cy.byTestID('console-select-search-input').type(provisioner); - cy.contains(provisioner).click(); -}; - -export const validatePresenceOfParameterAndFeedData = (parameter: Parameter) => { - const elementId = getParameterIdFromName(parameter.name); - cy.get('label').contains(parameter.name); - const parameterType = getParameterType(parameter); - if (parameterType === ParameterType.DROPDOWN) { - cy.byTestID(elementId).click(); - validateDropdownElementsPresence(parameter.values as string[]); - cy.byTestDropDownMenu(parameter.values[0]).click(); - } else if (parameterType === ParameterType.CHECKBOX) { - cy.byTestID(elementId).click(); - } else if (parameterType === ParameterType.TEXT) { - cy.byTestID(elementId).type(parameter.values as string); - } - if (parameter.hintText) { - cy.contains(parameter.hintText).should('be.visible'); - } - if (parameter.nestedParameter) { - validatePresenceOfParameterAndFeedData(parameter.nestedParameter); - } -}; - -export const createStorageClassValidateAndCleanUp = ( - provisioner: string, - parameters: Parameter[], -) => { - const name = getSCNameFromProvisioner(provisioner); - // Creation - cy.get('[id="save-changes"]').click(); - cy.byTestID('loading-indicator').should('not.exist'); - detailsPage.isLoaded(); - // Validation - cy.exec(`oc get sc ${name} -o json`) - .its('stdout') - .then((response) => { - const storageClass: StorageClass = JSON.parse(response); - validateAttributes(storageClass, parameters); - }); - // Cleanup - detailsPage.clickPageActionFromDropdown('Delete StorageClass'); - modal.shouldBeOpened(); - modal.submit(); - modal.shouldBeClosed(); -}; diff --git a/frontend/packages/integration-tests/views/storage/snapshot.ts b/frontend/packages/integration-tests/views/storage/snapshot.ts deleted file mode 100644 index 82ebc7cfacf..00000000000 --- a/frontend/packages/integration-tests/views/storage/snapshot.ts +++ /dev/null @@ -1,8 +0,0 @@ -export namespace SnapshotDetails { - export const pvc = '[data-test="details-item-value__PVC"] a'; - export const vsc = '[data-test="details-item-value__VSC"] a'; - export const sc = '[data-test="details-item-value__SC"] a'; - export const scDropdown = '#restore-storage-class'; -} - -export const dropdownFirstItem = '.dropdown-menu__autocomplete-filter li a';