]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Add rudimentary pool create/exist/delete test 28928/head
authorPatrick Nawracay <pnawracay@suse.com>
Mon, 1 Jul 2019 15:42:55 +0000 (17:42 +0200)
committerPatrick Nawracay <pnawracay@suse.com>
Wed, 17 Jul 2019 13:38:57 +0000 (15:38 +0200)
Fixes: http://tracker.ceph.com/issues/38093
Signed-off-by: Patrick Nawracay <pnawracay@suse.com>
src/pybind/mgr/dashboard/HACKING.rst
src/pybind/mgr/dashboard/frontend/e2e/helper.po.ts
src/pybind/mgr/dashboard/frontend/e2e/page-helper.po.ts
src/pybind/mgr/dashboard/frontend/e2e/pools/pools.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/e2e/pools/pools.po.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html

index 51e0c83bca8e3f21ccb306643493604dd1ead0c3..7316fb328f31957602db373ef94cca829d59e1da 100644 (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
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+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
 ~~~~~~~~~~~~
index 1dcdf5862f37acb0d48d18b2f007d12c68ca6e95..c0aa7f5d79a5013a1ac547d2faba32510f765e5d 100644 (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();
   }
 
   /**
index e0d9353220db6105edad3f6362918741daa360e2..72a93b4955a0166d3ee2ccbb7cdf86fa532eb650 100644 (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];
index 69a626f64789ff03cc963f36cd034beff2efed6a..302ff8255a9b5fec8191ccd383ee7a1907d43a03 100644 (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);
+    });
+  });
 });
index 4fa15b79c0636c3559fbfda5b176a8f2c466f391..9f110a79133f431a2a390eec388930b28d672482 100644 (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
+                    });
+                  });
+              });
+          });
+      });
+    });
+  }
 }
index 124a670d2353cb0d899c11e754407c76fe26f592..cdcf248522a3e8f4ab826eef4873caaca5b08d0b 100644 (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">
index 902d89328ba0b0c784a97dc4a8a44fd0d85573fe..e76a9f07dd11b97cd544abe3ffcbf02163c6369c 100644 (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>