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
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+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.
-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.
+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
~~~~~~~~~~~~
import { browser } from 'protractor';
+import { PoolPageHelper } from './pools/pools.po';
import { BucketsPageHelper } from './rgw/buckets.po';
export class Helper {
static TIMEOUT = 30000;
buckets: BucketsPageHelper;
+ pools: PoolPageHelper;
constructor() {
this.buckets = new BucketsPageHelper();
+ this.pools = new PoolPageHelper();
}
/**
-import { $, $$, browser, by, element } from 'protractor';
+import { $, $$, browser, by, element, ElementFinder, promise } from 'protractor';
interface Pages {
index: string;
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()
.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];
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(() => {
});
describe('breadcrumb and tab tests', () => {
- beforeAll(() => {
- page.navigateTo();
- });
-
it('should open and show breadcrumb', () => {
expect(PoolPageHelper.getBreadcrumbText()).toEqual('Pools');
});
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);
+ });
+ });
});
+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
+ });
+ });
+ });
+ });
+ });
+ });
+ }
}
[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">
<!-- 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>