Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions frontend/integration-tests/protractor.conf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@ export const config: Config = {
]),
'kubevirt-plugin': suite([
'../packages/kubevirt-plugin/integration-tests/tests/vm.wizard.scenario.ts',
'../packages/kubevirt-plugin/integration-tests/tests/vm.actions.scenario.ts',
'../packages/kubevirt-plugin/integration-tests/tests/vm.migration.scenario.ts',
'../packages/kubevirt-plugin/integration-tests/tests/vm.resources.scenario.ts',
]),
all: suite([
'tests/crud.scenario.ts',
Expand Down
13 changes: 13 additions & 0 deletions frontend/packages/console-shared/src/test-utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,16 @@ export const waitForStringNotInElement = (elem: ElementFinder, needle: string) =
return !content.includes(needle);
};
};

/**
* Search YAML manifest for a given string. Return true if found.
* @param {string} needle String to search in YAML.
* @param {string} name Name of the resource.
* @param {string} namespace Namespace of the resource.
* @param {string} kind Kind of the resource.
* @returns {boolean} True if found, false otherwise.
*/
export function searchYAML(needle: string, name: string, namespace: string, kind: string): boolean {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is an old PR that's already merged, but this isn't the way we should test for content in the resources. Instead, we should

  1. Get the JSON with -o json
  2. Parse the JSON
  3. Check the specific property we're interested in

The searchYAML approach is bad because it can result in incorrect failures due to different quoting styles in the YAML or it can result in false positives where the string is matched somewhere else in the document.

Can we look at removing this utility and replacing its usage? At the very least, I'd like to move this out of console-shared to discourage its use elsewhere.

@rhrazdil

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, it's currently only used in old kubevirt tests, I'll update those and remove this method from console-shared

const result = execSync(`kubectl get -o yaml -n ${namespace} ${kind} ${name}`).toString();
return result.search(needle) >= 0;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { browser } from 'protractor';
import { appHost, testName } from '../../../../../integration-tests/protractor.conf';
import { clickHorizontalTab } from '../../../../../integration-tests/views/horizontal-nav.view';
import { isLoaded, resourceTitle } from '../../../../../integration-tests/views/crud.view';
import { activeTab } from '../../views/detailView.view';

export class DetailView {
readonly name: string;
Expand All @@ -25,12 +26,15 @@ export class DetailView {
await browser.get(`${appHost}/k8s/ns/${this.namespace}/${this.kind}/${this.name}`);
await isLoaded();
}
await clickHorizontalTab(tabName);
await isLoaded();
if ((await activeTab.getText()) !== tabName) {
await clickHorizontalTab(tabName);
await isLoaded();
}
}

async navigateToListView() {
const vmsListUrl = (namespace) => `${appHost}/k8s/ns/${namespace}/${this.kind}`;
const vmsListUrl = (namespace) =>
`${appHost}/k8s/${namespace === 'all-namespaces' ? '' : 'ns/'}${namespace}/${this.kind}`;
const currentUrl = await browser.getCurrentUrl();
if (![vmsListUrl(testName), vmsListUrl('all-namespaces')].includes(currentUrl)) {
await browser.get(vmsListUrl(this.namespace));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
/* eslint-disable no-await-in-loop */
import { selectDropdownOption, click } from '../../../../console-shared/src/test-utils/utils';
import { browser, ExpectedConditions as until } from 'protractor';
import {
selectDropdownOption,
click,
waitForCount,
} from '../../../../console-shared/src/test-utils/utils';
import { isLoaded, resourceRows } from '../../../../../integration-tests/views/crud.view';
import { TABS, diskTabCol, networkTabCol } from '../utils/consts';
import { TABS, diskTabCol, networkTabCol, PAGE_LOAD_TIMEOUT_SECS } from '../utils/consts';
import { StorageResource, NetworkResource } from '../utils/types';
import { fillInput } from '../utils/utils';
import * as kubevirtDetailView from '../../views/kubevirtDetailView.view';
Expand All @@ -11,31 +16,29 @@ import { DetailView } from './detailView';
export class KubevirtDetailView extends DetailView {
async getAttachedDisks(): Promise<StorageResource[]> {
await this.navigateToTab(TABS.DISKS);
const resources = [];
for (const row of await resourceRows) {
const cells = row.$$('div');
resources.push({
name: await cells.get(diskTabCol.name).getText(),
size: (await cells.get(diskTabCol.size).getText()).match(/^\d*/)[0],
storageClass: await cells.get(diskTabCol.storageClass).getText(),
});
}
return resources;
const rows = await kubevirtDetailView.tableRows();
return rows.map((line) => {
const cols = line.split(/\s+/);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you please explain how is the table text and rows are resolved? How come we get the columns just by splitting it by spaces?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well calling getText() on the table returns string in the following format

attr00 attr01 ... attr0n\n
attr10 attr11 ... attr1n\n

So forEach iterates over lines and thus I can split the columns by spaces.

return {
name: cols[diskTabCol.name],
size: cols[diskTabCol.size].slice(0, -2),
storageClass: cols[diskTabCol.storageClass],
};
});
}

async getAttachedNICs(): Promise<NetworkResource[]> {
await this.navigateToTab(TABS.NICS);
const resources = [];
for (const row of await resourceRows) {
const cells = row.$$('div');
resources.push({
name: await cells.get(networkTabCol.name).getText(),
mac: await cells.get(networkTabCol.mac).getText(),
networkDefinition: await cells.get(networkTabCol.networkDefinition).getText(),
binding: await cells.get(networkTabCol.binding).getText(),
});
}
return resources;
const rows = await kubevirtDetailView.tableRows();
return rows.map((line) => {
const cols = line.split(/\s+/);
return {
name: cols[networkTabCol.name],
mac: cols[networkTabCol.mac],
networkDefinition: cols[networkTabCol.networkDefinition],
binding: cols[networkTabCol.binding],
};
});
}

async addDisk(disk: StorageResource) {
Expand All @@ -50,8 +53,10 @@ export class KubevirtDetailView extends DetailView {

async removeDisk(name: string) {
await this.navigateToTab(TABS.DISKS);
const count = await resourceRows.count();
await kubevirtDetailView.selectKebabOption(name, 'Delete');
await confirmAction();
await browser.wait(until.and(waitForCount(resourceRows, count - 1)), PAGE_LOAD_TIMEOUT_SECS);
}

async addNIC(nic: NetworkResource) {
Expand All @@ -67,7 +72,9 @@ export class KubevirtDetailView extends DetailView {

async removeNIC(name: string) {
await this.navigateToTab(TABS.NICS);
const count = await resourceRows.count();
await kubevirtDetailView.selectKebabOption(name, 'Delete');
await confirmAction();
await browser.wait(until.and(waitForCount(resourceRows, count - 1)), PAGE_LOAD_TIMEOUT_SECS);
}
}
Original file line number Diff line number Diff line change
@@ -1,110 +1,58 @@
/* eslint-disable no-await-in-loop, no-console */
import { browser, ExpectedConditions as until } from 'protractor';
import { testName } from '../../../../../integration-tests/protractor.conf';
import { resourceTitle, isLoaded } from '../../../../../integration-tests/views/crud.view';
import { isLoaded } from '../../../../../integration-tests/views/crud.view';
import {
selectDropdownOption,
waitForStringNotInElement,
resolveTimeout,
} from '../../../../console-shared/src/test-utils/utils';
import * as vmView from '../../views/virtualMachine.view';
import { nameInput, errorMessage } from '../../views/wizard.view';
import { errorMessage } from '../../views/wizard.view';
import { VMConfig } from '../utils/types';
import {
PAGE_LOAD_TIMEOUT_SECS,
VM_BOOTUP_TIMEOUT_SECS,
VM_STOP_TIMEOUT_SECS,
VM_ACTIONS_TIMEOUT_SECS,
WIZARD_CREATE_VM_ERROR,
UNEXPECTED_ACTION_ERROR,
TABS,
WIZARD_TABLE_FIRST_ROW,
TABS,
DASHES,
} from '../utils/consts';
import { listViewAction } from '../../views/vm.actions.view';
import { rowForName } from '../../views/kubevirtDetailView.view';
import { KubevirtDetailView } from './kubevirtDetailView';
import { detailViewAction } from '../../views/vm.actions.view';
import { tableRowForName } from '../../views/kubevirtDetailView.view';
import { Wizard } from './wizard';
import { KubevirtDetailView } from './kubevirtDetailView';
import { VirtualMachineInstance } from './virtualMachineInstance';

export class VirtualMachine extends KubevirtDetailView {
constructor(config) {
super({ ...config, kind: 'virtualmachines' });
}

async navigateToVMI(vmiTab: string): Promise<VirtualMachineInstance> {
await this.navigateToTab(TABS.OVERVIEW);
const vmPodName = await vmView
.vmDetailPod(this.namespace, this.name)
.$('a')
.getText();
const vmi = new VirtualMachineInstance({ name: vmPodName, namespace: testName });
await vmi.navigateToTab(vmiTab);
return vmi;
}

async action(action: string, waitForAction?: boolean, timeout?: number) {
await this.navigateToListView();
await this.navigateToTab(TABS.OVERVIEW);

let confirmDialog = true;
if (['Clone'].includes(action)) {
confirmDialog = false;
}

await listViewAction(this.name)(action, confirmDialog);
await detailViewAction(action, confirmDialog);
if (waitForAction !== false) {
switch (action) {
case 'Start':
await this.waitForStatusIcon(
vmView.statusIcons.running,
resolveTimeout(timeout, VM_BOOTUP_TIMEOUT_SECS),
);
break;
case 'Restart':
await this.waitForStatusIcon(
vmView.statusIcons.starting,
resolveTimeout(timeout, VM_BOOTUP_TIMEOUT_SECS),
);
await this.waitForStatusIcon(
vmView.statusIcons.running,
resolveTimeout(timeout, VM_BOOTUP_TIMEOUT_SECS),
);
break;
case 'Stop':
await this.waitForStatusIcon(
vmView.statusIcons.off,
resolveTimeout(timeout, VM_STOP_TIMEOUT_SECS),
);
break;
case 'Clone':
await browser.wait(
until.presenceOf(nameInput),
resolveTimeout(timeout, PAGE_LOAD_TIMEOUT_SECS),
);
await browser.sleep(500); // Wait until the fade in effect is finished, otherwise we may misclick
break;
case 'Migrate':
await this.waitForStatusIcon(
vmView.statusIcons.migrating,
resolveTimeout(timeout, PAGE_LOAD_TIMEOUT_SECS),
);
await this.waitForStatusIcon(
vmView.statusIcons.running,
resolveTimeout(timeout, VM_ACTIONS_TIMEOUT_SECS),
);
break;
case 'Cancel':
await this.waitForStatusIcon(
vmView.statusIcons.running,
resolveTimeout(timeout, PAGE_LOAD_TIMEOUT_SECS),
);
break;
case 'Delete':
// wait for redirect
await browser.wait(
until.textToBePresentInElement(resourceTitle, 'Virtual Machines'),
resolveTimeout(timeout, PAGE_LOAD_TIMEOUT_SECS),
);
break;
default:
throw Error(UNEXPECTED_ACTION_ERROR);
}
await vmView.waitForActionFinished(action, timeout);
}
}

async waitForStatusIcon(statusIcon: string, timeout: number) {
await this.navigateToTab(TABS.OVERVIEW);
await browser.wait(until.presenceOf(vmView.statusIcon(statusIcon)), timeout);
}

async waitForMigrationComplete(fromNode: string, timeout: number) {
await browser.wait(
until.and(
Expand All @@ -116,7 +64,7 @@ export class VirtualMachine extends KubevirtDetailView {
}

async resourceExists(resourceName: string) {
return rowForName(resourceName).isPresent();
return tableRowForName(resourceName).isPresent();
}

async selectConsole(type: string) {
Expand Down Expand Up @@ -214,7 +162,7 @@ export class VirtualMachine extends KubevirtDetailView {

if (startOnCreation === true) {
// If startOnCreation is true, wait for VM to boot up
await this.waitForStatusIcon(vmView.statusIcons.running, VM_BOOTUP_TIMEOUT_SECS);
await vmView.waitForStatusIcon(vmView.statusIcons.running, VM_BOOTUP_TIMEOUT_SECS);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/* eslint-disable no-unused-vars, no-undef, no-await-in-loop, no-console */
import { volumeRows } from '../../views/virtualMachineInstance.view';
import { DetailView } from './detailView';

export class VirtualMachineInstance extends DetailView {
constructor(vmiConfig) {
super({ ...vmiConfig, kind: 'pods' });
}

async getVolumes() {
const disks = [];
for (const row of await volumeRows) {
disks.push(
await row
.$$('td')
.first()
.getText(),
);
}
return disks;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const PAGE_LOAD_TIMEOUT_SECS = 15 * SEC;
export const TEMPLATE_ACTIONS_TIMEOUT_SECS = 90 * SEC;
export const VM_ACTIONS_TIMEOUT_SECS = 250 * SEC;
export const VM_BOOTUP_TIMEOUT_SECS = 200 * SEC;
export const VM_MIGRATION_TIMEOUT_SECS = 150 * SEC;
export const VM_MIGRATION_TIMEOUT_SECS = 190 * SEC;
export const VM_STOP_TIMEOUT_SECS = 10 * SEC;
export const VM_IP_ASSIGNMENT_TIMEOUT_SECS = 180 * SEC;
export const WINDOWS_IMPORT_TIMEOUT_SECS = 150 * SEC;
Expand Down Expand Up @@ -48,6 +48,17 @@ export const TABS = {
};
Object.freeze(TABS);

// Tab names
export const VMACTIONS = {
START: 'Start',
STOP: 'Stop',
CLONE: 'Clone',
RESTART: 'Restart',
MIGRATE: 'Events',
DELETE: 'Delete',
};
Object.freeze(VMACTIONS);

// Network tab columns in VM Wizard
export const networkWizardTabCol = {
name: 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,6 @@ export function getRandomMacAddress() {
}

export function getResourceObject(name: string, namespace: string, kind: string) {
const resourceJson = execSync(`oc get -o json -n ${namespace} ${kind} ${name}`).toString();
const resourceJson = execSync(`kubectl get -o json -n ${namespace} ${kind} ${name}`).toString();
return JSON.parse(resourceJson);
}
Loading