Merge pull request #28928 from p-na/wip-pna-e2e-pools

mgr/dashboard: Write E2E tests for pool creation, deletion and verification

Reviewed-by: Adam King <adking@redhat.com>
Reviewed-by: Laura Paduano <lpaduano@suse.com>
Reviewed-by: Rafael Quintero <rquinter@redhat.com>
Reviewed-by: Ricardo Marques <rimarques@suse.com>
Reviewed-by: Tiago Melo <tmelo@suse.com>
This commit is contained in:
Ricardo Marques 2019-07-18 14:32:23 +01:00 committed by GitHub
commit ab1c42e3b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 241 additions and 40 deletions

View File

@ -170,47 +170,96 @@ Note:
When using docker, as your device, you might need to run the script with sudo
permissions.
Writing End-to-End Tests
~~~~~~~~~~~~~~~~~~~~~~~~
When writing E2E tests, it is not necessary to compile the frontend code on
each change of the test files. When your development environment is running
(``npm start``), you can point Protractor to just use this environment. To
attach `Protractor <http://www.protractortest.org/>`__ to this process, run
``npm run e2e:dev``.
When developing E2E tests, it is not necessary to compile the frontend code
on each change of the test files. When your development environment is
running (``npm start``), you can point Protractor to just use this
environment. To attach `Protractor <http://www.protractortest.org/>`__ to
this process, run ``npm run e2e:dev``.
Note::
In case you have a somewhat particular environment, you might need to adapt
`protractor.conf.js` to point to the appropriate destination.
Making code reuseable
~~~~~~~~~~~~~~~~~~~~~
Writing End-to-End Tests
~~~~~~~~~~~~~~~~~~~~~~~~
In order to make some code reuseable, you just need to put it in a derived
class of the ``PageHelper``. If you create a new class derived from the
``PageHelper``, please also register it in the ``Helper`` class, so that it can
automatically be used by all other classes. To do so, you just need to create a
new attribute on the ``Helper`` class and ensure it's instantiated in the
constructor of the ``Helper`` class.
The PagerHelper class
^^^^^^^^^^^^^^^^^^^^^
The ``PageHelper`` class is supposed to be used for general purpose code that
can be used on various pages or suites. Examples are
``getTableCellByContent()``, ``getTabsCount()`` or ``checkCheckbox()``. Every
method that could be useful on several pages belongs there. Also, methods
which enhance the derived classes of the PageHelper belong there. A good
example for such a case is the ``restrictTo()`` decorator. It ensures that a
method implemented in a subclass of PageHelper is called on the correct page.
It will also show a developer-friendly warning if this is not the case.
Subclasses of PageHelper
^^^^^^^^^^^^^^^^^^^^^^^^
Helper Methods
""""""""""""""
In order to make code reusable which is specific for a particular suite, make
sure to put it in a derived class of the ``PageHelper``. For instance, when
talking about the pool suite, such methods would be ``create()``, ``exist()``
and ``delete()``. These methods are specific to a pool but are useful for other
suites.
Methods that return HTML elements (for instance of type ``ElementFinder`` or
``ElementArrayFinder``, but also ``Promise<ElementFinder>``) which can only
be found on a specific page, should be either implemented in the helper
methods of the subclass of PageHelper or as own methods of the subclass of
PageHelper.
Registering a new PageHelper
""""""""""""""""""""""""""""
If you have to create a new Helper class derived from the ``PageHelper``,
please also ensure that it is instantiated in the constructor of the
``Helper`` class. That way it can automatically be used by all other suites.
.. code:: TypeScript
class Helper {
// ...
pools: PoolPageHelper;
class Helper {
// ...
pools: PoolPageHelper;
constructor() {
this.pools = new PoolPageHelper();
}
constructor() {
this.pools = new PoolPageHelper();
}
// ...
}
// ...
}
Using PageHelpers
"""""""""""""""""
In any suite, an instance of the ``Helper`` class should be used to call
various ``PageHelper`` objects and their methods. This makes all methods of all
PageHelpers available to all suites.
.. code:: TypeScript
it('should create a pool', () => {
helper.pools.exist(poolName, false).then(() => {
helper.pools.navigateTo('create');
helper.pools.create(poolName).then(() => {
helper.pools.navigateTo();
helper.pools.exist(poolName, true);
});
});
});
Code Style
^^^^^^^^^^
Please refer to the official `Protractor style-guide
<https://www.protractortest.org/#/style-guide>`__
for a better insight on how to write and structure tests
as well as what exactly should be covered by end-to-end tests.
<https://www.protractortest.org/#/style-guide>`__ for a better insight on how
to write and structure tests as well as what exactly should be covered by
end-to-end tests.
Further Help
~~~~~~~~~~~~

