diff --git a/frontend/integration-tests/protractor.conf.ts b/frontend/integration-tests/protractor.conf.ts index f41f6ac0cba..c81cc498491 100644 --- a/frontend/integration-tests/protractor.conf.ts +++ b/frontend/integration-tests/protractor.conf.ts @@ -101,7 +101,7 @@ export const config: Config = { catalog: ['tests/base.scenario.ts', 'tests/catalog.scenario.ts'], marketplace: ['tests/base.scenario.ts', 'tests/marketplace/kubernetes-marketplace.scenario.ts'], overview: ['tests/base.scenario.ts', 'tests/overview/overview.scenario.ts'], - kubevirt: ['tests/base.scenario.ts', 'tests/kubevirt/vm.wizard.scenario.ts'], + kubevirt: ['tests/base.scenario.ts', 'tests/kubevirt/vm.wizard.scenario.ts', 'tests/kubevirt/vm.actions.scenario.ts'], all: ['tests/base.scenario.ts', 'tests/crud.scenario.ts', 'tests/overview/overview.scenareio.ts', diff --git a/frontend/integration-tests/tests/kubevirt/mocks.ts b/frontend/integration-tests/tests/kubevirt/mocks.ts new file mode 100644 index 00000000000..d2da1bf0d97 --- /dev/null +++ b/frontend/integration-tests/tests/kubevirt/mocks.ts @@ -0,0 +1,76 @@ +/* eslint-disable no-undef */ +import { testName } from '../../protractor.conf'; + +const testLabel = 'automatedTestName'; + +export const testVM = { + apiVersion: 'kubevirt.io/v1alpha2', + kind: 'VirtualMachine', + metadata: { + name: `vm-${testName}`, + namespace: testName, + labels: {[testLabel]: testName}, + }, + spec: { + running: false, + template: { + spec: { + domain: { + cpu: { + cores: 1, + }, + devices: { + disks: [ + { + bootOrder: 1, + disk: { + bus: 'virtio', + }, + name: 'rootdisk', + volumeName: 'rootdisk', + }, + ], + interfaces: [ + { + bridge: {}, + name: 'eth0', + }, + ], + }, + resources: { + requests: { + memory: '1G', + }, + }, + }, + networks: [ + { + name: 'eth0', + pod: {}, + }, + ], + volumes: [ + { + containerDisk: { + image: 'kubevirt/cirros-registry-disk-demo:latest', + }, + name: 'rootdisk', + }, + ], + }, + }, + }, +}; + +export const testNAD = { + apiVersion: 'k8s.cni.cncf.io/v1', + kind: 'NetworkAttachmentDefinition', + metadata: { + name: `ovs-net-1${testName}-${testName}`, + namespace: testName, + labels: {[testLabel]: testName}, + }, + spec: { + config: '{ "cniVersion": "0.3.1", "type": "ovs", "bridge": "br0" }', + }, +}; diff --git a/frontend/integration-tests/tests/kubevirt/utils.ts b/frontend/integration-tests/tests/kubevirt/utils.ts new file mode 100644 index 00000000000..caa08f4e347 --- /dev/null +++ b/frontend/integration-tests/tests/kubevirt/utils.ts @@ -0,0 +1,17 @@ +/* eslint-disable no-undef */ +import { execSync } from 'child_process'; + +export function removeLeakedResources(leakedResources: Set){ + const leakedArray: Array = [...leakedResources]; + if (leakedArray.length > 0) { + console.error(`Leaked ${leakedArray.join()}`); + leakedArray.map(r => JSON.parse(r) as {name: string, namespace: string, kind: string}) + .forEach(({name, namespace, kind}) => { + try { + execSync(`kubectl delete -n ${namespace} --cascade ${kind} ${name}`); + } catch (error) { + console.error(`Failed to delete ${kind} ${name}:\n${error}`); + } + }); + } +} diff --git a/frontend/integration-tests/tests/kubevirt/vm.actions.scenario.ts b/frontend/integration-tests/tests/kubevirt/vm.actions.scenario.ts new file mode 100644 index 00000000000..975478d8673 --- /dev/null +++ b/frontend/integration-tests/tests/kubevirt/vm.actions.scenario.ts @@ -0,0 +1,96 @@ +/* eslint-disable no-undef */ + +import { execSync } from 'child_process'; +import { browser, ExpectedConditions as until } from 'protractor'; + +import { appHost, testName } from '../../protractor.conf'; +import { resourceRowsPresent, filterForName, isLoaded } from '../../views/crud.view'; +import { testVM } from './mocks'; +import { removeLeakedResources } from './utils'; +import {detailViewAction, detailViewVMmStatus, listViewAction, listViewVMmStatus} from '../../views/kubevirt/vm.actions.view'; + +const VM_BOOTUP_TIMEOUT = 60000; +const VM_ACTIONS_TIMEOUT = 90000; + +describe('Test VM actions', () => { + const leakedResources = new Set(); + afterAll(async() => { + removeLeakedResources(leakedResources); + }); + + describe('Test VM list view kebab actions', () => { + const vmName = `vm-list-view-actions-${testName}`; + beforeAll(async() => { + testVM.metadata.name = vmName; + execSync(`echo '${JSON.stringify(testVM)}' | kubectl create -f -`); + leakedResources.add(JSON.stringify({name: vmName, namespace: testName, kind: 'vm'})); + }); + + // Workaround for https://github.com/kubevirt/web-ui/issues/177, remove when resolved + afterEach(async() => await browser.sleep(1000)); + + it('Navigates to VMs', async() => { + await browser.get(`${appHost}/k8s/all-namespaces/virtualmachines`); + await isLoaded(); + await filterForName(vmName); + await resourceRowsPresent(); + }); + + it('Starts VM', async() => { + await listViewAction(vmName)('Start'); + await browser.wait(until.textToBePresentInElement(listViewVMmStatus(vmName), 'Running'), VM_BOOTUP_TIMEOUT); + }); + + it('Restarts VM', async() => { + await listViewAction(vmName)('Restart'); + await browser.wait(until.textToBePresentInElement(listViewVMmStatus(vmName), 'Starting'), VM_BOOTUP_TIMEOUT); + await browser.wait(until.textToBePresentInElement(listViewVMmStatus(vmName), 'Running'), VM_BOOTUP_TIMEOUT); + }, VM_ACTIONS_TIMEOUT); + + it('Stops VM', async() => { + await listViewAction(vmName)('Stop'); + await browser.wait(until.textToBePresentInElement(listViewVMmStatus(vmName), 'Off'), 10000); + }); + + it('Deletes VM', async() => { + await listViewAction(vmName)('Delete'); + await browser.wait(until.not(until.presenceOf(listViewVMmStatus(vmName)))); + leakedResources.delete(JSON.stringify({name: vmName, namespace: testName, kind: 'vm'})); + }); + }); + + describe('Test VM detail view kebab actions', () => { + const vmName = `vm-detail-view-actions-${testName}`; + beforeAll(async() => { + testVM.metadata.name = vmName; + execSync(`echo '${JSON.stringify(testVM)}' | kubectl create -f -`); + leakedResources.add(JSON.stringify({name: vmName, namespace: testName, kind: 'vm'})); + }); + + it('Navigates to VMs detail page', async() => { + await browser.get(`${appHost}/k8s/all-namespaces/virtualmachines/${vmName}`); + await isLoaded(); + }); + + it('Starts VM', async() => { + await detailViewAction('Start'); + await browser.wait(until.textToBePresentInElement(detailViewVMmStatus, 'Running'), VM_BOOTUP_TIMEOUT); + }); + + it('Restarts VM', async() => { + await detailViewAction('Restart'); + await browser.wait(until.textToBePresentInElement(detailViewVMmStatus, 'Starting'), VM_BOOTUP_TIMEOUT); + await browser.wait(until.textToBePresentInElement(detailViewVMmStatus, 'Running'), VM_BOOTUP_TIMEOUT); + }, VM_ACTIONS_TIMEOUT); + + it('Stops VM', async() => { + await detailViewAction('Stop'); + await browser.wait(until.textToBePresentInElement(detailViewVMmStatus, 'Off'), 10000); + }); + + it('Deletes VM', async() => { + await detailViewAction('Delete'); + leakedResources.delete(JSON.stringify({name: vmName, namespace: testName, kind: 'vm'})); + }); + }); +}); diff --git a/frontend/integration-tests/tests/kubevirt/vm.wizard.scenario.ts b/frontend/integration-tests/tests/kubevirt/vm.wizard.scenario.ts index 3f575da2cae..074d1835e38 100644 --- a/frontend/integration-tests/tests/kubevirt/vm.wizard.scenario.ts +++ b/frontend/integration-tests/tests/kubevirt/vm.wizard.scenario.ts @@ -2,11 +2,13 @@ import { execSync } from 'child_process'; import { browser, by, ExpectedConditions as until } from 'protractor'; -import { appHost, testName } from '../../protractor.conf'; -import { resourceRowsPresent, filterForName, deleteRow, isLoaded, createItemButton, errorMessage } from '../../views/crud.view'; -import * as vmView from '../../views/kubevirt/vm.view'; import { OrderedMap } from 'immutable'; +import { appHost, testName } from '../../protractor.conf'; +import { resourceRowsPresent, filterForName, deleteRow, createItemButton, isLoaded, errorMessage } from '../../views/crud.view'; +import { removeLeakedResources } from './utils'; +import { testNAD } from './mocks'; +import * as vmView from '../../views/kubevirt/vm.view'; describe('Kubevirt create VM using wizard', () => { const leakedResources = new Set(); @@ -16,19 +18,7 @@ describe('Kubevirt create VM using wizard', () => { const workloadProfile = 'generic'; const sourceURL = 'https://download.cirros-cloud.net/0.4.0/cirros-0.4.0-x86_64-disk.img'; const sourceContainer = 'kubevirt/cirros-registry-disk-demo:latest'; - const networkDefinitionName = `${testName}-ovs-net-1`; const pxeInterface = 'eth1'; - const testNAD = { - apiVersion: 'k8s.cni.cncf.io/v1', - kind: 'NetworkAttachmentDefinition', - metadata: { - name: networkDefinitionName, - namespace: testName, - }, - spec: { - config: '{ "cniVersion": "0.3.1", "type": "ovs", "bridge": "br0" }', - }, - }; const provisionMethods = OrderedMap void>() .set('PXE', async function(provisionSource) { await vmView.provisionSourceButton.click(); @@ -47,96 +37,73 @@ describe('Kubevirt create VM using wizard', () => { await vmView.provisionSourceURL.sendKeys(sourceURL); }); - beforeAll(async() => { - execSync(`echo '${JSON.stringify(testNAD)}' | kubectl create -f -`); - }); - - afterAll(async() => { - execSync(`kubectl delete -n ${testName} net-attach-def ${networkDefinitionName}`); - const leakedArray: Array = [...leakedResources]; - if (leakedArray.length > 0) { - console.error(`Leaked ${leakedArray.join()}`); - leakedArray.map(r => JSON.parse(r) as {name: string, namespace: string, kind: string}) - .forEach(({name, namespace, kind}) => { - try { - execSync(`kubectl delete -n ${namespace} --cascade ${kind} ${name}`); - } catch (error) { - console.error(`Failed to delete ${kind} ${name}:\n${error}`); - } - }); - } - }); - - provisionMethods.forEach((provisionMethod, methodName) => { - describe(`Using ${methodName} method.`, () => { - it('Navigates to VMs', async() => { - await browser.get(`${appHost}/k8s/all-namespaces/virtualmachines`); - await isLoaded(); - }); - - it('Opens VM wizard', async() => { - await createItemButton.click().then(() => vmView.createWithWizardLink.click()); - }); - - it('Configures VM Basic Settings', async() => { - await browser.wait(until.presenceOf(vmView.nameInput), 10000); - await vmView.nameInput.sendKeys(vmName); + async function fillBasicSettings(provisionMethod: (provisionSource: string) => void, provisionSourceName: string){ + await browser.wait(until.presenceOf(vmView.nameInput), 10000); + await vmView.nameInput.sendKeys(vmName); - await vmView.namespaceButton.click(); - await vmView.namespaceMenu.element(by.linkText(testName)).click(); + await vmView.namespaceButton.click(); + await vmView.namespaceMenu.element(by.linkText(testName)).click(); - await provisionMethod(methodName); + await provisionMethod(provisionSourceName); - await vmView.operatingSystemButton.click(); - await vmView.operatingSystemMenu.element(by.linkText(operatingSystem)).click(); + await vmView.operatingSystemButton.click(); + await vmView.operatingSystemMenu.element(by.linkText(operatingSystem)).click(); - await vmView.flavorButton.click(); - await vmView.flavorSourceMenu.element(by.linkText(flavor)).click(); + await vmView.flavorButton.click(); + await vmView.flavorSourceMenu.element(by.linkText(flavor)).click(); - await vmView.workloadProfileButton.click(); - await vmView.workloadProfileMenu.element(by.linkText(workloadProfile)).click(); + await vmView.workloadProfileButton.click(); + await vmView.workloadProfileMenu.element(by.linkText(workloadProfile)).click(); - await vmView.startVMOnCreation.click(); + await vmView.startVMOnCreation.click(); - await vmView.nextButton.click(); - }); + await vmView.nextButton.click(); + } - it('Configures VM Networking', async() => { - if (methodName === 'PXE'){ - await vmView.createNIC.click(); + async function fillVMNetworking(provisionSourceName: string){ + if (provisionSourceName === 'PXE'){ + await vmView.createNIC.click(); - await vmView.networkDefinitionButton.click(); - await vmView.networkDefinitionMenu.element(by.linkText(networkDefinitionName)).click(); + await vmView.networkDefinitionButton.click(); + await vmView.networkDefinitionMenu.element(by.linkText(testNAD.metadata.name)).click(); - await vmView.pxeNICButton.click(); - await vmView.pxeNICMenu.element(by.linkText(pxeInterface)).click(); - await vmView.applyButton.click(); - } - await vmView.nextButton.click(); - }); - - it('Configures VM Storage', async() => { - await vmView.nextButton.click(); - }); - - it('Confirms to create VM', async() => { - await browser.wait(until.elementToBeClickable(vmView.nextButton), 5000).then(() => vmView.nextButton.click()); + await vmView.pxeNICButton.click(); + await vmView.pxeNICMenu.element(by.linkText(pxeInterface)).click(); + await vmView.applyButton.click(); + } + await vmView.nextButton.click(); + } - expect(errorMessage.isPresent()).toBe(false); - leakedResources.add(JSON.stringify({name: vmName, namespace: testName, kind: 'vm'})); - }); + beforeAll(async() => { + execSync(`echo '${JSON.stringify(testNAD)}' | kubectl create -f -`); + }); - it('Verifies created VM', async() => { - await browser.wait(until.invisibilityOf(vmView.wizardHeader), 5000); - await filterForName(vmName); - await resourceRowsPresent(); - await browser.wait(until.textToBePresentInElement(vmView.firstRowVMStatus, 'Running'), 20000); - }); + afterAll(async() => { + execSync(`kubectl delete -n ${testName} net-attach-def ${testNAD.metadata.name}`); + removeLeakedResources(leakedResources); + }); - it('Removes created VM', async() => { - await deleteRow('VirtualMachine')(vmName); - leakedResources.delete(JSON.stringify({name: vmName, namespace: testName, kind: 'vm'})); - }); + provisionMethods.forEach((provisionMethod, methodName) => { + it(`Using ${methodName} provision source.`, async() => { + await browser.get(`${appHost}/k8s/all-namespaces/virtualmachines`); + await isLoaded(); + await createItemButton.click().then(() => vmView.createWithWizardLink.click()); + await fillBasicSettings(provisionMethod, methodName); + await fillVMNetworking(methodName); + // Use default storage settings + await vmView.nextButton.click(); + // Confirm to create VM + await browser.wait(until.elementToBeClickable(vmView.nextButton), 5000).then(() => vmView.nextButton.click()); + expect(errorMessage.isPresent()).toBe(false); + leakedResources.add(JSON.stringify({name: vmName, namespace: testName, kind: 'vm'})); + // Verify VM is created and running + await browser.wait(until.invisibilityOf(vmView.wizardHeader), 5000); + await filterForName(vmName); + await resourceRowsPresent(); + await browser.wait(until.textToBePresentInElement(vmView.firstRowVMStatus, 'Running'), 20000); + // Delete VM + await deleteRow('VirtualMachine')(vmName); + leakedResources.delete(JSON.stringify({name: vmName, namespace: testName, kind: 'vm'})); }); }); }); diff --git a/frontend/integration-tests/views/crud.view.ts b/frontend/integration-tests/views/crud.view.ts index 488fb4d5bc4..07744247c47 100644 --- a/frontend/integration-tests/views/crud.view.ts +++ b/frontend/integration-tests/views/crud.view.ts @@ -13,6 +13,8 @@ export const saveChangesBtn = $('#save-changes'); export const reloadBtn = $('#reload-object'); export const cancelBtn = $('#cancel'); +export const confirmAction = () => browser.wait(until.presenceOf($('#confirm-action'))).then(() => $('#confirm-action').click()); + /** * Returns a promise that resolves after the loading spinner is not present. */ diff --git a/frontend/integration-tests/views/kubevirt/vm.actions.view.ts b/frontend/integration-tests/views/kubevirt/vm.actions.view.ts new file mode 100644 index 00000000000..0002b330aab --- /dev/null +++ b/frontend/integration-tests/views/kubevirt/vm.actions.view.ts @@ -0,0 +1,41 @@ +import { $, $$, browser, ExpectedConditions as until } from 'protractor'; +import { rowForName, confirmAction } from '../crud.view'; + +export const detailViewVMmStatus = $('#details-column-1 > dl:nth-child(1) > dd:nth-child(2)'); +export const listViewVMmStatus = (name: string) => rowForName(name).$('div.co-m-row:first-child > div:first-child > div:nth-child(3)'); + +const listViewKebabDropdown = '.co-kebab__button'; +const listViewKebabDropdownMenu = '.co-kebab__dropdown'; +const detailViewDropdown = '.co-m-nav-title button'; +const detailViewDropdownMenu = '.dropdown-menu-right'; + +/** + * Selects option link from given dropdown element. + */ +const selectDropdownItem = (getActionsDropdown, getActionsDropdownMenu) => async(action) => { + await getActionsDropdown().click(); + await getActionsDropdownMenu().$$('a').filter(link => link.getText().then(text => text.startsWith(action))).first().click(); +}; + +/** + * Performs action for VM via list view kebab menu. + */ +export const listViewAction = (name) => async(action) => { + const getActionsDropdown = () => rowForName(name).$$(listViewKebabDropdown).first(); + const getActionsDropdownMenu = () => rowForName(name).$(listViewKebabDropdownMenu); + selectDropdownItem(getActionsDropdown, getActionsDropdownMenu)(action); + await confirmAction(); + await browser.wait(until.not(until.presenceOf(rowForName(name).$(listViewKebabDropdownMenu)))); +}; + +/** + * Performs action for VM on its detail page. + */ +export const detailViewAction = async(action) => { + const getActionsDropdown = () => $$(detailViewDropdown).first(); + const getActionsDropdownMenu = () => $(detailViewDropdownMenu); + selectDropdownItem(getActionsDropdown, getActionsDropdownMenu)(action); + await confirmAction(); + await browser.wait(until.not(until.presenceOf($(detailViewDropdownMenu)))); +}; +