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 @@ -165,6 +165,9 @@ export const config: Config = {
'tests/monitoring.scenario.ts',
'tests/crd-extensions.scenario.ts',
]),
'kubevirt-plugin': suite([
'../packages/kubevirt-plugin/integration-tests/tests/vm.wizard.scenario.ts',
]),
all: suite([
'tests/crud.scenario.ts',
'tests/overview/overview.scenareio.ts',
Expand Down
145 changes: 145 additions & 0 deletions frontend/packages/console-shared/src/test-utils/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/* eslint-disable no-await-in-loop, no-console, no-underscore-dangle */
Copy link
Contributor

Choose a reason for hiding this comment

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

If the runtime for this code is Node.js (i.e. Protractor or Jest) then you can simply add

/* eslint-env node */

at the top of the file.

By doing that, the above rule disable list might not be necessary.

Copy link
Author

Choose a reason for hiding this comment

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

didn't help

import { execSync } from 'child_process';
import {
$,
by,
ElementFinder,
browser,
ExpectedConditions as until,
element,
ElementArrayFinder,
} from 'protractor';
import { config } from '../../../../integration-tests/protractor.conf';

export function resolveTimeout(timeout: number, defaultTimeout: number) {
return timeout !== undefined ? timeout : defaultTimeout;
}

export function removeLeakedResources(leakedResources: Set<string>) {
const leakedArray: string[] = [...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}`);
}
});
}
leakedResources.clear();
}

export function addLeakableResource(leakedResources: Set<string>, resource) {
leakedResources.add(
JSON.stringify({
name: resource.metadata.name,
namespace: resource.metadata.namespace,
kind: resource.kind,
}),
);
}

export function removeLeakableResource(leakedResources: Set<string>, resource) {
leakedResources.delete(
JSON.stringify({
name: resource.metadata.name,
namespace: resource.metadata.namespace,
kind: resource.kind,
}),
);
}

export function createResource(resource) {
execSync(`echo '${JSON.stringify(resource)}' | kubectl create -f -`);
}

export function createResources(resources) {
resources.forEach(createResource);
}

export function deleteResource(resource) {
const kind = resource.kind === 'NetworkAttachmentDefinition' ? 'net-attach-def' : resource.kind;
execSync(
`kubectl delete -n ${resource.metadata.namespace} --cascade ${kind} ${resource.metadata.name}`,
);
}

export function deleteResources(resources) {
resources.forEach(deleteResource);
}

export async function withResource(
resourceSet: Set<string>,
resource: any,
callback: Function,
keepResource: boolean = false,
) {
addLeakableResource(resourceSet, resource);
await callback();
if (!keepResource) {
deleteResource(resource);
removeLeakableResource(resourceSet, resource);
}
}

export async function click(elem: ElementFinder, timeout?: number) {
const _timeout = resolveTimeout(timeout, config.jasmineNodeOpts.defaultTimeoutInterval);
await browser.wait(until.elementToBeClickable(elem), _timeout);
await elem.click();
}

export async function selectDropdownOption(dropdownId: string, option: string) {
await click($(dropdownId));
await browser.wait(until.presenceOf(element(by.linkText(option))));
await $(`${dropdownId} + ul`)
.element(by.linkText(option))
.click();
}

export async function getDropdownOptions(dropdownId: string): Promise<string[]> {
const options = [];
await $(`${dropdownId} + ul`)
.$$('li')
.each((elem) => {
elem
.getText()
.then((text) => {
options.push(text);
})
.catch((err) => {
return Promise.reject(err);
});
});
return options;
}

export async function asyncForEach(iterable, callback) {
const array = [...iterable];
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
}

export const waitForCount = (elementArrayFinder: ElementArrayFinder, targetCount: number) => {
return async () => {
const count = await elementArrayFinder.count();
return count === targetCount;
};
};

export const waitForStringInElement = (elem: ElementFinder, needle: string) => {
return async () => {
const content = await elem.getText();
return content.includes(needle);
};
};

export const waitForStringNotInElement = (elem: ElementFinder, needle: string) => {
return async () => {
const content = await elem.getText();
return !content.includes(needle);
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
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';

export class DetailView {
readonly name: string;

readonly namespace: string;

readonly kind: string;

constructor(instance) {
this.name = instance.name;
this.namespace = instance.namespace;
this.kind = instance.kind;
}

static async getResourceTitle() {
return resourceTitle.getText();
}

async navigateToTab(tabName: string) {
if (!(await resourceTitle.isPresent()) || (await resourceTitle.getText()) !== this.name) {
await browser.get(`${appHost}/k8s/ns/${this.namespace}/${this.kind}/${this.name}`);
await isLoaded();
}
await clickHorizontalTab(tabName);
await isLoaded();
}

async navigateToListView() {
const vmsListUrl = (namespace) => `${appHost}/k8s/ns/${namespace}/${this.kind}`;
const currentUrl = await browser.getCurrentUrl();
if (![vmsListUrl(testName), vmsListUrl('all-namespaces')].includes(currentUrl)) {
await browser.get(vmsListUrl(this.namespace));
await isLoaded();
}
}

asResource() {
return {
kind: this.kind,
metadata: {
namespace: this.namespace,
name: this.name,
},
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/* eslint-disable no-await-in-loop */
Copy link
Contributor

Choose a reason for hiding this comment

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

You can disable or tweak no-await-in-loop (and other rules too) in the appropriate .eslintrc.js file.

import { selectDropdownOption, click } from '../../../../console-shared/src/test-utils/utils';
import { isLoaded, resourceRows } from '../../../../../integration-tests/views/crud.view';
import { TABS, diskTabCol, networkTabCol } from '../utils/consts';
import { StorageResource, NetworkResource } from '../utils/types';
import { fillInput } from '../utils/utils';
import * as kubevirtDetailView from '../../views/kubevirtDetailView.view';
import { confirmAction } from '../../views/vm.actions.view';
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;
}

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;
}

async addDisk(disk: StorageResource) {
await this.navigateToTab(TABS.DISKS);
await click(kubevirtDetailView.createDisk, 1000);
await fillInput(kubevirtDetailView.diskName, disk.name);
await fillInput(kubevirtDetailView.diskSize, disk.size);
await selectDropdownOption(kubevirtDetailView.diskStorageClassDropdownId, disk.storageClass);
await click(kubevirtDetailView.applyBtn);
await isLoaded();
}

async removeDisk(name: string) {
await this.navigateToTab(TABS.DISKS);
await kubevirtDetailView.selectKebabOption(name, 'Delete');
await confirmAction();
}

async addNIC(nic: NetworkResource) {
await this.navigateToTab(TABS.NICS);
Copy link
Contributor

Choose a reason for hiding this comment

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

I really like the way how we express the logical operations here. It's highly readable and clean!

await click(kubevirtDetailView.createNic, 1000);
await fillInput(kubevirtDetailView.nicName, nic.name);
await selectDropdownOption(kubevirtDetailView.networkTypeDropdownId, nic.networkDefinition);
await selectDropdownOption(kubevirtDetailView.networkBindingId, nic.binding);
await fillInput(kubevirtDetailView.macAddress, nic.mac);
await click(kubevirtDetailView.applyBtn);
await isLoaded();
}

async removeNIC(name: string) {
await this.navigateToTab(TABS.NICS);
await kubevirtDetailView.selectKebabOption(name, 'Delete');
await confirmAction();
}
}
Loading