View File

@ -1,4 +1,5 @@
import { browser } from 'protractor';
import { PoolPageHelper } from './pools/pools.po';
import { BucketsPageHelper } from './rgw/buckets.po';
export class Helper {
@ -6,9 +7,11 @@ export class Helper {
static TIMEOUT = 30000;
buckets: BucketsPageHelper;
pools: PoolPageHelper;
constructor() {
this.buckets = new BucketsPageHelper();
this.pools = new PoolPageHelper();
}
/**

View File

@ -1,4 +1,4 @@
import { $, $$, browser, by, element } from 'protractor';
import { $, $$, browser, by, element, ElementFinder, promise } from 'protractor';
interface Pages {
index: string;
@ -47,8 +47,11 @@ export abstract class PageHelper {
return element.all(by.cssContainingText('.datatable-body-cell-label', content)).first();
}
// Used for instances where a modal container recieved the click rather than the
// desired element
/**
* Used for instances where a modal container received the click rather than the desired element.
*
* https://stackoverflow.com/questions/26211751/protractor-chrome-driver-element-is-not-clickable-at-point
*/
static moveClick(object) {
return browser
.actions()
@ -57,6 +60,62 @@ export abstract class PageHelper {
.perform();
}
/**
* Returns the cell with the content given in `content`. Will not return a
* rejected Promise if the table cell hasn't been found. It behaves this way
* to enable to wait for visiblity/invisiblity/precense of the returned
* element.
*
* It will return a rejected Promise if the result is ambigous, though. That
* means if after the search for content has been completed, but more than a
* single row is shown in the data table.
*/
static getTableCellByContent(content: string): promise.Promise<ElementFinder> {
const searchInput = $('#pool-list > div .search input');
const rowAmountInput = $('#pool-list > div > div > .dataTables_paginate input');
const footer = $('#pool-list > div datatable-footer');
rowAmountInput.clear();
rowAmountInput.sendKeys('10');
searchInput.clear();
searchInput.sendKeys(content);
return footer.getAttribute('ng-reflect-row-count').then((rowCount: string) => {
const count = Number(rowCount);
if (count !== 0 && count > 1) {
return Promise.reject('getTableCellByContent: Result is ambigous');
} else {
return element(
by.cssContainingText('.datatable-body-cell-label', new RegExp(`^\\s${content}\\s$`))
);
}
});
}
/**
* Decorator to be used on Helper methods to restrict access to one
* particular URL. This shall help developers to prevent and highlight
* mistakes. It also reduces boilerplate code and by thus, increases
* readability.
*/
static restrictTo(page): any {
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
const fn: Function = descriptor.value;
descriptor.value = function(...args) {
return browser
.getCurrentUrl()
.then((url) =>
url.endsWith(page)
? fn.apply(this, args)
: promise.Promise.reject(
`Method ${target.constructor.name}::${propertyKey} is supposed to be ` +
`run on path "${page}", but was run on URL "${url}"`
)
);
};
};
}
navigateTo(page = null) {
page = page || 'index';
const url = this.pages[page];

View File

@ -3,9 +3,13 @@ import { PoolPageHelper } from './pools.po';
describe('Pools page', () => {
let page: PoolPageHelper;
let helper: Helper;
const poolName = 'pool_e2e_pool_test';
beforeAll(() => {
page = new PoolPageHelper();
helper = new Helper();
page.navigateTo();
});
afterEach(() => {
@ -13,10 +17,6 @@ describe('Pools page', () => {
});
describe('breadcrumb and tab tests', () => {
beforeAll(() => {
page.navigateTo();
});
it('should open and show breadcrumb', () => {
expect(PoolPageHelper.getBreadcrumbText()).toEqual('Pools');
});
@ -33,4 +33,22 @@ describe('Pools page', () => {
expect(PoolPageHelper.getTabText(1)).toEqual('Overall Performance');
});
});
it('should create a pool', () => {
helper.pools.exist(poolName, false).then(() => {
helper.pools.navigateTo('create');
helper.pools.create(poolName, 8).then(() => {
helper.pools.navigateTo();
helper.pools.exist(poolName, true);
});
});
});
it('should delete a pool', () => {
helper.pools.exist(poolName);
helper.pools.delete(poolName).then(() => {
helper.pools.navigateTo();
helper.pools.exist(poolName, false);
});
});
});

View File

@ -1,8 +1,77 @@
import { $, browser, by, element, ElementFinder, promise, protractor } from 'protractor';
import { Helper } from '../helper.po';
import { PageHelper } from '../page-helper.po';
const EC = protractor.ExpectedConditions;
const pages = {
index: '/#/pool',
create: '/#/pool/create'
};
export class PoolPageHelper extends PageHelper {
pages = {
index: '/#/pool',
create: '/#/pool/create'
};
pages = pages;
private isPowerOf2(n: number): boolean {
// tslint:disable-next-line: no-bitwise
return (n & (n - 1)) === 0;
}
@PageHelper.restrictTo(pages.index)
exist(name: string, oughtToBePresent = true): promise.Promise<any> {
return PageHelper.getTableCellByContent(name).then((elem) => {
const waitFn = oughtToBePresent ? EC.visibilityOf(elem) : EC.invisibilityOf(elem);
return browser.wait(waitFn, Helper.TIMEOUT).catch(() => {
const visibility = oughtToBePresent ? 'invisible' : 'visible';
const msg = `Pool "${name}" is ${visibility}, but should not be. Waiting for a change timed out`;
return promise.Promise.reject(msg);
});
});
}
@PageHelper.restrictTo(pages.create)
create(name: string, placement_groups: number): promise.Promise<any> {
const nameInput = $('input[name=name]');
nameInput.clear();
if (!this.isPowerOf2(placement_groups)) {
return Promise.reject(`Placement groups ${placement_groups} are not a power of 2`);
}
return nameInput.sendKeys(name).then(() => {
element(by.cssContainingText('select[name=poolType] option', 'replicated'))
.click()
.then(() => {
expect(element(by.css('select[name=poolType] option:checked')).getText()).toBe(
' replicated '
);
$('input[name=pgNum]')
.sendKeys(protractor.Key.CONTROL, 'a', protractor.Key.NULL, placement_groups)
.then(() => {
return element(by.css('cd-submit-button')).click();
});
});
});
}
@PageHelper.restrictTo(pages.index)
delete(name: string): promise.Promise<any> {
return PoolPageHelper.getTableCellByContent(name).then((tableCell: ElementFinder) => {
return tableCell.click().then(() => {
return $('.table-actions button.dropdown-toggle') // open submenu
.click()
.then(() => {
return $('li.delete a') // click on "delete" menu item
.click()
.then(() => {
const getConfirmationCheckbox = () => $('#confirmation');
browser
.wait(() => EC.visibilityOf(getConfirmationCheckbox()), Helper.TIMEOUT)
.then(() => {
PageHelper.moveClick(getConfirmationCheckbox()).then(() => {
return element(by.cssContainingText('button', 'Delete Pool')).click(); // Click Delete item
});
});
});
});
});
});
}
}

View File

@ -5,16 +5,19 @@
[statusFor]="viewCacheStatus.statusFor"></cd-view-cache>
<cd-table #table
id="pool-list"
[data]="pools"
[columns]="columns"
selectionType="single"
(updateSelection)="updateSelection($event)">
<cd-table-actions class="table-actions"
<cd-table-actions id="pool-list-actions"
class="table-actions"
[permission]="permissions.pool"
[selection]="selection"
[tableActions]="tableActions">
</cd-table-actions>
<cd-pool-details cdTableDetail
id="pool-list-details"
[selection]="selection"
[permissions]="permissions"
[cacheTiers]="selectionCacheTiers">

View File

@ -15,7 +15,7 @@
<!-- end filters -->
<!-- search -->
<div class="input-group">
<div class="input-group search">
<span class="input-group-prepend">
<span class="input-group-text">
<i [ngClass]="[icons.search]"></i>