From: Patrick Nawracay Date: Mon, 1 Jul 2019 15:42:55 +0000 (+0200) Subject: mgr/dashboard: Add rudimentary pool create/exist/delete test X-Git-Tag: v15.1.0~2177^2 X-Git-Url: http://git.apps.os.sepia.ceph.com/?a=commitdiff_plain;h=d5b508809e6c1c77b269bdbfaa5e94478b362ad8;p=ceph-ci.git mgr/dashboard: Add rudimentary pool create/exist/delete test Fixes: http://tracker.ceph.com/issues/38093 Signed-off-by: Patrick Nawracay --- diff --git a/src/pybind/mgr/dashboard/HACKING.rst b/src/pybind/mgr/dashboard/HACKING.rst index 51e0c83bca8..7316fb328f3 100644 --- a/src/pybind/mgr/dashboard/HACKING.rst +++ b/src/pybind/mgr/dashboard/HACKING.rst @@ -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 `__ 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 `__ 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``) 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 -`__ -for a better insight on how to write and structure tests -as well as what exactly should be covered by end-to-end tests. +`__ 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 ~~~~~~~~~~~~ diff --git a/src/pybind/mgr/dashboard/frontend/e2e/helper.po.ts b/src/pybind/mgr/dashboard/frontend/e2e/helper.po.ts index 1dcdf5862f3..c0aa7f5d79a 100644 --- a/src/pybind/mgr/dashboard/frontend/e2e/helper.po.ts +++ b/src/pybind/mgr/dashboard/frontend/e2e/helper.po.ts @@ -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(); } /** diff --git a/src/pybind/mgr/dashboard/frontend/e2e/page-helper.po.ts b/src/pybind/mgr/dashboard/frontend/e2e/page-helper.po.ts index e0d9353220d..72a93b4955a 100644 --- a/src/pybind/mgr/dashboard/frontend/e2e/page-helper.po.ts +++ b/src/pybind/mgr/dashboard/frontend/e2e/page-helper.po.ts @@ -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 { + 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]; diff --git a/src/pybind/mgr/dashboard/frontend/e2e/pools/pools.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/e2e/pools/pools.e2e-spec.ts index 69a626f6478..302ff8255a9 100644 --- a/src/pybind/mgr/dashboard/frontend/e2e/pools/pools.e2e-spec.ts +++ b/src/pybind/mgr/dashboard/frontend/e2e/pools/pools.e2e-spec.ts @@ -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); + }); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/e2e/pools/pools.po.ts b/src/pybind/mgr/dashboard/frontend/e2e/pools/pools.po.ts index 4fa15b79c06..9f110a79133 100644 --- a/src/pybind/mgr/dashboard/frontend/e2e/pools/pools.po.ts +++ b/src/pybind/mgr/dashboard/frontend/e2e/pools/pools.po.ts @@ -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 { + 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 { + 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 { + 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 + }); + }); + }); + }); + }); + }); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.html index 124a670d235..cdcf248522a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.html @@ -5,16 +5,19 @@ [statusFor]="viewCacheStatus.statusFor"> - diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html index 902d89328ba..e76a9f07dd1 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html @@ -15,7 +15,7 @@ -
+