mirror of
https://github.com/ceph/ceph
synced 2025-02-19 08:57:27 +00:00
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:
commit
ab1c42e3b1
@ -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
|
||||
~~~~~~~~~~~~
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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];
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user