Skip to content

Commit cb71560

Browse files
committed
Add integration tests for VM CRD actions on VM list/detail pages
1 parent a95bac1 commit cb71560

File tree

6 files changed

+220
-0
lines changed

6 files changed

+220
-0
lines changed

frontend/integration-tests/protractor.conf.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export const config: Config = {
9999
catalog: ['tests/base.scenario.ts', 'tests/catalog.scenario.ts'],
100100
operatorHub: ['tests/base.scenario.ts', 'tests/operator-hub/operator-hub.scenario.ts'],
101101
overview: ['tests/base.scenario.ts', 'tests/overview/overview.scenario.ts'],
102+
kubevirt: ['tests/base.scenario.ts', 'tests/kubevirt/vm.actions.scenario.ts'],
102103
all: ['tests/base.scenario.ts',
103104
'tests/crud.scenario.ts',
104105
'tests/overview/overview.scenareio.ts',
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/* eslint-disable no-undef */
2+
import { testName } from '../../protractor.conf';
3+
4+
const testLabel = 'automatedTestName';
5+
6+
export const testVM = {
7+
apiVersion: 'kubevirt.io/v1alpha2',
8+
kind: 'VirtualMachine',
9+
metadata: {
10+
name: `vm-${testName}`,
11+
namespace: testName,
12+
labels: {[testLabel]: testName},
13+
},
14+
spec: {
15+
running: false,
16+
template: {
17+
spec: {
18+
domain: {
19+
cpu: {
20+
cores: 1,
21+
},
22+
devices: {
23+
disks: [
24+
{
25+
bootOrder: 1,
26+
disk: {
27+
bus: 'virtio',
28+
},
29+
name: 'rootdisk',
30+
volumeName: 'rootdisk',
31+
},
32+
],
33+
interfaces: [
34+
{
35+
bridge: {},
36+
name: 'eth0',
37+
},
38+
],
39+
},
40+
resources: {
41+
requests: {
42+
memory: '1G',
43+
},
44+
},
45+
},
46+
networks: [
47+
{
48+
name: 'eth0',
49+
pod: {},
50+
},
51+
],
52+
volumes: [
53+
{
54+
containerDisk: {
55+
image: 'kubevirt/cirros-registry-disk-demo:latest',
56+
},
57+
name: 'rootdisk',
58+
},
59+
],
60+
},
61+
},
62+
},
63+
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/* eslint-disable no-undef */
2+
import { execSync } from 'child_process';
3+
4+
export function removeLeakedResources(leakedResources: Set<string>){
5+
const leakedArray: Array<string> = [...leakedResources];
6+
if (leakedArray.length > 0) {
7+
console.error(`Leaked ${leakedArray.join()}`);
8+
leakedArray.map(r => JSON.parse(r) as {name: string, namespace: string, kind: string})
9+
.forEach(({name, namespace, kind}) => {
10+
try {
11+
execSync(`kubectl delete -n ${namespace} --cascade ${kind} ${name}`);
12+
} catch (error) {
13+
console.error(`Failed to delete ${kind} ${name}:\n${error}`);
14+
}
15+
});
16+
}
17+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/* eslint-disable no-undef */
2+
3+
import { execSync } from 'child_process';
4+
import { browser, ExpectedConditions as until } from 'protractor';
5+
6+
import { appHost, testName } from '../../protractor.conf';
7+
import { resourceRowsPresent, filterForName, isLoaded } from '../../views/crud.view';
8+
import { testVM } from './mocks';
9+
import { removeLeakedResources } from './utils';
10+
import { detailViewAction, detailViewVMmStatus, listViewAction, listViewVMmStatus } from '../../views/kubevirt/vm.actions.view';
11+
12+
const VM_BOOTUP_TIMEOUT = 60000;
13+
const VM_ACTIONS_TIMEOUT = 90000;
14+
15+
describe('Test VM actions', () => {
16+
const leakedResources = new Set<string>();
17+
afterAll(async() => {
18+
removeLeakedResources(leakedResources);
19+
});
20+
21+
describe('Test VM list view kebab actions', () => {
22+
const vmName = `vm-list-view-actions-${testName}`;
23+
beforeAll(async() => {
24+
testVM.metadata.name = vmName;
25+
execSync(`echo '${JSON.stringify(testVM)}' | kubectl create -f -`);
26+
leakedResources.add(JSON.stringify({name: vmName, namespace: testName, kind: 'vm'}));
27+
});
28+
29+
// Workaround for https://github.com/kubevirt/web-ui/issues/177, remove when resolved
30+
afterEach(async() => await browser.sleep(1000));
31+
32+
it('Navigates to VMs', async() => {
33+
await browser.get(`${appHost}/k8s/all-namespaces/virtualmachines`);
34+
await isLoaded();
35+
await filterForName(vmName);
36+
await resourceRowsPresent();
37+
});
38+
39+
it('Starts VM', async() => {
40+
await listViewAction(vmName)('Start');
41+
await browser.wait(until.textToBePresentInElement(listViewVMmStatus(vmName), 'Running'), VM_BOOTUP_TIMEOUT);
42+
});
43+
44+
it('Restarts VM', async() => {
45+
await listViewAction(vmName)('Restart');
46+
await browser.wait(until.textToBePresentInElement(listViewVMmStatus(vmName), 'Starting'), VM_BOOTUP_TIMEOUT);
47+
await browser.wait(until.textToBePresentInElement(listViewVMmStatus(vmName), 'Running'), VM_BOOTUP_TIMEOUT);
48+
}, VM_ACTIONS_TIMEOUT);
49+
50+
it('Stops VM', async() => {
51+
await listViewAction(vmName)('Stop');
52+
await browser.wait(until.textToBePresentInElement(listViewVMmStatus(vmName), 'Off'), 10000);
53+
});
54+
55+
it('Deletes VM', async() => {
56+
await listViewAction(vmName)('Delete');
57+
await browser.wait(until.not(until.presenceOf(listViewVMmStatus(vmName))));
58+
leakedResources.delete(JSON.stringify({name: vmName, namespace: testName, kind: 'vm'}));
59+
});
60+
});
61+
62+
describe('Test VM detail view kebab actions', () => {
63+
const vmName = `vm-detail-view-actions-${testName}`;
64+
beforeAll(async() => {
65+
testVM.metadata.name = vmName;
66+
execSync(`echo '${JSON.stringify(testVM)}' | kubectl create -f -`);
67+
leakedResources.add(JSON.stringify({name: vmName, namespace: testName, kind: 'vm'}));
68+
});
69+
70+
it('Navigates to VMs detail page', async() => {
71+
await browser.get(`${appHost}/k8s/all-namespaces/virtualmachines/${vmName}`);
72+
await isLoaded();
73+
});
74+
75+
it('Starts VM', async() => {
76+
await detailViewAction('Start');
77+
await browser.wait(until.textToBePresentInElement(detailViewVMmStatus, 'Running'), VM_BOOTUP_TIMEOUT);
78+
});
79+
80+
it('Restarts VM', async() => {
81+
await detailViewAction('Restart');
82+
await browser.wait(until.textToBePresentInElement(detailViewVMmStatus, 'Starting'), VM_BOOTUP_TIMEOUT);
83+
await browser.wait(until.textToBePresentInElement(detailViewVMmStatus, 'Running'), VM_BOOTUP_TIMEOUT);
84+
}, VM_ACTIONS_TIMEOUT);
85+
86+
it('Stops VM', async() => {
87+
await detailViewAction('Stop');
88+
await browser.wait(until.textToBePresentInElement(detailViewVMmStatus, 'Off'), 10000);
89+
});
90+
91+
it('Deletes VM', async() => {
92+
await detailViewAction('Delete');
93+
leakedResources.delete(JSON.stringify({name: vmName, namespace: testName, kind: 'vm'}));
94+
});
95+
});
96+
});

frontend/integration-tests/views/crud.view.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export const saveChangesBtn = $('#save-changes');
1313
export const reloadBtn = $('#reload-object');
1414
export const cancelBtn = $('#cancel');
1515

16+
export const confirmAction = () => browser.wait(until.presenceOf($('#confirm-action'))).then(() => $('#confirm-action').click());
17+
1618
/**
1719
* Returns a promise that resolves after the loading spinner is not present.
1820
*/
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { $, $$, browser, ExpectedConditions as until } from 'protractor';
2+
import { rowForName, confirmAction } from '../crud.view';
3+
4+
export const detailViewVMmStatus = $('#details-column-1 > dl:nth-child(1) > dd:nth-child(2)');
5+
export const listViewVMmStatus = (name: string) => rowForName(name).$('div.co-m-row:first-child > div:first-child > div:nth-child(3)');
6+
7+
const listViewKebabDropdown = '.co-kebab__button';
8+
const listViewKebabDropdownMenu = '.co-kebab__dropdown';
9+
const detailViewDropdown = '.co-m-nav-title button';
10+
const detailViewDropdownMenu = '.dropdown-menu-right';
11+
12+
/**
13+
* Selects option link from given dropdown element.
14+
*/
15+
const selectDropdownItem = (getActionsDropdown, getActionsDropdownMenu) => async(action) => {
16+
await getActionsDropdown().click();
17+
await getActionsDropdownMenu().$$('a').filter(link => link.getText().then(text => text.startsWith(action))).first().click();
18+
};
19+
20+
/**
21+
* Performs action for VM via list view kebab menu.
22+
*/
23+
export const listViewAction = (name) => async(action) => {
24+
const getActionsDropdown = () => rowForName(name).$$(listViewKebabDropdown).first();
25+
const getActionsDropdownMenu = () => rowForName(name).$(listViewKebabDropdownMenu);
26+
selectDropdownItem(getActionsDropdown, getActionsDropdownMenu)(action);
27+
await confirmAction();
28+
await browser.wait(until.not(until.presenceOf(rowForName(name).$(listViewKebabDropdownMenu))));
29+
};
30+
31+
/**
32+
* Performs action for VM on its detail page.
33+
*/
34+
export const detailViewAction = async(action) => {
35+
const getActionsDropdown = () => $$(detailViewDropdown).first();
36+
const getActionsDropdownMenu = () => $(detailViewDropdownMenu);
37+
selectDropdownItem(getActionsDropdown, getActionsDropdownMenu)(action);
38+
await confirmAction();
39+
await browser.wait(until.not(until.presenceOf($(detailViewDropdownMenu))));
40+
};
41+

0 commit comments

Comments
 (0)